Implementação de Rede em Contêineres: Conectando os Cabos de Rede

Visão Geral do Modelo de Rede

Os contêineres criados anteriormente não possuíam conectividade com a rede externa ou entre si. Este artigo aborda essa lacuna, introduzindo os componentes básicos para habilitar a comunicação de rede: IPAM (Gerenciamento de Endereços IP) e Drivers de Rede. O objetivo final é permitir que contêineres sejam associados a uma rede bridge, obtendo acesso interno e externo.

Abstrações: Network e Endpoint

  • Network: Representa uma rede lógica (ex.: bridge). Contém configurações como o intervalo de IP e o driver utilizado.
  • Endpoint: Conecta um contêiner a uma Network. Inclui o par VETH, endereço IP e informações de mapeamento de porta.

Interface do Driver de Rede

<span class="hljs-keyword">type</span> Driver <span class="hljs-keyword">interface</span> {
   Name() <span class="hljs-built_in">string</span>
   Create(subnet <span class="hljs-built_in">string</span>, name <span class="hljs-built_in">string</span>) (*Network, error)
   Delete(name <span class="hljs-built_in">string</span>) error
   Connect(network *Network, endpoint *Endpoint) error
   Disconnect(network Network, endpoint *Endpoint) error
}

Interface IPAM

<span class="hljs-keyword">type</span> IPAMer <span class="hljs-keyword">interface</span> {
   Allocate(subnet *net.IPNet) (ip net.IP, err error)
   Release(subnet *net.IPNet, ipaddr *net.IP) error
}

Implementação do IPAM

Utiliza uma estrutura de bitmap para controlar a alocação de IPs dentro de uma sub-rede. Cada bit representa um IP: 1 = ocupado, 0 = livre. Os dados são persistidos em um arquivo JSON.

Estrutura de Dados

<span class="hljs-keyword">const</span> defaultIPAMPath = <span class="hljs-string">"/var/lib/mydocker/network/ipam/subnet.json"</span>

<span class="hljs-keyword">type</span> IPAM <span class="hljs-keyword">struct</span> {
   StoragePath <span class="hljs-built_in">string</span>
   Subnets     *<span class="hljs-keyword">map</span>[<span class="hljs-built_in">string</span>]<span class="hljs-built_in">string</span>
}

<span class="hljs-keyword">var</span> allocator = &IPAM{
   StoragePath: defaultIPAMPath,
}

Persistência

Carregamento e salvamento usando JSON.

func (ipam *IPAM) load() error {
   file, err := os.Open(ipam.StoragePath)
   <span class="hljs-keyword">if</span> os.IsNotExist(err) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>  // primeira vez: sem dados
   }
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-keyword">defer</span> file.Close()
   data, _ := ioutil.ReadAll(file)
   <span class="hljs-keyword">return</span> json.Unmarshal(data, ipam.Subnets)
}

func (ipam *IPAM) dump() error {
   dir := filepath.Dir(ipam.StoragePath)
   os.MkdirAll(dir, <span class="hljs-number">0644</span>)
   file, err := os.OpenFile(ipam.StoragePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, <span class="hljs-number">0644</span>)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-keyword">defer</span> file.Close()
   data, _ := json.Marshal(ipam.Subnets)
   _, err = file.Write(data)
   <span class="hljs-keyword">return</span> err
}

Alocação (Allocate)

func (ipam *IPAM) Allocate(subnet *net.IPNet) (net.IP, error) {
   ipam.Subnets = &<span class="hljs-built_in">map</span>[<span class="hljs-built_in">string</span>]<span class="hljs-built_in">string</span>{}
   <span class="hljs-keyword">if</span> err := ipam.load(); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err }

   _, subnet, _ := net.ParseCIDR(subnet.String())
   one, size := subnet.Mask.Size()
   <span class="hljs-keyword">if</span> _, exists := (*ipam.Subnets)[subnet.String()]; !exists {
      (*ipam.Subnets)[subnet.String()] = strings.Repeat(<span class="hljs-string">"0"</span>, <span class="hljs-number">1</span><<uint8(size-one))
   }

   bitmap := []<span class="hljs-keyword">byte</span>((*ipam.Subnets)[subnet.String()])
   <span class="hljs-keyword">for</span> idx, ch := <span class="hljs-keyword">range</span> bitmap {
      <span class="hljs-keyword">if</span> ch == <span class="hljs-string">'0'</span> {
         bitmap[idx] = <span class="hljs-string">'1'</span>
         (*ipam.Subnets)[subnet.String()] = <span class="hljs-keyword">string</span>(bitmap)
         ip := <span class="hljs-built_in">make</span>(net.IP, <span class="hljs-number">4</span>)
         copy(ip, subnet.IP)
         <span class="hljs-keyword">for</span> t := uint(<span class="hljs-number">4</span>); t > <span class="hljs-number">0</span>; t-- {
            ip[<span class="hljs-number">4</span>-t] += <span class="hljs-keyword">byte</span>(idx >> ((t-<span class="hljs-number">1</span>) * <span class="hljs-number">8</span>))
         }
         ip[<span class="hljs-number">3</span>]++  // pula o gateway (primeiro IP)
         <span class="hljs-keyword">if</span> err := ipam.dump(); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err }
         <span class="hljs-keyword">return</span> ip, <span class="hljs-literal">nil</span>
      }
   }
   <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"no available IP in subnet %s"</span>, subnet.String())
}

