Write Docker series yourself -- 6.2 creating a network

brief introduction

In the previous article, we completed an IP management and allocation tool class, which can automatically allocate and recycle the IP of the subnet. This article will continue to the previous article and start the creation of the network

Source code description

At the same time, it is put on Gitee and Github, which can be obtained

The version label corresponding to this chapter is: 6.2. To prevent too many codes from being difficult to view, you can switch to the label version for viewing

code implementation

The main ideas are as follows:

  • Create a network: read the subnet entered by the user, how to take the first as the gateway ip, and create a virtual network (or network card); After the creation is successful, the network information is serialized into JSON and saved locally for subsequent reading
  • Delete network: delete the corresponding network according to the entered virtual network name

The following is the specific implementation:

network command added

First, add relevant command operations:

Add network command operation

func main() {
	......
	
	app.Commands = []cli.Command{
		command.InitCommand,
		command.RunCommand,
		command.CommitCommand,
		command.ListCommand,
		command.LogCommand,
		command.ExecCommand,
		command.StopCommand,
		command.RemoveCommand,
		command.NetworkCommand,
	}
	......
}

network related detailed commands

var NetworkCommand = cli.Command{
	Name:  "network",
	Usage: "container network commands",
	Subcommands: []cli.Command{
		{
			Name:  "create",
			Usage: "create a container network",
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "driver",
					Usage: "network driver",
				},
				cli.StringFlag{
					Name:  "subnet",
					Usage: "subnet cidr",
				},
			},
			Action: func(context *cli.Context) error {
				if len(context.Args()) < 1 {
					return fmt.Errorf("missing network name")
				}

				err := network.Init()
				if err != nil {
					return err
				}

				driver := context.String("driver")
				subnet := context.String("subnet")
				name := context.Args()[0]
				if err := network.CreateNetwork(driver, subnet, name); err != nil {
					return err
				}
				return nil
			},
		},

		{
			Name:  "list",
			Usage: "list container network",
			Action: func(context *cli.Context) error {
				if err := network.Init(); err != nil {
					return err
				}
				return network.ListNetwork()
			},
		},

		{
			Name:  "remove",
			Usage: "remove container network",
			Action: func(context *cli.Context) error {
				if len(context.Args()) < 1 {
					return fmt.Errorf("missing network name")
				}
				if err := network.Init(); err != nil {
					return err
				}
				if err := network.DeleteNetwork(context.Args()[0]); err != nil {
					return err
				}
				return nil
			},
		},
	},
}

As above, the operations of creating new network, viewing network list and deleting network are added

Create a new network

When the command starts, pass in the subnet and virtual network name, and we create the virtual network according to these

func CreateNetwork(driver, subnet, name string) error {
	nw, err := drivers[driver].Create(subnet, name)
	if err != nil {
		return err
	}
	log.Infof("create network success")
	return nw.dump(defaultNetworkPath)
}

The following Network information: Network, some minor modifications have been made, GatewayIP (gateway IP) and Subnet (Subnet segment information) have been added, and they have been saved. Because the IpRange field is used, our IP recycling operation cannot be used normally

func (b *BridgeNetworkDriver) Create(subnet string, name string) (*NetWork, error) {
	_, ipRange, _ := net.ParseCIDR(subnet)
	// Get the first IP of the network segment as the gateway IP
	ip, err := ipAllocator.Allocate(ipRange)
	if err != nil {
		return nil, err
	}
	ipRange.IP = ip
	n := &NetWork{
		Name:      name,
		IpRange:   ipRange,
		Driver:    b.Name(),
		GatewayIP: ip,
		Subnet:    subnet,
	}
	log.Infof("BridgeNetworkDriver creat network subnet: %s, gateway ip: %s", ipRange.String(), ip.String())
	// Initialize virtual network
	return n, b.initBridge(n)
}

The following is the specific virtual network creation, which basically uses system functions (bloggers don't know much about this at present, so they can't say anything. Copy it first...)

func (b *BridgeNetworkDriver) initBridge(n *NetWork) error {
	// try to get bridge by name, if it already exists then just exit
	bridgeName := n.Name
	if err := createBridgeInterface(bridgeName); err != nil {
		return err
	}
	log.Infof("createBridgeInterface success")

	// set bridge ip
	gatewayIp := *n.IpRange
	gatewayIp.IP = n.IpRange.IP

	if err := setInterfaceIp(bridgeName, gatewayIp.String()); err != nil {
		return err
	}
	log.Infof("setInterfaceIp success")

	if err := setInterfaceUp(bridgeName); err != nil {
		return err
	}
	log.Infof("crsetInterfaceUp success")

	if err := setupIpTables(bridgeName, n.IpRange); err != nil {
		return err
	}
	log.Infof("setInterfaceUp success")
	return nil
}
func createBridgeInterface(bridgeName string) error {
	_, err := net.InterfaceByName(bridgeName)
	if err == nil || !strings.Contains(err.Error(), "no such network interface") {
		return err
	}

	// create *netlink.Bridge object
	la := netlink.NewLinkAttrs()
	la.Name = bridgeName

	br := &netlink.Bridge{LinkAttrs: la}
	if err := netlink.LinkAdd(br); err != nil {
		return fmt.Errorf("bridge creating failed for bridge %s, err: %w", bridgeName, err)
	}
	return nil
}

