Sieťová komunikácia 1

Bez sieťovej komunikácie to ma dnes akákoľvek technológia dosť ťažké. Aplikácie dnes potrebujú komunikovať. Sieťovanie v kontajnerovom vesmíre je alchýmia sama o sebe.

Priznám sa, že aj ja som nad týmto článkom strávil omnoho viac času. Nebolo jednoduché si stanoviť, kam až zájsť. Nakoniec som sa rozhodol túto tému rozdeliť na 2 články. V tomto prvom článku venovanému sieťovej komunikácii si ukážeme ako kontajner v Dockeri komunikuje s okolitým svetom. Hneď na úvod musim povedať, že Docker ponúka niekoľko typov sieťovej komunikácie a toto je len jeden z nich.

Mne osobne sieťová komunikácia kontajnerov pripomína malú domácu sieť. Základným prvkom v dnešnej domácej sieti je WiFi router. Odmyslíme si teraz slovíčko WiFi. Router disponuje jedným WAN portom a viacero LAN portami. WAN port pripájame k internetu a má IP adresu priradenú poskytovateľom internetu. LAN porty pripájame k rôznym zariadeniam. IP adresa pre zariadene je väčšinou z nejakého rozsahu vnútornej siete napríklad 192.168.0.1/16.

router

Veľmi podobne to funguje aj v Dockeri, akurát všetko je to virtuálne. Namiesto zariadenia ako TV alebo počítač tu máme kontajner. Ten disponuje sieťovým rozhraním vo vlastnom mennom priestore. Každý kontajner je pripojený do akéhosi virtuálneho switchu, do akejsi virtuálnej sub-siete v hosťovskom systéme. Tým je sieťový most - bridge a jeho rozsah adries je 172.18.0.1/16. Sieťový most nasmeruje všetku komunikáciu mimo svojej sub-siete na fyzické rozhranie hosťovského systému. Najčastejšie je to eth0 a v mojom prípade ma IP adresu 192.168.0.15.

bridge

Sieťový menný priestor

Skôr ako začnem vytvárať virtuálnu sieť, budem potrebovať upraviť moje behové prostredie anton. Má totiž jeden problém. Momentálne má kontajner prístup ku všetkým sieťovým rozhraniam systému.

$ ./anton run /bin/sh
anton> ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: bond0: <BROADCAST,MULTICAST400> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 86:c2:ca:15:9b:f5 brd ff:ff:ff:ff:ff:ff
3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 46:50:66:73:ab:17 brd ff:ff:ff:ff:ff:ff
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:15:5d:98:bd:a8 brd ff:ff:ff:ff:ff:ff
5: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0

Hups. Toto nie je práve najlepšie. Tých dôvodov je niekoľko. Jeden praktický je, že viacero kontajnerov tej istej aplikácie nemôžu používať jeden port. Napríklad nemôžem mať dva bežiace PostgreSQL, pretože obe používajú port 5432. Každý PostgreSQL by tak musel používať vlastný port.

