© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_5

5. Containers with Networking

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 

In Chapter 4, you learned about the different features of the Linux kernel used for containers. You also explored namespaces and how they help applications isolate from other processes. In this chapter, you will focus solely on the network namespace and understand how it works and how to configure it.

The network namespace allows applications that run on their own namespaces to have a network interface that allows running processes to send and receive data to the host or to the Internet. In this chapter, you will learn how to do the following:
  • Create your own network namespace

  • Communicate with the host

  • Use network space in Go

Source Code

The source code for this chapter is available from the https://github.com/Apress/Software-Development-Go repository.

Network Namespace

In Chapter 4, you looked at namespaces, which are used to create a virtual isolation for an application, which is one of the key ingredients in running applications inside a container. The network namespace is another isolation feature that applications need because it allows them to communicate with the host or the Internet.

Why is the network namespace important?

Looking at Figure 5-1, you can see that there are two different applications running on a single host in different namespaces and each of the namespaces has their own network namespace.

A framework of the Linux host includes 2 namespaces named red and blue. Both the host includes an app and network.

Figure 5-1

Network namespaces

The applications are allowed to talk to each other, but they are not allowed to talk to the host and vice versa. This not only makes the applications more secure, but also it makes the application easier to maintain because it does not need to worry about services outside the host.

Using a network namespace requires a few things to be configured properly in order for the application to use it. Figure 5-2 shows the different things that are needed.

A framework of the Linux host shows a namespace labeled red and includes network l0 and network peer 0 reversibly linked to veth 0 and further to br 0.

Figure 5-2

Virtual networks

Let’s take a look at them individually:

Network (lo)

In your computer, you normally access servers that are running locally using localhost. This inside the network namespace is also configured the same; it is known as lo.

Network (peer0)

This is known as a peer name, and it is configured for the namespace that will communicate with traffic outside the namespace. As shown in Figure 5-2, it communicates with veth0.

veth0

This is called a virtual ethernet and it is configured in the host computer. The virtual ethernet, or in this case veth0, communicates between the host and the namespace.

br0

This is a virtual switch. It’s also known as a bridge. Any network attached to the bridge can communicate with the others. In this case, there is only one virtual ethernet (veth0) but if there was another virtual ethernet, they could communicate with each other.

Now that you have a good understanding of the different things that need to be configured in a network namespace, in the next section you will explore using a Linux tool to play around with network namespaces.

Setting Up with the ip Tool

In this section, you will look at setting up two different network namespaces and each will be assigned its own IP address. The script will use the standard Linux tool called ip. If you don’t have the tool installed on your machine, use the following command to install it:
sudo apt-get install -y iproute OR
sudo apt-get install -y iproute2
The script will set up the network namespaces to allow them to access each other but they cannot communicate with any external services. The script can be found inside the chapter5/ns directory. Change to this directory and execute it as follows (make sure you run it as root):
sudo ./script.sh
You will get output that looks like the following:
...
64: virt0: <NO-CARRIER,BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000
...
66: virt1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 1000
...
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.069 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.052 ms
...
--- 10.0.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.044/0.053/0.069/0.009 ms
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.060 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.044 ms
...
--- 10.0.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.044/0.053/0.060/0.006 ms
PING 10.0.0.10 (10.0.0.10) 56(84) bytes of data.
64 bytes from 10.0.0.10: icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from 10.0.0.10: icmp_seq=2 ttl=64 time=0.035 ms
...
--- 10.0.0.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.031/0.037/0.047/0.005 ms
PING 10.0.0.10 (10.0.0.10) 56(84) bytes of data.
64 bytes from 10.0.0.10: icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from 10.0.0.10: icmp_seq=2 ttl=64 time=0.070 ms
...
--- 10.0.0.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.043/0.058/0.070/0.013 ms
PING 10.0.0.11 (10.0.0.11) 56(84) bytes of data.
64 bytes from 10.0.0.11: icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from 10.0.0.11: icmp_seq=2 ttl=64 time=0.042 ms
...
--- 10.0.0.11 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.042/0.057/0.070/0.010 ms
PING 10.0.0.11 (10.0.0.11) 56(84) bytes of data.
64 bytes from 10.0.0.11: icmp_seq=1 ttl=64 time=0.032 ms
...

The script creates two different namespaces called ns1 and ns2, assigning virtual networks to both of them, as explained in the previous section. The virtual networks are assigned IP addresses 10.0.0.10 and 10.0.0.11, and both networks are connected to each other via a bridge that is assigned IP address 10.0.0.1.

