[{"data":1,"prerenderedAt":547},["ShallowReactive",2],{"mdc--2jws3i-key":3,"mdc-qufpjp-key":36,"mdc-2fs8xr-key":52,"mdc--2xgvw4-key":110,"mdc--cd8suq-key":155,"mdc-n2sbdp-key":272,"mdc-qf13a0-key":292,"mdc-9n3utr-key":304,"mdc-436b5v-key":392,"mdc-hzvj9u-key":530},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,25],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,23],{"type":14,"value":15},"text","The container has an address, a route, and no way to reach anything beyond the host. Its ",{"type":9,"tag":17,"props":18,"children":20},"code",{"className":19},[],[21],{"type":14,"value":22},"10.0.0.2",{"type":14,"value":24}," is a private address from RFC 1918 — the packet gets out, and the reply has nowhere to come back to.",{"type":9,"tag":10,"props":26,"children":27},{},[28,34],{"type":9,"tag":29,"props":30,"children":31},"strong",{},[32],{"type":14,"value":33},"Network Address Translation",{"type":14,"value":35}," fixes that by having the host rewrite the packet's source address to its own on the way out, remember the substitution, and undo it on the way back. Three iptables rules.",{"data":37,"body":38},{},{"type":6,"children":39},[40,47],{"type":9,"tag":41,"props":42,"children":44},"h2",{"id":43},"find-your-real-interface-first",[45],{"type":14,"value":46},"Find your real interface first",{"type":9,"tag":10,"props":48,"children":49},{},[50],{"type":14,"value":51},"Everything below refers to the host's uplink, which is whatever your default route uses:",{"data":53,"body":54},{},{"type":6,"children":55},[56,61,98],{"type":9,"tag":57,"props":58,"children":60},"terminal-teaser",{":lines":59},"[{\"cmd\":\"ip route | grep default\",\"out\":\"default via 192.168.1.1 dev wlan0 proto dhcp metric 600\"}]",[],{"type":9,"tag":10,"props":62,"children":63},{},[64,66,72,74,80,82,88,90,96],{"type":14,"value":65},"Take the name after ",{"type":9,"tag":17,"props":67,"children":69},{"className":68},[],[70],{"type":14,"value":71},"dev",{"type":14,"value":73},". On a laptop it is usually ",{"type":9,"tag":17,"props":75,"children":77},{"className":76},[],[78],{"type":14,"value":79},"wlan0",{"type":14,"value":81},"; on a server, ",{"type":9,"tag":17,"props":83,"children":85},{"className":84},[],[86],{"type":14,"value":87},"eth0",{"type":14,"value":89}," or something like ",{"type":9,"tag":17,"props":91,"children":93},{"className":92},[],[94],{"type":14,"value":95},"enp3s0",{"type":14,"value":97},".",{"type":9,"tag":99,"props":100,"children":104},"pre",{"className":101,"code":103,"language":14},[102],"language-text","DIF=\"wlan0\"              # the host's real interface\nBRIDGE_IFACE=\"bridge0\"\n",[105],{"type":9,"tag":17,"props":106,"children":108},{"__ignoreMap":107},"",[109],{"type":14,"value":103},{"data":111,"body":112},{},{"type":6,"children":113},[114,120,125,134],{"type":9,"tag":41,"props":115,"children":117},{"id":116},"enable-forwarding",[118],{"type":14,"value":119},"Enable forwarding",{"type":9,"tag":10,"props":121,"children":122},{},[123],{"type":14,"value":124},"By default a Linux host will not route packets between its interfaces at all — it is a host, not a router. That has to be turned on:",{"type":9,"tag":99,"props":126,"children":129},{"className":127,"code":128,"language":14},[102],"sudo sysctl -w net.ipv4.ip_forward=1\n",[130],{"type":9,"tag":17,"props":131,"children":132},{"__ignoreMap":107},[133],{"type":14,"value":128},{"type":9,"tag":10,"props":135,"children":136},{},[137,139,145,147,153],{"type":14,"value":138},"This one setting is behind a surprising share of \"containers have no internet\" incidents, because a ",{"type":9,"tag":17,"props":140,"children":142},{"className":141},[],[143],{"type":14,"value":144},"sysctl -w",{"type":14,"value":146}," is lost on reboot unless it is written into ",{"type":9,"tag":17,"props":148,"children":150},{"className":149},[],[151],{"type":14,"value":152},"/etc/sysctl.d/",{"type":14,"value":154},". Docker sets it on start; nothing sets it for you here.",{"data":156,"body":157},{},{"type":6,"children":158},[159,165,174,216,234,259],{"type":9,"tag":41,"props":160,"children":162},{"id":161},"the-three-rules",[163],{"type":14,"value":164},"The three rules",{"type":9,"tag":99,"props":166,"children":169},{"className":167,"code":168,"language":14},[102],"# 1. Masquerade container traffic as the host's own\nsudo iptables -t nat -A POSTROUTING -o $DIF -j MASQUERADE\n\n# 2. Allow container -> internet\nsudo iptables -A FORWARD -i $BRIDGE_IFACE -o $DIF -j ACCEPT\n\n# 3. Allow the replies back\nsudo iptables -A FORWARD -o $BRIDGE_IFACE -m state --state RELATED,ESTABLISHED -j ACCEPT\n",[170],{"type":9,"tag":17,"props":171,"children":172},{"__ignoreMap":107},[173],{"type":14,"value":168},{"type":9,"tag":10,"props":175,"children":176},{},[177,182,184,190,192,198,200,206,208,214],{"type":9,"tag":29,"props":178,"children":179},{},[180],{"type":14,"value":181},"Rule 1",{"type":14,"value":183}," is the translation. ",{"type":9,"tag":17,"props":185,"children":187},{"className":186},[],[188],{"type":14,"value":189},"POSTROUTING",{"type":14,"value":191}," in the ",{"type":9,"tag":17,"props":193,"children":195},{"className":194},[],[196],{"type":14,"value":197},"nat",{"type":14,"value":199}," table is the last point before a packet leaves; ",{"type":9,"tag":17,"props":201,"children":203},{"className":202},[],[204],{"type":14,"value":205},"MASQUERADE",{"type":14,"value":207}," rewrites its source address to whatever address the outgoing interface currently has. It is ",{"type":9,"tag":17,"props":209,"children":211},{"className":210},[],[212],{"type":14,"value":213},"SNAT",{"type":14,"value":215}," for interfaces whose address you don't know in advance — DHCP, a dial-up link, a laptop moving between networks.",{"type":9,"tag":10,"props":217,"children":218},{},[219,224,226,232],{"type":9,"tag":29,"props":220,"children":221},{},[222],{"type":14,"value":223},"Rule 2",{"type":14,"value":225}," permits forwarding from the bridge out to the world. Forwarding was enabled by the sysctl, but the ",{"type":9,"tag":17,"props":227,"children":229},{"className":228},[],[230],{"type":14,"value":231},"FORWARD",{"type":14,"value":233}," chain still has to accept the packet.",{"type":9,"tag":10,"props":235,"children":236},{},[237,242,244,249,251,257],{"type":9,"tag":29,"props":238,"children":239},{},[240],{"type":14,"value":241},"Rule 3",{"type":14,"value":243}," is the return path, and it is the interesting one. It does not permit the internet to initiate connections to your container — it permits packets belonging to a connection the container ",{"type":9,"tag":29,"props":245,"children":246},{},[247],{"type":14,"value":248},"already started",{"type":14,"value":250},". ",{"type":9,"tag":17,"props":252,"children":254},{"className":253},[],[255],{"type":14,"value":256},"RELATED,ESTABLISHED",{"type":14,"value":258}," is conntrack, the kernel's connection tracking table, deciding that a packet is part of a flow it has already seen.",{"type":9,"tag":10,"props":260,"children":261},{},[262,264,270],{"type":14,"value":263},"That asymmetry is why a container gets outbound internet access without becoming reachable from outside. Inbound access is a separate, deliberate act — a DNAT rule, which is what ",{"type":9,"tag":17,"props":265,"children":267},{"className":266},[],[268],{"type":14,"value":269},"-p 8080:80",{"type":14,"value":271}," creates.",{"data":273,"body":274},{},{"type":6,"children":275},[276,280],{"type":9,"tag":57,"props":277,"children":279},{":lines":278},"[{\"cmd\":\"sudo ip netns exec netns_my-container ping -c1 8.8.8.8\",\"out\":\"64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=14.2 ms\"},{\"cmd\":\"sudo iptables -t nat -L POSTROUTING -n -v\",\"out\":\"Chain POSTROUTING (policy ACCEPT)\\n pkts bytes target      prot opt in   out    source     destination\\n    4   336 MASQUERADE  all  --  *    wlan0  0.0.0.0/0  0.0.0.0/0\"}]",[],{"type":9,"tag":10,"props":281,"children":282},{},[283,285,290],{"type":14,"value":284},"The packet counter on that rule is the fastest diagnostic there is: zero means traffic is not reaching NAT at all, and the problem is routing or the ",{"type":9,"tag":17,"props":286,"children":288},{"className":287},[],[289],{"type":14,"value":231},{"type":14,"value":291}," chain rather than translation.",{"data":293,"body":294},{},{"type":6,"children":295},[296],{"type":9,"tag":297,"props":298,"children":303},"quiz",{":answer":299,":options":300,"explanation":301,"question":302},"0","[\"Because only replies to connections the container started should get in — accepting everything would expose it to the internet\",\"Because conntrack is faster than a plain ACCEPT rule\",\"Because MASQUERADE requires a state match to function\"]","A blanket ACCEPT on the return path would let anyone on the network initiate connections into the container. Matching on connection state keeps the door open only for traffic the container asked for, which is exactly the property that makes outbound-only the safe default.","Rule 3 permits `RELATED,ESTABLISHED` traffic back to the bridge. Why not simply accept everything inbound?",[],{"data":305,"body":306},{},{"type":6,"children":307},[308,314,319,387],{"type":9,"tag":41,"props":309,"children":311},{"id":310},"what-nat-costs-you",[312],{"type":14,"value":313},"What NAT costs you",{"type":9,"tag":10,"props":315,"children":316},{},[317],{"type":14,"value":318},"Worth knowing before you assume it is free.",{"type":9,"tag":320,"props":321,"children":322},"ul",{},[323,350,360,377],{"type":9,"tag":324,"props":325,"children":326},"li",{},[327,332,334,340,342,348],{"type":9,"tag":29,"props":328,"children":329},{},[330],{"type":14,"value":331},"Conntrack has a table, and the table has a size.",{"type":14,"value":333}," Every tracked flow occupies an entry. Under enough connections you hit ",{"type":9,"tag":17,"props":335,"children":337},{"className":336},[],[338],{"type":14,"value":339},"nf_conntrack: table full, dropping packet",{"type":14,"value":341}," in ",{"type":9,"tag":17,"props":343,"children":345},{"className":344},[],[346],{"type":14,"value":347},"dmesg",{"type":14,"value":349},", and traffic fails in a way that looks like random packet loss.",{"type":9,"tag":324,"props":351,"children":352},{},[353,358],{"type":9,"tag":29,"props":354,"children":355},{},[356],{"type":14,"value":357},"Inbound connections need explicit mapping.",{"type":14,"value":359}," Hence port publishing, and hence the fact that two containers cannot both publish host port 8080.",{"type":9,"tag":324,"props":361,"children":362},{},[363,368,370,375],{"type":9,"tag":29,"props":364,"children":365},{},[366],{"type":14,"value":367},"The container does not know its own public address.",{"type":14,"value":369}," It sees ",{"type":9,"tag":17,"props":371,"children":373},{"className":372},[],[374],{"type":14,"value":22},{"type":14,"value":376},"; the world sees the host's address. Anything that advertises its own address — SIP, FTP in active mode, some clustering protocols — needs to be told what to say.",{"type":9,"tag":324,"props":378,"children":379},{},[380,385],{"type":9,"tag":29,"props":381,"children":382},{},[383],{"type":14,"value":384},"It costs a little latency and CPU",{"type":14,"value":386}," on every packet. Rarely decisive, occasionally measurable at high packet rates.",{"type":9,"tag":10,"props":388,"children":389},{},[390],{"type":14,"value":391},"Kubernetes avoids most of this by giving every pod a routable address on a flat network, which is what a CNI plugin is for. The trade is complexity: you need something that actually routes those addresses.",{"data":393,"body":394},{},{"type":6,"children":395},[396],{"type":9,"tag":397,"props":398,"children":400},"deep-dive",{"title":399},"Reading the rules Docker writes",[401,414,423,450,455,512],{"type":9,"tag":10,"props":402,"children":403},{},[404,406,412],{"type":14,"value":405},"With Docker installed, ",{"type":9,"tag":17,"props":407,"children":409},{"className":408},[],[410],{"type":14,"value":411},"sudo iptables -t nat -L -n",{"type":14,"value":413}," shows the same shapes:",{"type":9,"tag":99,"props":415,"children":418},{"className":416,"code":417,"language":14},[102],"Chain POSTROUTING (policy ACCEPT)\nMASQUERADE  all  --  172.17.0.0/16   0.0.0.0/0\n\nChain DOCKER (2 references)\nRETURN     all  --  0.0.0.0/0        0.0.0.0/0\nDNAT       tcp  --  0.0.0.0/0        0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80\n",[419],{"type":9,"tag":17,"props":420,"children":421},{"__ignoreMap":107},[422],{"type":14,"value":417},{"type":9,"tag":10,"props":424,"children":425},{},[426,428,433,435,441,443,448],{"type":14,"value":427},"The ",{"type":9,"tag":17,"props":429,"children":431},{"className":430},[],[432],{"type":14,"value":205},{"type":14,"value":434}," is rule 1 above, scoped to the bridge subnet instead of an interface. The ",{"type":9,"tag":17,"props":436,"children":438},{"className":437},[],[439],{"type":14,"value":440},"DNAT",{"type":14,"value":442}," line is one ",{"type":9,"tag":17,"props":444,"children":446},{"className":445},[],[447],{"type":14,"value":269},{"type":14,"value":449}," — inbound packets for host port 8080 get their destination rewritten to the container.",{"type":9,"tag":10,"props":451,"children":452},{},[453],{"type":14,"value":454},"Two things follow that catch people out.",{"type":9,"tag":10,"props":456,"children":457},{},[458,463,465,471,473,479,481,487,489,494,496,502,504,510],{"type":9,"tag":29,"props":459,"children":460},{},[461],{"type":14,"value":462},"Docker's rules can outrank yours.",{"type":14,"value":464}," They live in a ",{"type":9,"tag":17,"props":466,"children":468},{"className":467},[],[469],{"type":14,"value":470},"DOCKER",{"type":14,"value":472}," chain jumped to early from ",{"type":9,"tag":17,"props":474,"children":476},{"className":475},[],[477],{"type":14,"value":478},"PREROUTING",{"type":14,"value":480},", so a published port is reachable even if you thought your ",{"type":9,"tag":17,"props":482,"children":484},{"className":483},[],[485],{"type":14,"value":486},"INPUT",{"type":14,"value":488}," firewall closed it. ",{"type":9,"tag":17,"props":490,"children":492},{"className":491},[],[493],{"type":14,"value":486},{"type":14,"value":495}," is not consulted for forwarded traffic at all. Publishing to ",{"type":9,"tag":17,"props":497,"children":499},{"className":498},[],[500],{"type":14,"value":501},"127.0.0.1:8080:80",{"type":14,"value":503}," rather than ",{"type":9,"tag":17,"props":505,"children":507},{"className":506},[],[508],{"type":14,"value":509},"8080:80",{"type":14,"value":511}," is the fix when you want a port bound only locally.",{"type":9,"tag":10,"props":513,"children":514},{},[515,520,522,528],{"type":9,"tag":29,"props":516,"children":517},{},[518],{"type":14,"value":519},"Restarting Docker rewrites the chains.",{"type":14,"value":521}," Rules you added by hand into a Docker-managed chain disappear. ",{"type":9,"tag":17,"props":523,"children":525},{"className":524},[],[526],{"type":14,"value":527},"DOCKER-USER",{"type":14,"value":529}," exists precisely for this: it is jumped to before Docker's own rules and is never flushed.",{"data":531,"body":532},{},{"type":6,"children":533},[534,542],{"type":9,"tag":535,"props":536,"children":541},"fill-blank",{":answer":537,"hint":538,"placeholder":539,"prompt":540},"[\"sudo sysctl -w net.ipv4.ip_forward=1\",\"sysctl -w net.ipv4.ip_forward=1\",\"sudo sysctl net.ipv4.ip_forward=1\"]","One sysctl key, set to 1.","sudo sysctl ...","Turn on IPv4 packet forwarding on the host at runtime.",[],{"type":9,"tag":10,"props":543,"children":544},{},[545],{"type":14,"value":546},"Next up: cgroups — capping what the container is allowed to consume, rather than what it is allowed to see.",1787908868380]