Na scénu prichádzajú menné priestory. O izoláciu sieťových rozhraní sa postará CLONE_NEWNET. Do kódu z minulého článku doplním tento nový druh menného priestoru.

   // nastavim menne priestory pre re-exec
   cmd.SysProcAttr = &syscall.SysProcAttr{
      Cloneflags: syscall.CLONE_NEWUSER |
         syscall.CLONE_NEWPID |
         syscall.CLONE_NEWNS |
         syscall.CLONE_NEWNET,

Kompletný kód aj s touto zmenou je dostupný na GitHube. Keď spustím Antona s touto úpravou, tak po vypísaní dostupných sieťových rozhraní nastáva viac-menej očakávaná zmena.

$ ./anton run /bin/sh
anton> ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Kontajner má teraz prístup iba k localhost rozhraniu. Je odstrihnutý od akejkoľvek sieťovej komunikácie.

Vytvorenie mosta

Na strane hosťovského systému musím pripraviť virtuálnu sub-sieť, do ktorej zapojím kontajner. Ako som už spomenul vyžšie, k tomu poslúži sieťový most. Pod root účtom v hosťovskom systéme vytvorím anton0.

root$ ip link add name anton0 type bridge
root$ ip addr add dev anton0 172.18.0.1/16
root$ ip link set anton0 up

Tento sieťový most je vytvorený pre rozsah IP adries 172.18.0.0/16 a IP adresa 172.18.0.1 bude slúžiť pre bránu - gateway. Ak to prirovnáme k domácej sieti, tak je to vnútorná adresa routera. Sem bude smerovať všetka komunikácia od kontajnera smerom von na internet.

Sieťové rozhranie v kontajneri

Na to, aby som vedel vytvoriť rozhranie priamo v izolovanom kontajneri, budem potrebovať PID nejakého procesu tohto kontajnera. Podľa toho určí jadro Linuxu v akom mennom priestore vytvoriť sieťové rozhranie. PID získam pomocou lsns.

root$ lsns -t net
        NS TYPE NPROCS   PID USER    NETNSID NSFS COMMAND
4026532205 net       2  1635 zdenko        0      /proc/self/exe reexec /bin/sh

Keďže Anton pri vytvorení procesu použije CLONE_NEWNET, tak v zozname bude viditeľný reexec aj z jeho PID. V mojom prípade to je 1635`. Môžem teda vytvoriť virtuálne rozhranie.

root$ ip link add lan0 type veth peer name veth0 netns 1635

Príkaz je trochu krkolomný, ale dá sa rozdeliť na 2 časti. Prvá hosťovská časť lan0 type veth definuje rozhranie na strane hosťa. Sieťari mi odpustia, že používam meno lan0. Je to len kvôli prirovnaniu k routeru. Potom nasleduje virtuálny kábel peer a druhá časť name veth0. Posledná časť netns 1635 zabezpečí, že veth0 bude súčasťou menného priestoru vo vnútri kontajnera. Výsledkom bude. že v bežiacom kontajneri sa zobrazí veth0.

anton> ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: veth0@if6: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether f2:6c:8e:84:3d:73 brd ff:ff:ff:ff:ff:ff

Všimol som si, že rozhranie kontajnera ešte nemá priradenú žiadnu adresu. Priradím mu adresu 172.18.0.2 a zapnem ho.

anton> ip addr add 172.18.0.2/16 dev veth0
anton> ip link set veth0 up

To ešte nie je všetko. Na strane hosťovského systému mi vzniklo rozhranie lan0. Toto rozhranie musím priradiť mostu anton0 a zapnúť.

root$ ip link set lan0 master anton0
root$ ip link set lan0 up

Teraz už kontajner vie komunikovať s hosťovským systémom.

anton> ping 172.18.0.1
PING 172.18.0.1 (172.18.0.1): 56 data bytes
64 bytes from 172.18.0.1: seq=0 ttl=64 time=0.068 ms
64 bytes from 172.18.0.1: seq=1 ttl=64 time=0.112 ms

Skúsim si teraz opingovať fyzickú IP adresu môjho hosťovského systému 192.168.0.15.

anton> ping 192.168.0.15
PING 192.168.0.15 (192.168.0.15): 56 data bytes
ping: sendto: Network unreachable

Dostávam nemilú odpoveď. Linuxáci už asi tušia problém. Pozrel som sa na smerovaciu tabuľku kontajnera. Smerovacia tabuľka nemá určenú bránu - default gw. Komunikácia z kontajnera je teraz smerovaná na bránu 172.18.0.1 a dotiaľ ďalej na moje fyzické rozhranie eth0.

anton> ip route add default via 172.18.0.1
anton> ping 192.168.0.15
64 bytes from 192.168.0.15: seq=0 ttl=64 time=0.189 ms
64 bytes from 192.168.0.15: seq=1 ttl=64 time=0.209 ms
64 bytes from 192.168.0.15: seq=2 ttl=64 time=0.208 ms

Tu však celá komunikácia končí. Zatiaľ sa neviem dostať mimo môj hosťovský systém a teda na internet.

Konfigurácia prekladu adries - NAT

Na hosťovskom systéme mi chyba pár pravidiel pre smerovacie tabuľky. Na hosťovskom systéme potrebujem nastaviť IP forwarding a NAT.

comm1

NAT(Network Address Translation) sa stará o preklad sieťových adries. Táto funkcia bude prekladať adresy z vnútornej siete kde beží kontajner, na verejnú adresu počítača a naopak. Vďaka tomu bude vnútorná sieť “schovaná” za fyzické rozhranie eth0. Presne ako domáci router zabezpečuje, že máme internet dostupný na každom zariadení na domácej sieti. Aby NAT fungoval, je potrebné mať zapnutú funkcionalitu IP forwarding a Masquerade.

root$ sysctl -w net.ipv4.ip_forward=1
root$ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

Potom nasleduje sekvencia pravidiel, ktoré prepoja eth0 a anton0.

root$ iptables -A FORWARD -i eth0 -o anton0 -m state --state RELATED,ESTABLISHED -j ACCEPT
root$ iptables -A FORWARD -i anton0 -o eth0 -j ACCEPT

Nebudem sa veľmi rozpisovať ako funguje iptables. Táto sekvencia príkazov zabezpečí, že zdrojová adresa každého paketu vychádzajúceho z kontajnera smerom von, bude preložená na adresu fyzického rozhrania eth0. Ak je všetko správne nastavené, z kontajnera budem vedieť dosiahnuť na môj domáci router s IP 192.168.0.1.

anton> ping 192.168.0.1
PING 192.168.0.1 (192.168.0.1): 56 data bytes
64 bytes from 192.168.0.1: seq=0 ttl=62 time=2.705 ms
64 bytes from 192.168.0.1: seq=1 ttl=62 time=1.747 ms

Poslednou kozmetickou zmenou je nastavenie DNS v kontajneri.

anton> echo "nameserver 8.8.8.8" > /etc/resolv.conf

Od tohto momentu kontajner vie plnohodnotne komunikovať.

comm2

A čo Docker?

Docker to rieši veľmi podobne. Teda aby som bol presnejší, Docker poskytuje 4 typy sieťovej komunikácie. To čo som predviedol v tomto článku je bridge typ a ide o najpoužívanejší typ. Kedže väčšinou do sieťovej komunikácie nešprtáme a bridge je predvolený typ, tak je veľká šanca, že presne tak to funguje aj vo vašom počítači.

Ak používate Linux a pozriete sa na zoznam sieťových rozhraní, uvidíme niečo veľmi podobné ako anton0. Pre tých, čo používajú Mac OS a chcú sa pošprtať v sieťovaní, odporúčam použiť:

docker run -it --rm --privileged --pid=host justincormack/nsenter1

Pre Windows používateľov mam zlé správy. Hoci Docker používa WSL2 integráciu a môžeme sa dostať k hosťovskému systému, tak sieť je tu trošku mágia v kompetencii Windows.

Ale pozrime sa teda na rozhrania:

/ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:49:44:e3:3e brd ff:ff:ff:ff:ff:ff
4: veth7fbb3c4@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether 1e:d8:2b:b2:1e:46 brd ff:ff:ff:ff:ff:ff link-netnsid 0
5: veth61878a6@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether e2:bd:13:96:7f:88 brd ff:ff:ff:ff:ff:ff link-netnsid 1    

Z výstupu si všimnite docker0 a dvojicu veth7fbb3c4 a veth61878a6. Ide o most podobný tomu môjmu anton0 a dvojica rozhraní k dvom kontajnerom v mojom systéme. Most docker0 je pre rozsah IP adries 172.17.0.0/16 a bránou je 172.17.0.1.

/ # ip addr show docker0 | grep "inet "
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

Docker dosť intenzívne manipuluje aj so smerovacími tabuľkami. Pozrime sa na smerovacie tabuľky cez iptables. Docker v systéme vytvára niekoľko reťazi ako DOCKER, DOCKER-USER, DOCKER-ISOLATION-STAGE. Docker, rovnako ako ja v príklade nastavuje NAT pre docker0.

/ # iptables -S -t nat
...
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
...

Manipuláciu so smerovacími tabuľkami môžeme zbadať aj pri inom, dosť častom prípade. Čo sa stane ak spustím kontajner a chcem vystaviť jeho port ako verejný? Zoberme si ngxin:

docker run -p 80:80 nginx:latest

Docker v tomto prípade vytvorí DNAT - akýsi opačný NAT. Ten nasmeruje prichádzajúcu komunikáciu na IP konkrétneho kontajnera. V mojom prípade je to 172.17.0.3.

/ # iptables  -S -t nat
...
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT
...

Úprimne poviem že je mi stále záhadou prečo Docker s iptables manipuluje tak ako manipuluje. Niektoré pravidla sú ťažký oriešok. Veľmi rýchlo sa človek dokáže stratiť vo všetkých tých pravidlách. Tu však nastal presne ten moment, kedy som si musel povedať kam až chcem zájsť.

Záver

Ako som spomenul už v úvode, sieťová komunikácia nie je triviálna téma. Už len sa vysomáriť z iptables môže spôsobovať slušné zatočenie hlavy. Človeku to najprv príde zbytočné vedieť, no práve tieto znalosti môžu pomôcť s riešením zložitých problémov z kategórie WTF. Obzvlášť vo svete Kubernetes, kde je sieťová komunikácia o niečo komplexnejšia.

Nabudúce sa pozrieme na to, ako sú tieto manipulácie so sieťou naprogramované priamo v Dockeri. Pre mňa osobne je toto zábavnejšia časť - ako sa programuje sieť. Popíšeme si, ktorá časť Docker architektúry rieši čo a aké abstrakcie existujú.

<