Liberação (Release)

func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr net.IP) error {
   ipam.Subnets = &<span class="hljs-built_in">map</span>[<span class="hljs-built_in">string</span>]<span class="hljs-built_in">string</span>{}
   _, subnet, _ = net.ParseCIDR(subnet.String())
   <span class="hljs-keyword">if</span> err := ipam.load(); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }

   releaseIP := ipaddr.To4()
   releaseIP[<span class="hljs-number">3</span>]--  // desfaz o ajuste feito no Allocate
   offset := <span class="hljs-number">0</span>
   <span class="hljs-keyword">for</span> t := uint(<span class="hljs-number">4</span>); t > <span class="hljs-number">0</span>; t-- {
      offset += int(releaseIP[t-<span class="hljs-number">1</span>]-subnet.IP[t-<span class="hljs-number">1</span>]) << ((<span class="hljs-number">4</span> - t) * <span class="hljs-number">8</span>)
   }
   bitmap := []<span class="hljs-keyword">byte</span>((*ipam.Subnets)[subnet.String()])
   bitmap[offset] = <span class="hljs-string">'0'</span>
   (*ipam.Subnets)[subnet.String()] = <span class="hljs-keyword">string</span>(bitmap)
   <span class="hljs-keyword">return</span> ipam.dump()
}

Implementação do BridgeNetworkDriver

Gerencia uma bridge Linux usando os pacotes netlink e iptables.

Create

<span class="hljs-keyword">func</span> (d *BridgeNetworkDriver) <span class="hljs-title function_">Create</span>(subnet <span class="hljs-type">string</span>, name <span class="hljs-type">string</span>) (*Network, <span class="hljs-type">error</span>) {
   ip, ipRange, _ := net.ParseCIDR(subnet)
   ipRange.IP = ip
   n := &Network{
      Name:    name,
      IPRange: ipRange,
      Driver:  d.Name(),
   }
   <span class="hljs-keyword">return</span> n, d.initBridge(n)
}

<span class="hljs-keyword">func</span> (d *BridgeNetworkDriver) <span class="hljs-title function_">initBridge</span>(n *Network) <span class="hljs-type">error</span> {
   brName := n.Name
   <span class="hljs-comment">// 1) Criar a bridge</span>
   <span class="hljs-keyword">if</span> err := createBridge(brName); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-comment">// 2) Atribuir IP e ativar</span>
   gwIP := n.IPRange.IP
   <span class="hljs-keyword">if</span> err := setLinkIP(brName, n.IPRange.String()); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-comment">// 3) Subir a interface</span>
   <span class="hljs-keyword">if</span> err := setLinkUp(brName); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-comment">// 4) Regra SNAT</span>
   <span class="hljs-keyword">return</span> setupSNAT(brName, n.IPRange)
}

Criar Bridge

<span class="hljs-keyword">func</span> <span class="hljs-title function_">createBridge</span>(name <span class="hljs-type">string</span>) <span class="hljs-type">error</span> {
   _, err := net.InterfaceByName(name)
   <span class="hljs-keyword">if</span> err == <span class="hljs-literal">nil</span> || !strings.Contains(err.Error(), <span class="hljs-string">"no such network interface"</span>) {
       <span class="hljs-keyword">return</span> err
   }
   la := netlink.NewLinkAttrs()
   la.Name = name
   br := &netlink.Bridge{LinkAttrs: la}
   <span class="hljs-keyword">return</span> netlink.LinkAdd(br)
}

Atribuir IP e Ativar

<span class="hljs-keyword">func</span> <span class="hljs-title function_">setLinkIP</span>(name, rawIP <span class="hljs-type">string</span>) <span class="hljs-type">error</span> {
   link, err := netlink.LinkByName(name)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   ipNet, err := netlink.ParseIPNet(rawIP)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   addr := &netlink.Addr{IPNet: ipNet}
   <span class="hljs-keyword">return</span> netlink.AddrAdd(link, addr)
}

<span class="hljs-keyword">func</span> <span class="hljs-title function_">setLinkUp</span>(name <span class="hljs-type">string</span>) <span class="hljs-type">error</span> {
   link, err := netlink.LinkByName(name)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-keyword">return</span> netlink.LinkSetUp(link)
}