func setInterfaceUp(interfaceName string) error {
	iface, err := netlink.LinkByName(interfaceName)
	if err != nil {
		return fmt.Errorf("error retrieving a link named [ %s ], err: %w", iface.Attrs().Name, err)
	}

	if err := netlink.LinkSetUp(iface); err != nil {
		return fmt.Errorf("error enabling interface for %s, err: %w", interfaceName, err)
	}
	return nil
}

func setInterfaceIp(name string, rawIp string) error {
	retries := 2
	var iface netlink.Link
	var err error
	for i := 0; i < retries; i++ {
		iface, err = netlink.LinkByName(name)
		if err == nil {
			break
		}
		log.Debugf("error retrieving new bridge netlink link [ %s ]... retrying", name)
		time.Sleep(2 * time.Second)
	}
	if err != nil {
		return fmt.Errorf("abandoning retrieving the new bridge ink from netlink, run [ip link] to troubleshoot the err: %w", err)
	}

	ipNet, err := netlink.ParseIPNet(rawIp)
	if err != nil {
		return fmt.Errorf("netlink.ParseIPNet err: %w", err)
	}

	addr := &netlink.Addr{
		IPNet:     ipNet,
		Peer:      ipNet,
		Label:     "",
		Flags:     0,
		Scope:     0,
		Broadcast: nil,
	}
	return netlink.AddrAdd(iface, addr)
}

func setupIpTables(bridgeName string, subnet *net.IPNet) error {
	iptablesCmd := fmt.Sprintf("-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE", subnet.String(), bridgeName)
	cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
	output, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("iptablse err %v, %w", output, err)
	}
	return nil
}

The above is the code related to the creation of virtual network

View existing virtual networks

When viewing, you need to initialize the network configuration information and Init function

We have written a fixed location of network configuration information. All network configuration files will be stored below with their names, and the network driver and configuration will also be loaded into memory

var (
	// Default storage location of network configuration
	defaultNetworkPath = "/var/run/mydocker/network/network/"
	// Network drivers for different networks
	drivers            = map[string]NetworkDriver{}
	// Network configuration of different networks
	networks           = map[string]*NetWork{}
)

The basic principle of network information initialization is to read the default storage folder of network configuration and load all network configurations:

func Init() error {
	var bridgeDriver = BridgeNetworkDriver{}
	drivers[bridgeDriver.Name()] = &bridgeDriver

	if _, err := os.Stat(defaultNetworkPath); err != nil {
		if os.IsNotExist(err) {
			_ = os.MkdirAll(defaultNetworkPath, 0644)
		} else {
			return err
		}
	}

	_ = filepath.Walk(defaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error {
		if strings.HasSuffix(nwPath, "/") {
			return nil
		}
		_, nwName := path.Split(nwPath)
		nw := &NetWork{
			Name: nwName,
		}

		if err := nw.load(nwPath); err != nil {
			log.Errorf("error load network: %v", err)
		}

		networks[nwName] = nw
		return nil
	})
	return nil
}

It's easy to view the network configuration. Just traverse and print:

func ListNetwork() error {
	w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
	_, _ = fmt.Fprint(w, "NAME\tIpRange\tDriver\n")
	for _, nw := range networks {
		fmt.Fprintf(w, "%s\t%s\t\t%s\n",
			nw.Name,
			nw.IpRange.String(),
			nw.Driver,
		)
	}
	if err := w.Flush(); err != nil {
		return fmt.Errorf("flush error: %w", err)
	}
	return nil
}

Delete network

Deleting a network is to delete the relevant virtual network and configuration file

func DeleteNetwork(networkName string) error {
	nw, ok := networks[networkName]
	if !ok {
		return fmt.Errorf("no such network: %s", networkName)
	}

	// Release gateway IP
	_, ipNet, _ := net.ParseCIDR(nw.Subnet)
	if err := ipAllocator.Release(ipNet, &nw.GatewayIP); err != nil {
		return fmt.Errorf("remove network gateway ip err: %w", err)
	}

	// Delete virtual network
	if err := drivers[nw.Driver].Delete(*nw); err != nil {
		return fmt.Errorf("remove network driver err: %w", err)
	}

	// Delete profile
	return nw.remove(defaultNetworkPath)
}