Let’s go through the script to understand what it is doing. The following snippet creates two network namespaces labeled ns1 and ns2:
ip netns add ns1
ip netns add ns2
Once the namespace has been set up, it will set up a local network interface inside the namespace.
ip netns exec ns1 ip link set lo up
ip netns exec ns1 ip link
ip netns exec ns2 ip link set lo up
ip netns exec ns2 ip link
Now, you need to create a network bridge and assign 10.0.0.1 as its IP address.
ip link add br0 type bridge
ip link set br0 up
# setup bridge IP
ip addr add 10.0.0.1/8 dev br0
Once the bridge has been set up, the script will link the virtual networks to the network namespaces and also link them to the bridge. This will link all the different virtual networks together through the bridge. The script will assign the different IP address to the virtual networks.
# setup virtual ethernet and link it to namespace
ip link add v0 type veth peer name virt0
ip link set v0 master br0
ip link set v0 up
ip link set virt0 netns ns1
# bring up the virtual ethernet
ip netns exec ns1 ip link set virt0 up
# print out info about the network link
ip netns exec ns1 ip link
# setup virtual ethernet and link it to namespace
ip link add v1 type veth peer name virt1
ip link set v1 master br0
ip link set v1 up
ip link set virt1 netns ns2
# bring up the virtual ethernet
ip netns exec ns2 ip link set virt1 up
# print out info about the network link
ip netns exec ns2 ip link
# Set IP address to the different virtual interfaces
ip netns exec ns1 ip addr add 10.0.0.10/8 dev virt0
ip netns exec ns2 ip addr add 10.0.0.11/8 dev virt1
The last step that the script will do is route traffic between the bridge. This will allow traffic to flow through the ns1 and ns2 namespaces.
# register the bridge in iptables to allow forwarding
iptables -I FORWARD -i br0 -o br0 -j ACCEPT
Once the script has run successfully, you will see the routing information using the following command:
iptables  -v --list FORWARD  --line-number
You will see the output shown below. The output shows that bridge br0 has been registered into the routing table to allow traffic through.
Chain FORWARD (policy DROP 53 packets, 4452 bytes)
num      pkts bytes target                  prot opt in     out        source               destination
1        4   336 ACCEPT                     all  --  br0    br0        anywhere             anywhere
2        53  4452 DOCKER-USER               all  --  any    any        anywhere             anywhere
3        53  4452 DOCKER-ISOLATION-STAGE-1  all  --  any    any        anywhere             anywhere
4        0     0 ACCEPT                     all  --  any     docker0  anywhere   anywhere             ctstate RELATED, ESTABLISHED
5        0     0 DOCKER                     all  --  any    docker0    anywhere             anywhere
6        0     0 ACCEPT                     all  --  docker0 !docker0   anywhere             anywhere
7        0     0 ACCEPT                     all  --  docker0 docker0    anywhere             anywhere
After executing the script, you can remove the br0 routing information by using the following command. Replace the value 1 with the chain number you obtained when running the above command to print out the routing information.
iptables  -v --delete FORWARD  1

You just learned how to set up two network namespaces and allow traffic flow between the two of them using a Linux tool. In the next section, you will see how to set up network namespaces in a Go program, similar to what tools like Docker do.

Containers with Networks

In this section, you will look at a small project that provides Docker-like functionality. The project will be similar to the tool we discussed in Chapter 4, but this tool creates network namespaces to allow the container to have network capability. The project can be checked out from https://github.com/nanikjava/container-networking.

