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.