The network configuration can be deleted by using the system function

func (b *BridgeNetworkDriver) Delete(network NetWork) error {
	bridgeName := network.Name
	br, err := netlink.LinkByName(bridgeName)
	if err != nil {
		return err
	}
	return netlink.LinkDel(br)
}

Deletion of network profile:

func (nw *NetWork) remove(dumpPath string) error {
	if _, err := os.Stat(path.Join(dumpPath, nw.Name)); err != nil {
		if os.IsNotExist(err) {
			return nil
		} else {
			return fmt.Errorf("remvove path err: %w", err)
		}
	} else {
		return os.Remove(path.Join(dumpPath, nw.Name))
	}
}

Run test

Create and view the corresponding network as shown in the following command

  • Compiler, create network
  • View network conditions
  • Delete network and confirm
root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  go build mydocker/main.go                                                                                                      ✔  ⚡  651  11:00:01
root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ./main network create --driver bridge --subnet 192.168.10.1/24 testbridge                                                      ✔  ⚡  652  11:00:06
{"level":"info","msg":"load ipam file from: /var/run/mydocker/network/ipam/subnet.json","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"allocate subnet: 192.168.10.1/24, ip: 192.168.10.1","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"dump ipam file from: /var/run/mydocker/network/ipam/","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"BridgeNetworkDriver creat network subnet: 192.168.10.1/24, gateway ip: 192.168.10.1","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"createBridgeInterface success","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"setInterfaceIp success","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"crsetInterfaceUp success","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"setInterfaceUp success","time":"2022-04-16T11:00:11+08:00"}
{"level":"info","msg":"create network success","time":"2022-04-16T11:00:11+08:00"}

root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ./main network list                                                                                                            ✔  ⚡  653  11:00:16
NAME         IpRange           Driver
testbridge   192.168.10.1/24               bridge

root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ip link show dev testbridge                                                                                                    ✔  ⚡  654  11:00:19
7: testbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether 6a:a8:a1:a4:74:b4 brd ff:ff:ff:ff:ff:ff
    
root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ip addr show dev testbridge                                                                                                    ✔  ⚡  655  11:00:24
7: testbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 6a:a8:a1:a4:74:b4 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.1/24 brd 192.168.10.255 scope global testbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::68a8:a1ff:fea4:74b4/64 scope link
       valid_lft forever preferred_lft forever
       
root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  iptables -t nat -vnL POSTROUTING                                                                                               ✔  ⚡  656  11:00:29
Chain POSTROUTING (policy ACCEPT 44 packets, 4188 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
    0     0 MASQUERADE  all  --  *      !testbridge  192.168.10.0/24      0.0.0.0/0
    0     0 MASQUERADE  all  --  *      !testbridge  192.168.10.0/24      0.0.0.0/0
    0     0 MASQUERADE  all  --  *      !testbridge  192.168.10.0/24      0.0.0.0/0
    0     0 MASQUERADE  all  --  *      !testbridge  192.168.10.0/24      0.0.0.0/0
    0     0 MASQUERADE  all  --  *      !testbridge  192.168.10.0/24      0.0.0.0/0
    
root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ./main network remove testbridge                                                                                               ✔  ⚡  657  11:00:35
{"level":"info","msg":"release subnet: 192.168.10.0/24, ip: 192.168.10.1","time":"2022-04-16T11:00:40+08:00"}
{"level":"info","msg":"load ipam file from: /var/run/mydocker/network/ipam/subnet.json","time":"2022-04-16T11:00:40+08:00"}
{"level":"info","msg":"release index: 0","time":"2022-04-16T11:00:40+08:00"}
{"level":"info","msg":"dump ipam file from: /var/run/mydocker/network/ipam/","time":"2022-04-16T11:00:40+08:00"}

root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ./main network list                                                                                                            ✔  ⚡  658  11:00:42
NAME        IpRange     Driver

root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ip addr show dev testbridge                                                                                                    ✔  ⚡  659  11:00:47
Device "testbridge" does not exist.

Overall, it feels like creating a bridge network card? And when using VMware, a virtual network card will be generated. If you use the following command to view it when it is not deleted, you will get

root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo  ifconfig                                                                                                                       ✔  ⚡  661  11:04:37
testbridge: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.1  netmask 255.255.255.0  broadcast 192.168.10.255
        inet6 fe80::fc2b:95ff:fe8d:5c98  prefixlen 64  scopeid 0x20<link>
        ether fe:2b:95:8d:5c:98  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 33  bytes 4700 (4.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Generally speaking, the code in the book is a little messy. It can't be copied down and run. It's mainly the problem of ip distribution and release. Therefore, some small modifications have been made in this article to initially meet the expectations

Tags: Go Docker

Posted by TecTao on Sat, 16 Apr 2022 12:06:13 +0930