Check out the project and compile it as follows:
go build -o cnetwork
Once compiled, execute the following command to run it as an Alpine container:
sudo ./cnetwork run alpine /bin/sh
You will see output that looks like the following:
2022/06/05 12:59:11 Cmd args: [./cnetwork run alpine /bin/sh]
2022/06/05 12:59:11 New container ID: 20747aa00a4d
2022/06/05 12:59:11 Downloading metadata for alpine:latest, please wait...
2022/06/05 12:59:13 imageHash: e66264b98777
2022/06/05 12:59:13 Checking if image exists under another name...
2022/06/05 12:59:13 Image doesn't exist. Downloading...
2022/06/05 12:59:16 Successfully downloaded alpine
2022/06/05 12:59:16 Uncompressing layer to: /var/lib/gocker/images/e66264b98777/4a973e6cf97f/fs
2022/06/05 12:59:16 Image to overlay mount: e66264b98777
2022/06/05 12:59:16 Cmd args: [/proc/self/exe setup-netns 20747aa00a4d]
2022/06/05 12:59:16 Cmd args: [/proc/self/exe setup-veth 20747aa00a4d]
2022/06/05 12:59:16 Cmd args: [/proc/self/exe child-mode --img=e66264b98777 20747aa00a4d /bin/sh]
/ #
You will see a prompt (/#) to enter a command inside the container. Try using the ifconfig command that will print out the configured network interface.
/ # ifconfig
On my local machine, the output looks like the following:
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
veth1_7ea0e6 Link encap:Ethernet  HWaddr 02:42:4C:66:FD:FE
          inet addr:172.29.69.160  Bcast:172.29.255.255  Mask:255.255.0.0
          inet6 addr: fe11::11:4c11:fe11:fdfe/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:19 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:2872 (2.8 KiB)  TX bytes:516 (516.0 B)
As you can see, the virtual ethernet network has been configured with IP address 172.29.69.160. The bridge configured on the host looks like the following when you run ifconfig on the host:
...
gocker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.29.0.1  netmask 255.255.0.0  broadcast 172.29.255.255
        inet6 fe80::5851:6bff:fe0e:1768  prefixlen 64  scopeid 0x20<link>
        ether ce:cc:2c:e2:9e:97  txqueuelen 1000  (Ethernet)
        RX packets 61  bytes 4156 (4.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 110  bytes 15864 (15.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
...
veth0_7ea0e6: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::e8a3:faff:fed2:2ee9  prefixlen 64  scopeid 0x20<link>
        ether ea:a3:fa:d2:2e:e9  txqueuelen 1000  (Ethernet)
        RX packets 11  bytes 866 (866.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 46  bytes 7050 (7.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
...

The gocker0 bridge is configured with IP 172.29.0.1 and you can ping it from the container.

Let’s test the network communication between the container and the host. Open terminal and run the following command:
sudo ./cnetwork run alpine /bin/sh
Once the container is up and running, get the IP address of the container by using the following command:
ip addr show
Once you get the IP address of your container, run the following command in the container:
nc -l -p 4000
The container is now ready to accept a connection on port 4000. Open another terminal window from your host machine and run the following command:
nc <container_ip_address> 4000
Type in anything on your terminal window and the container will output your type. You will see something like Figure 5-3.

A screenshot has a program including loopback and I P address. The output of the program is nanik, followed by an I P address, and a text, this is a test message.

Figure 5-3

Communication between container and host

Let’s take a look at the code to understand how the application is able to do all this inside Go. The application performs a two-step execution process. The first step is setting up the bridge and virtual networks, and the second step is setting up the network namespaces, setting up the different configurations of the virtual networks, and executing the container inside the namespace.

Let’s take a look at the first step of creating the bridge and virtual networks, as shown here:
func setupGockerBridge() error {
  linkAttrs := netlink.NewLinkAttrs()
  linkAttrs.Name = "gocker0"
  gockerBridge := &netlink.Bridge{LinkAttrs: linkAttrs}
  if err := netlink.LinkAdd(gockerBridge); err != nil {
     return err
  }
  addr, _ := netlink.ParseAddr("172.29.0.1/16")
  netlink.AddrAdd(gockerBridge, addr)
  netlink.LinkSetUp(gockerBridge)
  return nil
}

The function sets up a new bridge by creating a new netlink.Bridge, which contain network bridge information that is populated with the name gocker0 and is assign the IP address 172.29.0.1.

Once it successfully sets up the bridge, it will set up the virtual ethernet that is called inside the initContainer(..) function, as shown here:
func initContainer(mem int, swap int, pids int, cpus float64, src string, args []string) {
  ...
  if err := setupVirtualEthOnHost(containerID); err != nil {
     log.Fatalf("Unable to setup Veth0 on host: %v", err)
  }
  ...
}
The setupVirtualEthOnHost(..) function is shown here:
unc setupVirtualEthOnHost(containerID string) error {
  veth0 := "veth0_" + containerID[:6]
  veth1 := "veth1_" + containerID[:6]
  linkAttrs := netlink.NewLinkAttrs()
  linkAttrs.Name = veth0
  veth0Struct := &netlink.Veth{
     LinkAttrs:        linkAttrs,
     PeerName:         veth1,
     PeerHardwareAddr: createMACAddress(),
  }
  if err := netlink.LinkAdd(veth0Struct); err != nil {
     return err
  }
  netlink.LinkSetUp(veth0Struct)
  gockerBridge, _ := netlink.LinkByName("gocker0")
  netlink.LinkSetMaster(veth0Struct, gockerBridge)
  return nil
}

The function creates two virtual networks labeled veth0_xxx and veth1_xxx. The xxx represents the generated container id, so in my case it looks like veth0_7ea0e6. The new virtual network will be given a generated MAC address by calling createMACAddress() and will be linked to the newly created gocker0 bridge.

Now that the bridge and virtual networks have been set up, the application will set up the network namespace, configure the container’s virtual network, and run the container, which is performed by initContainer(..).
func initContainer(mem int, swap int, pids int, cpus float64, src string, args []string) {
  ...
  prepareAndExecuteContainer(mem, swap, pids, cpus, containerID, imageShaHex, args)
  ...
}
The prepareAndExecuteContainer(..) function takes care of few things, as shown in the following snippet:
func prepareAndExecuteContainer(mem int, swap int, pids int, cpus float64,
  containerID string, imageShaHex string, cmdArgs []string) {
  cmd := &exec.Cmd{
     Path:   "/proc/self/exe",
     Args:   []string{"/proc/self/exe", "setup-netns", containerID},
     ...
  }
  cmd.Run()
  cmd = &exec.Cmd{
     Path:   "/proc/self/exe",
     Args:   []string{"/proc/self/exe", "setup-veth", containerID},
     ...
  }
  cmd.Run()
  ...
  opts = append(opts, "--img="+imageShaHex)
  args := append([]string{containerID}, cmdArgs...)
  args = append(opts, args...)
  args = append([]string{"child-mode"}, args...)
  cmd = exec.Command("/proc/self/exe", args...)
  ...
  cmd.SysProcAttr = &unix.SysProcAttr{
     Cloneflags: unix.CLONE_NEWPID |
        unix.CLONE_NEWNS |
        unix.CLONE_NEWUTS |
        unix.CLONE_NEWIPC,
  }
  doOrDie(cmd.Run())
}
The function runs itself again (via the /proc/self/exe way), passing the parameters setup-ns and setup-veth. These two functions perform the network namespace (setupNetNetworkNamespace) and virtual ethernet setup, (setupContainerNetworkInterfaceStep1 and setupContainerNetworkInterfaceStep2).
func setupNewNetworkNamespace(containerID string) {
  _ = createDirsIfDontExist([]string{getGockerNetNsPath()})
  ...
  if err := unix.Setns(fd, unix.CLONE_NEWNET); err != nil {
     log.Fatalf("Setns system call failed: %v ", err)
  }
}
func setupContainerNetworkInterfaceStep1(containerID string) {
  ...
  veth1 := "veth1_" + containerID[:6]
  veth1Link, err := netlink.LinkByName(veth1)
  ...
  if err := netlink.LinkSetNsFd(veth1Link, fd); err != nil {
     log.Fatalf("Unable to set network namespace for veth1: %v ", err)
  }
}
func setupContainerNetworkInterfaceStep2(containerID string) {
  ...
  if err := unix.Setns(fd, unix.CLONE_NEWNET); err != nil {
     log.Fatalf("Setns system call failed: %v ", err)
  }
  veth1 := "veth1_" + containerID[:6]
  veth1Link, err := netlink.LinkByName(veth1)
  if err != nil {
     log.Fatalf("Unable to fetch veth1: %v ", err)
  }
  ...
  route := netlink.Route{
     Scope:     netlink.SCOPE_UNIVERSE,
     LinkIndex: veth1Link.Attrs().Index,
     Gw:        net.ParseIP("172.29.0.1"),
     Dst:       nil,
  }
  ...
}
Once all the network setup is done, it calls itself again, passing in child-mode as the parameter, which is performed by the following code snippet:
  ...
case "child-mode":
  fs := flag.FlagSet{}
  fs.ParseErrorsWhitelist.UnknownFlags = true
  ...
  execContainerCommand(*mem, *swap, *pids, *cpus, fs.Args()[0], *image, fs.Args()[1:])
  ...

Once all setup is done, the final step is to set up the container by calling execContainerCommand(..) to allow the user to execute the command inside the container.

In this section, you learned the different steps involved in setting up virtual networks for a container. The sample application used in this section performs operations such as downloading images, setting up rootfs, setting up network namespaces, and configuring all the different virtual networks required for a container.

Summary

In this chapter, you learned about virtual networks that are used inside containers. You went through the steps of configuring network namespaces along with virtual networks manually using a Linux tool called ip. You looked at configuring iptables to allow communication to happen between the different network namespaces.

After understanding how to configure a network namespace with virtual networks, you looked at a Go example of how to configure virtual networks in a container. You went through the different functions that perform different tasks that are required to configure the virtual networks for a container.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.47.230