Regra SNAT

<span class="hljs-keyword">func</span> <span class="hljs-title function_">setupSNAT</span>(bridgeName <span class="hljs-type">string</span>, subnet *net.IPNet) <span class="hljs-type">error</span> {
   cmd := fmt.Sprintf(<span class="hljs-string">"iptables -t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE"</span>,
       subnet.String(), bridgeName)
   parts := strings.Split(cmd, <span class="hljs-string">" "</span>)
   <span class="hljs-keyword">return</span> exec.Command(parts[0], parts[<span class="hljs-number">1</span>:]...).Run()
}

Delete

<span class="hljs-keyword">func</span> (d *BridgeNetworkDriver) <span class="hljs-title function_">Delete</span>(network Network) <span class="hljs-type">error</span> {
   link, err := netlink.LinkByName(network.Name)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-keyword">return</span> netlink.LinkDel(link)
}

Connect

<span class="hljs-keyword">func</span> (d *BridgeNetworkDriver) <span class="hljs-title function_">Connect</span>(network *Network, ep *Endpoint) <span class="hljs-type">error</span> {
   br, err := netlink.LinkByName(network.Name)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }

   la := netlink.NewLinkAttrs()
   la.Name = ep.ID[:<span class="hljs-number">5</span>]
   la.MasterIndex = br.Attrs().Index

   ep.Device = netlink.Veth{
       LinkAttrs: la,
       PeerName:  <span class="hljs-string">"cif-"</span> + ep.ID[:<span class="hljs-number">5</span>],
   }
   <span class="hljs-keyword">if</span> err := netlink.LinkAdd(&ep.Device); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> err }
   <span class="hljs-keyword">return</span> netlink.LinkSetUp(&ep.Device)
}

Disconnect

<span class="hljs-keyword">func</span> (d *BridgeNetworkDriver) <span class="hljs-title function_">Disconnect</span>(network Network, ep *Endpoint) <span class="hljs-type">error</span> {
   <span class="hljs-comment">// Não implementado neste exemplo</span>
   <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

Testes Unitários

Teste IPAM

<span class="hljs-keyword">func</span> <span class="hljs-title function_">TestIPAMAllocate</span>(t *testing.T) {
   _, ipNet, _ := net.ParseCIDR(<span class="hljs-string">"192.168.0.0/24"</span>)
   ip, err := allocator.Allocate(ipNet)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { t.Fatal(err) }
   t.Logf(<span class="hljs-string">"Allocated IP: %s"</span>, ip.String())
}

$ go <span class="hljs-keyword">test</span> -v -run TestIPAMAllocate
=== RUN   TestIPAMAllocate
    ipam_test.go:<span class="hljs-number">14</span>: Allocated IP: <span class="hljs-number">192.168</span>.<span class="hljs-number">0.1</span>
--- PASS: TestIPAMAllocate (<span class="hljs-number">0.00</span>s)
PASS
ok      mydocker/network  <span class="hljs-number">0.006</span>s

Teste Bridge

<span class="hljs-keyword">func</span> <span class="hljs-title function_">TestBridgeCreate</span>(t *testing.T) {
   d := BridgeNetworkDriver{}
   n, err := d.Create(<span class="hljs-string">"172.16.0.1/16"</span>, <span class="hljs-string">"testbr"</span>)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { t.Fatal(err) }
   t.Logf(<span class="hljs-string">"Created network: %+v"</span>, n)
   <span class="hljs-comment">// Verificar bridge criada</span>
   link, _ := netlink.LinkByName(<span class="hljs-string">"testbr"</span>)
   t.Logf(<span class="hljs-string">"Bridge link: %+v"</span>, link.Attrs())
}

$ go <span class="hljs-keyword">test</span> -v -run TestBridgeCreate
=== RUN   TestBridgeCreate
    bridge_test.go:<span class="hljs-number">25</span>: Created network: &{Name:testbr IPRange:<span class="hljs-number">172.16</span>.<span class="hljs-number">0.1</span>/<span class="hljs-number">16</span> Driver:bridge}
    bridge_test.go:<span class="hljs-number">27</span>: Bridge link: &{Name:testbr ...}
--- PASS: TestBridgeCreate (<span class="hljs-number">0.50</span>s)
PASS

$ ip link show
...
3: testbr: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu <span class="hljs-number">1500</span> qdisc noqueue state UNKNOWN
    link/ether ...

Próximos Passos

Com o IPAM e o BridgeNetworkDriver funcionando, o próximo artigo integrará esses componentes aos comandos mydocker network create/delete e permitirá que contêineres se conetcem a uma rede através do parâmetro mydocker run --net.

Tags: Docker Linux bridge Veth iptables

Publicado em 7-18 22:41