Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১১/২২অ্যাডভান্সড৫৫ মিনিট

Container Networking — veth, bridge, ও NAT দিয়ে একটা isolated namespace-কে জগতের সাথে জোড়া লাগানো

Container Networking: veth Pairs, Linux Bridges, and NAT

linux-namespaces-deep-dive লেসন একটা network namespace-কে সম্পূর্ণ বিচ্ছিন্ন রেখে থামিয়েছিল — শুধু একটা নিষ্ক্রিয় loopback। এই লেসন সেই বিচ্ছিন্নতা ভাঙে: veth pair দিয়ে namespace-কে host-এর সাথে, Linux bridge দিয়ে একাধিক container-কে একে অপরের সাথে, আর iptables MASQUERADE দিয়ে পুরো ব্যবস্থাকে internet-এর সাথে জোড়া লাগানো — শেষে CNI, যে standard interface দিয়ে এই পুরো কাজটাই একটা pluggable বহিরাগত প্রোগ্রামের হাতে তুলে দেওয়া হয়।

এই লেসন শেষে আপনি পারবেন

  • veth pair-এর mechanic — কেন এটা সবসময় জোড়ায় তৈরি হয়, এক প্রান্ত container-এর network namespace-এ কেন `eth0` নামে দেখা যায় আর অন্য প্রান্ত host namespace-এ কেন আলাদা নামে থাকে — নির্ভুলভাবে ব্যাখ্যা করতে পারবেন।
  • Linux bridge (`docker0`-কে concrete উদাহরণ ধরে) কীভাবে `networking/switching-arp` লেসনের MAC learning ও broadcast domain মডেল হুবহু পুনরায় ব্যবহার করে একাধিক container-কে একটা virtual switch-এ সংযুক্ত করে, তা বলতে পারবেন।
  • iptables-এর MASQUERADE rule কীভাবে একটা private container IP-কে host-এর real IP-তে rewrite করে internet-এ পাঠায়, আর conntrack কীভাবে reply-কে সঠিক container-এ ফেরত রুট করে — ধাপে ধাপে trace করতে পারবেন।
  • CNI (Container Network Interface)-এর নির্ভুল contract — stdin-এ JSON config, environment variable-এ container namespace path, আর plugin-এর দায়িত্ব (veth তৈরি, bridge-এ সংযুক্তি, IP বরাদ্দ, JSON ফলাফল ফেরত) — বর্ণনা করতে পারবেন।
  • একটা multi-container packet path — container-to-container ও container-to-internet দুই ক্ষেত্রেই — হাতে-কলমে hop-by-hop trace করতে পারবেন, আর কেন এই single-host architecture-ই যথেষ্ট না বহু-মেশিন orchestration-এর জন্য তা যুক্তি দিয়ে বলতে পারবেন।

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

linux-namespaces-deep-dive লেসনের network namespace অংশটা একটা ইচ্ছাকৃত অসম্পূর্ণ ছবি দিয়ে থেমেছিল। মনে করুন সেই ছবিটা ঠিক কেমন ছিল: একটা তাজা CLONE_NEWNET namespace তৈরি হলো, আর তার ভেতরে কী থাকে? প্রায় কিছুই না। একটা lo (loopback) ইন্টারফেস — কিন্তু ডিফল্টে DOWN। কোনো eth0 নেই। কোনো routing table entry নেই (default via ... তো দূরের কথা)। কোনো iptables rule নেই। বাইরের জগতের সাথে সংযুক্ত কোনো physical বা virtual device নেই।

নতুন network namespace-এর ভেতরের সম্পূর্ণ দৃশ্য:
  lo: <LOOPBACK> state DOWN
  (আর কিছুই নেই — কোনো eth0, কোনো route, কোনো bridge সংযোগ)

এটা নিরাপত্তার জন্য একটা চমৎকার শুরু-বিন্দু — সম্পূর্ণ বিচ্ছিন্ন মানে কোনো accidental leak নেই। কিন্তু এটা একটা বাস্তব container-এর জন্য সম্পূর্ণ অকেজো। একটা ওয়েব সার্ভার চালানো একটা container-এর দরকার: (১) host-এর সাথে কথা বলা, (২) একই host-এ চলা অন্য container-দের সাথে কথা বলা (একটা web container-এর তো database container-কে খুঁজে পেতে হবে), আর (৩) internet-এ বেরিয়ে যাওয়া (একটা apt install, একটা external API call)। namespace নিজে থেকে এর একটাও দেয় না — namespace শুধু বিচ্ছিন্নতা দেয়, সংযোগ না।

এই লেসন ঠিক এই ফাঁকটাই ভরাট করে, তিনটা স্তরে:

১. veth pair — একটা virtual “ক্যাবল” যার এক প্রান্ত container-এর namespace-এ, অন্য প্রান্ত host-এর namespace-এ। এটাই সেই একমাত্র primitive যা দিয়ে দুইটা সম্পূর্ণ বিচ্ছিন্ন network namespace-কে প্রথমবারের মতো সংযুক্ত করা যায়।

২. Linux bridge — host namespace-এ চলা একটা software switch, যেটা একাধিক container-এর veth-এর host-প্রান্তকে একসাথে জোড়া দেয়। এখানেই networking/switching-arp লেসনের পুরো vocabulary — MAC learning, forwarding, flooding, broadcast domain — সরাসরি ফিরে আসবে, শুধু physical switch-এর বদলে একটা kernel data structure হিসেবে।

৩. NAT (MASQUERADE) — bridge পর্যন্ত container-রা একে অপরকে দেখে, কিন্তু internet তাদের private IP চেনে না। iptables MASQUERADE rule সেই শেষ ধাপ — private source IP-কে host-এর real IP দিয়ে rewrite করে, আর ফেরত আসা reply-কে সঠিক container-এ ফিরিয়ে দেওয়ার হিসাব রাখে।

শেষে একটা প্রশ্ন থাকবে যা এই তিনটা primitive-এর প্রতিটাকেই প্রাসঙ্গিক করে তোলে বড় স্কেলে: একটা container runtime বা Kubernetes কি নিজে থেকেই veth/bridge/NAT-এর কোড লিখে রাখবে, নাকি এই পুরো “নতুন container-এর জন্য networking সেটআপ করো” কাজটা একটা pluggable বহিরাগত প্রোগ্রামের কাছে delegate করবে? উত্তর হলো CNI — এই module-এর পরের লেসনে (Kubernetes) যে abstraction-এর উপর পুরো ব্যবস্থা দাঁড়িয়ে থাকবে।

মূল ধারণা

veth pair — namespace-এর দেয়াল ভাঙার একমাত্র সাধারণ পথ

veth মানে “virtual ethernet” — কিন্তু নামের চেয়ে গুরুত্বপূর্ণ তার একটা কাঠামোগত নিয়ম: veth কখনো একা তৈরি হয় না, সবসময় জোড়ায়। ip link add veth-a type veth peer name veth-b কমান্ডটা একবারে দুইটা ইন্টারফেস তৈরি করে, আর তারা kernel-এর ভেতরে একে অপরের সাথে permanently সংযুক্ত — একটা virtual cable-এর দুই প্রান্তের মতো। এই প্রান্ত দুটো ভিন্ন network namespace-এ থাকতে পারে (এবং container networking-এ প্রায় সবসময় থাকেই), কিন্তু জোড়া ভাঙা যায় না — একটা veth মুছলে তার সঙ্গীও সাথে সাথে মুছে যায়।

ip link add veth-container type veth peer name veth-host

  veth-container  ◄────────── in-kernel তারবিহীন সংযোগ ──────────►  veth-host
  (এখনো root namespace-এ)                                          (host namespace-এই থাকবে)

container networking-এ ব্যবহারের standard pattern

একটা container তৈরি হওয়ার সময় (runtime-এর নেটওয়ার্কিং ধাপে, linux-namespaces-deep-dive লেসনের LayerTrace-এর ঠিক পরের ধাপ হিসেবে ভাবুন):

১. একটা নতুন veth pair তৈরি হয় host namespace-এ (namespace তৈরির সময় দুইটা প্রান্তই প্রথমে এখানেই থাকে)। ২. একটা প্রান্ত (সাধারণত veth<random-hex> নামে, যেমন veth1a2b3c) host namespace-এই থেকে যায়। ৩. অন্য প্রান্তটা ip link set <dev> netns <pid> দিয়ে container-এর network namespace-এ সরিয়ে দেওয়া হয়, আর সেখানে rename করা হয় — প্রায় সবসময় eth0। container-এর ভেতর থেকে ip addr show চালালে এই eth0-টাই দেখা যাবে, container-এর একমাত্র “বাইরের দিকের” ইন্টারফেস হিসেবে।

LayerTrace-এর ধারাবাহিকতা (linux-namespaces-deep-dive লেসন থেকে):
  ... pivot_root, sethostname, cgroup সেটআপ ...
  ip link add veth1a2b3c type veth peer name eth0netns   ← host namespace-এ, দুই প্রান্তই এখনো এখানে
  ip link set eth0netns netns <container-pid>            ← এক প্রান্ত container namespace-এ সরানো
  (container namespace-এর ভেতরে) ip link set eth0netns name eth0
  (container namespace-এর ভেতরে) ip addr add 172.17.0.2/16 dev eth0
  (container namespace-এর ভেতরে) ip link set eth0 up
  (container namespace-এর ভেতরে) ip link set lo up       ← আগের লেসনের সেই ধাপ, ভোলা যাবে না

লক্ষ করুন — এখন namespace-এর ভেতরের দৃশ্য সম্পূর্ণ বদলে গেছে। আগে শুধু নিষ্ক্রিয় lo ছিল; এখন একটা সক্রিয় eth0 আছে, একটা IP নিয়ে। কিন্তু এই eth0 এখনো শুধু host-এর veth-প্রান্তের সাথেই সংযুক্ত — অন্য কোনো container বা internet-এর সাথে সরাসরি কোনো সম্পর্ক নেই। সেই সংযোগ তৈরি করে পরের ধাপ: bridge।

Linux bridge — docker0, host namespace-এ চলা একটা software switch

এক container-এর একটা veth pair দিয়ে শুধু host-এর সাথে যুক্ত হওয়া যায় — কিন্তু দুইটা container-কে একে অপরের সাথে সরাসরি সংযুক্ত করার কোনো veth pair নেই (থাকলে প্রতিটা container-jodi-container জোড়ার জন্য আলাদা veth লাগত, যা O(n2)O(n^2) স্কেল করে না)। সমাধান: প্রতিটা container-এর host-প্রান্তের veth-কে একটা common bridge-এ সংযুক্ত করা।

Linux bridge একটা kernel-level software Ethernet switch — host namespace-এ (root network namespace-এ) চলে, আর একাধিক ইন্টারফেসকে (এখানে: একাধিক veth-এর host-প্রান্ত) একসাথে জোড়া দেয়। Docker-এর ডিফল্ট bridge-এর নাম docker0 — এটাই এই লেসনের concrete উদাহরণ।

                         host namespace (root netns)
                    ┌─────────────────────────────────┐
                    │            docker0 bridge         │
                    │   (software switch, 172.17.0.1)   │
                    └──┬──────────┬──────────┬──────────┘
                       │          │          │
                 veth1a2b3c  veth4d5e6f  veth7g8h9i
                       │          │          │
      ╔════════════════╪══╗  ╔═══╪═══════╗  ╔═╪═════════════╗
      ║ container "web"  ║  ║ "cache"    ║  ║ "db"          ║
      ║ eth0 172.17.0.2  ║  ║ eth0 .0.3  ║  ║ eth0 172.17.0.4║
      ╚══════════════════╝  ╚════════════╝  ╚═══════════════╝

bridge নিজেই একটা network interface — আর একটা IP-ও রাখে

এখানে একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ পয়েন্ট: docker0 শুধু একটা passthrough switch না, এটা নিজেই host-এর একটা network interface, আর তার নিজস্ব IP আছে — সাধারণত 172.17.0.1। এই IP-টাই সংযুক্ত সব container-এর default gateway।

# host namespace-এ (conceptual — Docker-এর ডিফল্ট সেটআপ)
ip addr show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    link/ether 02:42:1a:2b:3c:4d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

container-এর ভেতর থেকে ip route চালালে এটাই দেখা যাবে:

default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2

মানে: container-এর subnet-এর ভেতরের যেকোনো dst (172.17.0.0/16) সরাসরি bridge দিয়ে পাঠানো হয়, আর subnet-এর বাইরের যেকোনো কিছু (93.184.216.34-এর মতো internet IP) bridge-এর নিজের MAC-এ পাঠানো হয় — কারণ bridge-ই default gateway। bridge frame-টা পেয়ে দেখে dst-IP তার নিজের subnet-এ না, তাই সেটা আর সাধারণ L2 forwarding না — packet host-এর নিজস্ব routing stack-এ উঠে যায়, আর সেখান থেকেই NAT-এর গল্প শুরু হয়।

NAT (MASQUERADE) — শেষ ধাপ, internet পর্যন্ত

networking-এর NAT ধারণাটা এখানে ধরে নেওয়া — মূল কথাটা এক লাইনে: private, non-routable IP-কে একটা routable public IP দিয়ে rewrite করে বাইরের নেটওয়ার্কে পাঠানো, আর reply-র জন্য সেই rewrite-এর একটা টেবিল রেখে দেওয়া। এই লেসনের কাজ শুধু এই সাধারণ ধারণাটা ঠিক docker0/veth context-এ প্রয়োগ করে দেখানো, নতুন করে derive করা না।

container-এর IP (172.17.0.2) সম্পূর্ণ private — RFC 1918 রেঞ্জের ভেতরে, internet-এ routable না। container যখন 93.184.216.34:443-এ একটা packet পাঠায়, সেই packet host-এর routing stack পর্যন্ত পৌঁছায় (bridge-কে gateway ধরে), আর host-এর iptables POSTROUTING chain-এ একটা MASQUERADE rule অপেক্ষা করছে:

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

এই rule-এর অর্থ: 172.17.0.0/16 থেকে আসা, docker0-এর বাইরের দিকে (মানে internet-এর দিকে) যাওয়া যেকোনো packet-এর source IP আর source port host-এর real interface-এর IP আর একটা নতুন, ephemeral port দিয়ে rewrite করা হবে — আর এই mapping-টা kernel-এর conntrack টেবিলে সংরক্ষিত থাকবে।

আগে (container-এর ভেতরের দৃষ্টি):   src=172.17.0.2:34729  dst=93.184.216.34:443
MASQUERADE-এর পর (internet যা দেখে): src=203.0.113.10:40001 dst=93.184.216.34:443

conntrack টেবিল (host-এর ভেতরে, দৃশ্যমান):
  172.17.0.2:34729  <->  203.0.113.10:40001   (এই mapping-টাই reply-র রুট নির্ধারণ করে)

Reply ফিরে এলে (dst=203.0.113.10:40001), host conntrack টেবিলে সেই entry খুঁজে পায়, উল্টো rewrite করে (dst=172.17.0.2:34729), আর bridge → veth → container-এর eth0 পর্যন্ত পাঠিয়ে দেয়। container নিজে কখনো জানতেই পারে না তার packet-এর source IP পথে বদলে গিয়েছিল — পুরো ব্যাপারটা তার কাছে সম্পূর্ণ transparent।

CNI — এই পুরো কাজটাকে একটা standard interface-এ পরিণত করা

এতক্ষণ যা বর্ণনা করা হলো — veth তৈরি, namespace-এ move, bridge-এ attach, IP assign, routing/NAT rule বসানো — এটা একটা container runtime যদি প্রতিবার নিজের হাতে, hardcoded ভাবে করে, তাহলে একটা বড় সমস্যা তৈরি হয়: networking approach বদলাতে চাইলে পুরো runtime-এর কোড বদলাতে হবে। একটা organization হয়তো simple bridge-based networking চায়, আরেকটা multi-host overlay network চায় (Flannel-এর মতো), আরেকটা BGP-ভিত্তিক routing চায় (Calico-এর মতো), আরেকটা eBPF-ভিত্তিক উচ্চ-performance networking চায় (Cilium-এর মতো)। এই সবগুলো approach-এর জন্য একই runtime-এর ভেতরে আলাদা কোড রাখা অচল।

CNI (Container Network Interface) এই সমস্যার সমাধান একটা খুব পরিচিত software-engineering কৌশলে — networking সেটআপের পুরো দায়িত্বটাই একটা pluggable, বহিরাগত executable-এর কাছে সরিয়ে দেওয়া, একটা কঠোরভাবে সংজ্ঞায়িত interface দিয়ে। Runtime শুধু জানে “কীভাবে একটা CNI plugin-কে ডাকতে হয়” — plugin-টা bridge বসাবে, না overlay বানাবে, না অন্য কিছু করবে, সেটা runtime-এর জানারই দরকার নেই।

precise contract — ইনপুট

CNI plugin একটা সাধারণ executable (একটা binary বা script) — runtime সেটাকে চালায় আর দুই ধরনের ইনপুট দেয়:

১. stdin-এ একটা JSON network configuration:

{
  "cniVersion": "1.0.0",
  "name": "mynet",
  "type": "bridge",
  "bridge": "docker0",
  "isGateway": true,
  "ipam": {
    "type": "host-local",
    "subnet": "172.17.0.0/16",
    "gateway": "172.17.0.1"
  }
}

২. environment variable-এ operation-এর প্যারামিটার:

CNI_COMMAND=ADD                         ← ADD | DEL | CHECK | VERSION
CNI_CONTAINERID=a1b2c3d4e5f6            ← container-এর unique id
CNI_NETNS=/var/run/netns/a1b2c3d4e5f6   ← container-এর network namespace-এর path
CNI_IFNAME=eth0                          ← namespace-এর ভেতরে ইন্টারফেসের নাম কী হবে
CNI_PATH=/opt/cni/bin                    ← অন্য plugin binary কোথায় খুঁজে পাওয়া যাবে

precise contract — দায়িত্ব ও আউটপুট

CNI_COMMAND=ADD পেলে plugin-এর দায়িত্ব ঠিক এই লেসনের প্রথম দুই সেকশনে বর্ণিত কাজগুলোই বাস্তবায়ন করা: একটা veth pair তৈরি করা, এক প্রান্ত CNI_NETNS-এ নির্দেশিত namespace-এ সরিয়ে CNI_IFNAME নামে rename করা, অন্য প্রান্ত নির্দিষ্ট bridge-এ (বা overlay-তে) সংযুক্ত করা, IPAM (IP Address Management, প্রায়ই একটা আলাদা “IPAM plugin” হিসেবে chain হয়) দিয়ে একটা IP বরাদ্দ করা, আর প্রয়োজনে routing/NAT rule বসানো। শেষে plugin stdout-এ একটা JSON ফলাফল ফেরত দেয়:

{
  "cniVersion": "1.0.0",
  "interfaces": [
    { "name": "eth0", "mac": "02:42:ac:11:00:02", "sandbox": "/var/run/netns/a1b2c3d4e5f6" }
  ],
  "ips": [
    { "address": "172.17.0.2/16", "gateway": "172.17.0.1", "interface": 0 }
  ],
  "routes": [{ "dst": "0.0.0.0/0" }]
}

Runtime এই JSON পড়ে জানে ঠিক কী IP, কী গেটওয়ে, কী রুট বসানো হয়েছে — কিন্তু কীভাবে বসানো হয়েছে (bridge? overlay? eBPF program?), সেই বাস্তবায়ন-বিস্তারিত runtime-এর কাছে সম্পূর্ণ অস্বচ্ছ, আর তাই সম্পূর্ণ প্রতিস্থাপনযোগ্য।

ভেতরে কী ঘটছে

veth-এর ভেতরে — একটা device driver, দুই instance

kernel-এর দৃষ্টিকোণ থেকে একটা veth pair আসলে একটা একক device driver (drivers/net/veth.c)-এর দুইটা instance, যারা একে অপরকে পয়েন্ট করে থাকে। একটা প্রান্তে dev_queue_xmit() (একটা frame পাঠানোর kernel function) কল হলে, driver-টা সেই frame সরাসরি অন্য প্রান্তের netif_rx() (একটা frame receive করার kernel function) কল করে দেয় — কোনো actual hardware queue, কোনো DMA, কোনো interrupt জড়িত না। এই কারণেই veth-এর মধ্য দিয়ে যাওয়া traffic physical NIC-এর তুলনায় বহুগুণ দ্রুত — পুরো পথটাই একটা synchronous function call chain, kernel-এর একই address space-এর ভেতরে।

veth-a-তে transmit (dev_queue_xmit)
   │
   │  (কোনো wire নেই — সরাসরি kernel function call)
   ▼
veth-b-তে receive (netif_rx)

এই তথ্যটাই ব্যাখ্যা করে কেন container-থেকে-container traffic (একই host-এ) এত দ্রুত — যতক্ষণ দুইটা container একই bridge-এ থাকে, পুরো packet path-টা physical network hardware ছোঁয়ই না।

bridge-এর ভেতরে — একটা Linux বাসের transparent layer

Linux bridge একটা net_bridge kernel data structure, যেটা একাধিক network device-কে (এখানে veth host-প্রান্তগুলো) একটা একক L2 broadcast domain-এ একত্র করে। প্রতিটা সংযুক্ত device একটা bridge “port”। একটা frame কোনো port থেকে ঢুকলে bridge কোড:

১. source MAC + ingress port নোট করে FDB-তে (learning — switching-arp লেসনের হুবহু একই ধাপ) ২. destination MAC FDB-তে খোঁজে ৩. পাওয়া গেলে সেই এক port-এ পাঠায় (forwarding); না পাওয়া গেলে বাকি সব port-এ পাঠায় (flooding)

এই প্রক্রিয়াটা container networking-এ কোনো নতুন কোড না — এটা সেই decades-old Ethernet bridging কোড, শুধু physical port-এর বদলে veth port নিয়ে চলছে। এটাই সেই মূল অন্তর্দৃষ্টি যা এই লেসনের পুরো bridge অংশটাকে switching-arp লেসনের উপর সরাসরি দাঁড় করায় — নতুন mental model তৈরির দরকার নেই, শুধু পুরনোটা প্রয়োগ।

routing decision — কোথায় “L2” শেষ হয়, “L3” শুরু হয়

container-এর ভেতরের eth0-এর দৃষ্টিকোণ থেকে দুই ধরনের destination সম্পূর্ণ ভিন্নভাবে হ্যান্ডল হয়, আর এই সিদ্ধান্তটাই switching-arp লেসনের “L2 বনাম L3” আলোচনার হুবহু পুনরাবৃত্তি:

dst-IP নিজের subnet-এ (172.17.0.0/16)?
 ├─ হ্যাঁ  → dst-MAC = dst-IP-এর সরাসরি MAC (bridge শুধু forward করে, L2 decision)
 └─ না    → dst-MAC = gateway-র (bridge-এর) MAC (L2-তে শুধু bridge পর্যন্ত)
             bridge-এর নিজের IP layer packet-টা গ্রহণ করে
             host routing table lookup (L3 decision)
             iptables POSTROUTING (MASQUERADE) — L3-এর পরের ধাপ
             physical/real interface দিয়ে বেরিয়ে যাওয়া

সবচেয়ে গুরুত্বপূর্ণ পয়েন্ট: container নিজে কখনো জানে না তার packet bridge পর্যন্তই L2 না internet পর্যন্ত পুরোটাই L2 — সে শুধু bridge-এর MAC-এ frame পাঠায়। বাকি সিদ্ধান্ত সম্পূর্ণভাবে host-এর routing stack-এর কাজ, container-এর namespace-এর বাইরে।

উদাহরণ

একটা container spawn-এর networking ধাপ, একটা সম্পূর্ণ trace

linux-namespaces-deep-dive-এর LayerTrace-এর সরাসরি সম্প্রসারণ — namespace তৈরির পর networking সেটআপ
  1. network namespace তৈরি (আগের লেসনের বিষয়)শুধু নিষ্ক্রিয় lo — কোনো eth0, কোনো route, কোনো bridge সংযোগ নেই
  2. ip link add veth1a2b3c type veth peer name eth0tmphost namespace-এ একটা veth pair তৈরি — দুই প্রান্তই এখনো এখানে
  3. ip link set eth0tmp netns <container-pid>এক প্রান্ত container namespace-এ সরানো; অন্য প্রান্ত (veth1a2b3c) host namespace-এই থেকে যায়
  4. (namespace-এর ভেতরে) rename eth0tmp -> eth0; ip addr add 172.17.0.2/16; ip link set upnamespace-এর ভেতরের দৃষ্টিতে এখন একটা সক্রিয় eth0 আছে
  5. ip link set veth1a2b3c master docker0host-প্রান্তের veth bridge-এ attach — এখন container bridge-এর broadcast domain-এর অংশ
  6. ip link set veth1a2b3c upbridge port সক্রিয় করা, নাহলে কোনো frame পার হবে না
  7. iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADEcontainer subnet-এর outbound traffic-এর জন্য NAT rule (bridge-প্রথম container-এই একবার বসে, প্রতি-container না)

লক্ষ করুন MASQUERADE rule পুরো 172.17.0.0/16 subnet-এর জন্য একবারই বসে (bridge তৈরির সময়), প্রতিটা container-এর জন্য আলাদা করে না — যেকোনো নতুন container এই subnet-এর একটা IP পেলেই স্বয়ংক্রিয়ভাবে এই rule-এর আওতায় চলে আসে।

container-to-container packet path — hop-by-hop

web (172.17.0.2) container cache (172.17.0.3)-কে port 6379-এ একটা packet পাঠাচ্ছে, দুইজনই একই docker0 bridge-এ:

১. web-এর ভেতরে: dst-IP 172.17.0.3 নিজের subnet-এ (172.17.0.0/16-এর ভেতরে)
   → dst-MAC = cache-এর MAC (ARP দিয়ে resolve, বা ইতিমধ্যে cache-এ থাকলে সরাসরি)
২. web eth0 -> veth (in-kernel copy, তাৎক্ষণিক) -> veth1a2b3c (host-প্রান্ত, bridge-এর port)
৩. docker0: source MAC (web) শেখে; destination MAC (cache) FDB-তে খোঁজে
   - প্রথমবার হলে: unknown -> FLOOD (সব port-এ, ঠিক switching-arp লেসনের মতো)
   - পরে (cache নিজে কিছু transmit করলে): known -> FORWARD শুধু cache-এর port-এ
৪. veth4d5e6f (cache-এর host-প্রান্ত) -> veth (in-kernel copy) -> cache eth0
৫. cache receive করল — কোনো L3 routing জড়িত হয়নি, পুরোটাই L2 bridge decision

container-to-internet packet path — hop-by-hop

সেই একই web container এবার 93.184.216.34:443-এ (internet-এর একটা সার্ভার) একটা packet পাঠাচ্ছে:

১. web-এর ভেতরে: dst-IP নিজের subnet-এ না (172.17.0.0/16-এর বাইরে)
   → dst-MAC = default gateway (docker0)-এর MAC
২. web eth0 -> veth -> veth1a2b3c -> docker0
৩. docker0: dst-IP নিজের subnet-এ না -> L2 forwarding না, host-এর routing stack-এ উঠে যায়
৪. host routing table: এই dst-এর জন্য route আছে host-এর real interface দিয়ে
৫. iptables POSTROUTING chain: MASQUERADE rule মিলল
   src 172.17.0.2:34729 -> host-এর real IP:ephemeral-port (conntrack entry তৈরি)
৬. host-এর real interface দিয়ে internet-এ প্যাকেট বেরিয়ে যায়
৭. (reply আসলে) conntrack টেবিলে reverse lookup -> dst rewrite -> bridge -> veth -> web eth0

এই দুই trace-এর মধ্যে মূল পার্থক্যটাই এই লেসনের কেন্দ্রীয় শিক্ষা: local traffic-এর জন্য শুধু L2 bridge decision যথেষ্ট; internet-বাউন্ড traffic-এর জন্য L3 routing + NAT দুটোই লাগে। container নিজে কখনো এই পার্থক্য জানতেই পারে না — তার কাছে দুটোই শুধু “dst-MAC-এ একটা frame পাঠানো”।

ছবিতে

Container network namespace           Host (root) network namespace
┌──────────────────────────┐          ┌───────────────────────────────┐
│                            │          │                                │
│   eth0 (namespace-এর       │          │   veth1a2b3c (host-প্রান্ত,       │
│   ভেতরে এই নামেই দেখা যায়) │◄────────►│   docker0 bridge-এর একটা port)  │
│   172.17.0.2/16            │  veth    │                                │
│                            │  pair    │   docker0 (bridge, 172.17.0.1) │
│   lo (up, 127.0.0.1)       │          │                                │
└──────────────────────────┘          └───────────────────────────────┘

একই device driver-এর দুই instance — এক প্রান্তে transmit,
অন্য প্রান্তে সাথে সাথে receive। কোনো physical medium নেই।
চিত্র ১veth pair-এর দুই প্রান্ত — এক namespace থেকে অন্য namespace-এ 'তার' টানা
                  docker0 (172.17.0.1) — একটা broadcast domain
        ┌─────────────┬─────────────┬─────────────┐
        │ veth1a2b3c   │ veth4d5e6f   │ veth7g8h9i   │
      ┌─┴───┐        ┌─┴─────┐     ┌─┴────┐
      │ web │        │ cache │     │  db  │
      │ .0.2│        │  .0.3 │     │ .0.4 │
      └─────┘        └───────┘     └──────┘

FDB evolution (শুধু source দেখে শেখে):
  t0: {}  (bridge খালি)
  t1: web -> cache পাঠাল      → FDB: {web: veth1a2b3c}       (cache এখনো অজানা, flood হলো)
  t2: cache -> web রিপ্লাই দিল → FDB: {web: ..., cache: veth4d5e6f}
  t3: db কখনো নিজে থেকে কিছু
      পাঠায়নি                 → FDB-তে db কখনোই আসেনি — db-মুখী সব frame চিরকাল flood
চিত্র ২তিন-container bridge topology — MAC learning table-এর evolution, একই bridge-এ
                          docker0 subnet                    internet
                       172.17.0.0/16                     (public IP space)
┌──────────┐   L2 bridge   ┌────────┐   L3 route + NAT   ┌─────────────┐
│  web      │──────────────►│ docker0│────────────────────►│ 93.184.216.34│
│ .0.2:34729│                │ (gw)   │                     │    :443      │
└──────────┘                └────────┘                     └─────────────┘
                                  │
                                  │ POSTROUTING MASQUERADE
                                  ▼
                     conntrack: 172.17.0.2:34729 <-> 203.0.113.10:40001
                     (host-এর real IP, ephemeral port — internet এটাই দেখে)
চিত্র ৩NAT/MASQUERADE-এর আগে-পরে — private IP আর conntrack mapping

নিজে চালিয়ে দেখুন

EXPERIMENT

একটা honest চেষ্টা — এই sandbox-এ সত্যিকার veth/bridge/netns তৈরি করা সম্ভব কি?

এই sandbox: macOS (Darwin) — Linux veth/bridge netlink API macOS-এ নেই· ৫ মিনিট

linux-namespaces-deep-dive লেসনের experiment অংশে যেভাবে সততার সাথে স্বীকার করা হয়েছিল যে এই লেসনগুলো একটা macOS sandbox-এ লেখা হচ্ছে, এখানেও একই সীমাবদ্ধতা প্রযোজ্য — বরং আরও কঠোরভাবে, কারণ veth/bridge শুধু একটা syscall না, পুরো ip link/bridge টুলচেইনের উপর নির্ভরশীল যা macOS-এ থাকেই না।

which ip
uname -a

এই sandbox-এ সত্যিই চালিয়ে পাওয়া output:

ip not found
Darwin MAs-MacBook-Pro.local 25.5.0 Darwin Kernel Version 25.5.0: ... RELEASE_X86_64 x86_64

ip কমান্ড (iproute2 প্যাকেজের অংশ, যেটা veth/bridge তৈরির স্ট্যান্ডার্ড টুল) macOS-এ নেই-ই — এটা কোনো permission সমস্যা না, প্যাকেজটাই এই platform-এ বিদ্যমান না, কারণ এটা Linux-এর rtnetlink socket family-র উপর নির্ভরশীল, যা macOS-এর XNU kernel-এ implement করা হয়নি।

এটা কী প্রমাণ করে

veth pair, Linux bridge, আর network namespace — তিনটাই Linux kernel-নির্দিষ্ট netlink/rtnetlink API-র উপর দাঁড়িয়ে; macOS-এর BSD-বংশোদ্ভূত network stack-এ এদের কোনো সমতুল্য নেই, তাই এই লেসনের 'সত্যিই চালানো' প্রমাণ পরের build সেকশনের Python simulation-এই থাকবে।

নিজে বানান

BUILD IT

Container Network Simulator — veth, bridge, ও NAT-এর packet-path একটা চলমান Python মডেলে

Python · ●●●○○
  1. VethPair ক্লাস — দুই নাম (container-প্রান্ত, host-প্রান্ত) ধরে রাখে, cross() মেথড in-kernel copy simulate করে
  2. Container ক্লাস — name, ip, mac, নিজের veth pair
  3. Bridge ক্লাস — switching-arp লেসনের এক্সাক্ট মডেল পুনর্ব্যবহার: mac_table (FDB), learn(), forward() যা FORWARD/FLOOD সিদ্ধান্ত নেয়
  4. NatTable ক্লাস — (container_ip, port) থেকে (host_public_ip, ephemeral_port)-এ mapping, masquerade() আর reverse_lookup()
  5. send_packet() ফাংশন — dst subnet-এর ভেতরে না বাইরে তার ভিত্তিতে local bridge-path বা NAT-path বেছে নেয়, প্রতিটা hop প্রিন্ট করে
  6. তিনটা container bridge-এ বসিয়ে container-to-container আর container-to-internet দুই ধরনের ট্রাফিক চালিয়ে দেখা

এই প্রোগ্রামটা এই লেসনের প্রধান “সত্যিই চালানো” প্রমাণ, কারণ পূর্ববর্তী experiment সেকশনে দেখানো হয়েছে raw Linux netlink কমান্ড এই macOS sandbox-এ চালানো সম্ভব না। এই simulation সেই সীমাবদ্ধতা থেকে সম্পূর্ণ মুক্ত — pure Python, কোনো OS-নির্দিষ্ট syscall লাগে না, কিন্তু প্রতিটা hop হুবহু বাস্তব kernel-এর veth/bridge/iptables যুক্তি অনুসরণ করে।

#!/usr/bin/env python3
"""
container_net_sim.py — একটা Docker-style single-host container network-এর
packet-path simulation: veth pair, Linux bridge (docker0), MAC learning
(switching-arp লেসনের এক্সাক্ট মডেল পুনর্ব্যবহার), আর outbound NAT/MASQUERADE।
"""

class VethPair:
    """একটা virtual ethernet cable — দুই প্রান্ত, in-kernel, তারবিহীন।
    এক প্রান্তে যা ঢোকে, অন্য প্রান্তে হুবহু তাৎক্ষণিক বেরিয়ে আসে।"""
    def __init__(self, container_end_name, host_end_name):
        self.container_end = container_end_name   # container namespace-এ eth0
        self.host_end = host_end_name              # host namespace-এ vethXXXX

    def cross(self, frame_desc):
        return f"[veth in-kernel copy: {self.container_end} -> {self.host_end}] {frame_desc}"


class Container:
    def __init__(self, name, ip, mac, veth):
        self.name = name
        self.ip = ip
        self.mac = mac
        self.veth = veth   # এই container-এর veth pair


class Bridge:
    """docker0 — host namespace-এ চলা একটা software switch।
    switching-arp লেসনের এক্সাক্ট মডেল: learn() source দেখে শেখে,
    forward() destination lookup করে FORWARD/FLOOD সিদ্ধান্ত নেয়।"""
    def __init__(self, name, gateway_ip):
        self.name = name
        self.gateway_ip = gateway_ip
        self.mac_table = {}          # mac -> veth host-end name (port)
        self.ports = {}              # veth host-end name -> Container

    def attach(self, container):
        self.ports[container.veth.host_end] = container
        print(f"  [bridge {self.name}] {container.veth.host_end} attached "
              f"(container {container.name}, {container.mac})")

    def learn(self, mac, port):
        if self.mac_table.get(mac) != port:
            self.mac_table[mac] = port
            print(f"  [bridge {self.name}] LEARN {mac} -> {port}")

    def forward(self, src_container, dst_mac, trace):
        self.learn(src_container.mac, src_container.veth.host_end)
        out_port = self.mac_table.get(dst_mac)
        if out_port is None:
            other_ports = [p for p in self.ports if p != src_container.veth.host_end]
            trace.append(f"  [bridge {self.name}] dst {dst_mac} unknown -> FLOOD {other_ports}")
            return None
        trace.append(f"  [bridge {self.name}] dst {dst_mac} known -> FORWARD {out_port}")
        return self.ports[out_port]


class NatTable:
    """host-এর iptables POSTROUTING MASQUERADE rule-এর একটা mapping-টেবিল রূপ।
    NAT-এর মূল ধারণা (private->public address rewrite, reply-তে reverse
    lookup) এখানে অনুমিত জ্ঞান — শুধু docker0/veth context-এ প্রয়োগ করছি।"""
    def __init__(self, host_public_ip):
        self.host_public_ip = host_public_ip
        self.table = {}   # (container_ip, container_port) -> ephemeral_port
        self._next_ephemeral = 40001

    def masquerade(self, container_ip, container_port, dst_ip, dst_port, trace):
        key = (container_ip, container_port)
        if key not in self.table:
            self.table[key] = self._next_ephemeral
            self._next_ephemeral += 1
        eph = self.table[key]
        trace.append(
            f"  [iptables MASQUERADE] {container_ip}:{container_port} -> "
            f"{self.host_public_ip}:{eph}  (conntrack entry তৈরি/আপডেট)"
        )
        trace.append(
            f"  [host eth0 -> internet] src={self.host_public_ip}:{eph} dst={dst_ip}:{dst_port}"
        )
        return eph

    def reverse_lookup(self, eph, trace):
        for (cip, cport), e in self.table.items():
            if e == eph:
                trace.append(
                    f"  [conntrack reverse] {self.host_public_ip}:{eph}-এ আসা reply -> "
                    f"{cip}:{cport}-এ ফেরত রুট"
                )
                return cip, cport
        return None


def send_packet(src, dst_ip, dst_port, bridge, containers_by_ip, nat, subnet_prefix):
    """একটা container থেকে packet পাঠানোর সম্পূর্ণ hop trace।
    subnet-এর ভেতরে হলে bridge-এর মধ্য দিয়ে সরাসরি, বাইরে হলে NAT দিয়ে।"""
    trace = [f"== {src.name} ({src.ip}) -> {dst_ip}:{dst_port} =="]
    trace.append(f"  [{src.name} eth0] packet built, dst={dst_ip}:{dst_port}")

    is_local = dst_ip.startswith(subnet_prefix)

    if is_local:
        dst = containers_by_ip[dst_ip]
        trace.append(f"  [{src.name} eth0 -> veth] {src.veth.cross(f'frame dst-mac={dst.mac}')}")
        winner = bridge.forward(src, dst.mac, trace)
        if winner is None:
            trace.append(f"  (flood-এর ভেতর {dst.name}-ও frame পেল, ARP-এর মতোই MAC শিখে নিল)")
            winner = dst
        trace.append(f"  [veth -> {winner.name} eth0] {winner.veth.cross('frame delivered')}")
        trace.append(f"  [{winner.name} eth0] packet received from {src.ip}")
    else:
        # subnet-এর বাইরে -> bridge-এর gateway IP-ই default route, তারপর NAT
        trace.append(f"  [{src.name} eth0 -> veth] dst-mac={bridge.gateway_ip}-এর MAC (bridge নিজেই gateway)")
        trace.append(f"  [bridge {bridge.name}] local subnet-এ নেই -> host routing stack-এ up")
        eph = nat.masquerade(src.ip, 34000 + hash(src.name) % 1000, dst_ip, dst_port, trace)
        trace.append(f"  [internet] {dst_ip}:{dst_port} প্যাকেট পেল, src দেখছে শুধু {nat.host_public_ip}:{eph}")
        trace.append(f"  [internet -> host eth0] reply আসছে dst={nat.host_public_ip}:{eph}")
        nat.reverse_lookup(eph, trace)
        trace.append(f"  [host -> bridge -> veth -> {src.name} eth0] reply {src.name}-এ ফিরে গেল")

    print("\n".join(trace))
    print()


if __name__ == "__main__":
    bridge = Bridge("docker0", gateway_ip="172.17.0.1")
    nat = NatTable(host_public_ip="203.0.113.10")

    c1 = Container("web",   "172.17.0.2", "02:42:ac:11:00:02", VethPair("eth0", "veth1a2b3c"))
    c2 = Container("cache", "172.17.0.3", "02:42:ac:11:00:03", VethPair("eth0", "veth4d5e6f"))
    c3 = Container("db",    "172.17.0.4", "02:42:ac:11:00:04", VethPair("eth0", "veth7g8h9i"))

    for c in (c1, c2, c3):
        bridge.attach(c)
    print()

    containers_by_ip = {c.ip: c for c in (c1, c2, c3)}

    print("---- container-to-container (একই bridge, একই host) ----\n")
    send_packet(c1, "172.17.0.3", 6379, bridge, containers_by_ip, nat, "172.17.")
    send_packet(c1, "172.17.0.3", 6379, bridge, containers_by_ip, nat, "172.17.")

    print("---- container-to-outside (internet-এর দিকে, NAT দিয়ে) ----\n")
    send_packet(c1, "93.184.216.34", 443, bridge, containers_by_ip, nat, "172.17.")
    send_packet(c2, "93.184.216.34", 443, bridge, containers_by_ip, nat, "172.17.")

    print("---- চূড়ান্ত bridge MAC table ----")
    for mac, port in sorted(bridge.mac_table.items()):
        print(f"  {mac} -> {port}")

    print("\n---- চূড়ান্ত NAT table ----")
    for (cip, cport), eph in nat.table.items():
        print(f"  {cip}:{cport} <-> {nat.host_public_ip}:{eph}")

এই sandbox-এ সত্যিই চালিয়ে পাওয়া output (macOS-এ Python 3.9.6, সম্পূর্ণ portable কোড — কোনো Linux syscall লাগেনি):

  [bridge docker0] veth1a2b3c attached (container web, 02:42:ac:11:00:02)
  [bridge docker0] veth4d5e6f attached (container cache, 02:42:ac:11:00:03)
  [bridge docker0] veth7g8h9i attached (container db, 02:42:ac:11:00:04)

---- container-to-container (একই bridge, একই host) ----

  [bridge docker0] LEARN 02:42:ac:11:00:02 -> veth1a2b3c
== web (172.17.0.2) -> 172.17.0.3:6379 ==
  [web eth0] packet built, dst=172.17.0.3:6379
  [web eth0 -> veth] [veth in-kernel copy: eth0 -> veth1a2b3c] frame dst-mac=02:42:ac:11:00:03
  [bridge docker0] dst 02:42:ac:11:00:03 unknown -> FLOOD ['veth4d5e6f', 'veth7g8h9i']
  (flood-এর ভেতর cache-ও frame পেল, ARP-এর মতোই MAC শিখে নিল)
  [veth -> cache eth0] [veth in-kernel copy: eth0 -> veth4d5e6f] frame delivered
  [cache eth0] packet received from 172.17.0.2

== web (172.17.0.2) -> 172.17.0.3:6379 ==
  [web eth0] packet built, dst=172.17.0.3:6379
  [web eth0 -> veth] [veth in-kernel copy: eth0 -> veth1a2b3c] frame dst-mac=02:42:ac:11:00:03
  [bridge docker0] dst 02:42:ac:11:00:03 unknown -> FLOOD ['veth4d5e6f', 'veth7g8h9i']
  (flood-এর ভেতর cache-ও frame পেল, ARP-এর মতোই MAC শিখে নিল)
  [veth -> cache eth0] [veth in-kernel copy: eth0 -> veth4d5e6f] frame delivered
  [cache eth0] packet received from 172.17.0.2

---- container-to-outside (internet-এর দিকে, NAT দিয়ে) ----

== web (172.17.0.2) -> 93.184.216.34:443 ==
  [web eth0] packet built, dst=93.184.216.34:443
  [web eth0 -> veth] dst-mac=172.17.0.1-এর MAC (bridge নিজেই gateway)
  [bridge docker0] local subnet-এ নেই -> host routing stack-এ up
  [iptables MASQUERADE] 172.17.0.2:34729 -> 203.0.113.10:40001  (conntrack entry তৈরি/আপডেট)
  [host eth0 -> internet] src=203.0.113.10:40001 dst=93.184.216.34:443
  [internet] 93.184.216.34:443 প্যাকেট পেল, src দেখছে শুধু 203.0.113.10:40001
  [internet -> host eth0] reply আসছে dst=203.0.113.10:40001
  [conntrack reverse] 203.0.113.10:40001-এ আসা reply -> 172.17.0.2:34729-এ ফেরত রুট
  [host -> bridge -> veth -> web eth0] reply web-এ ফিরে গেল

== cache (172.17.0.3) -> 93.184.216.34:443 ==
  [cache eth0] packet built, dst=93.184.216.34:443
  [cache eth0 -> veth] dst-mac=172.17.0.1-এর MAC (bridge নিজেই gateway)
  [bridge docker0] local subnet-এ নেই -> host routing stack-এ up
  [iptables MASQUERADE] 172.17.0.3:34264 -> 203.0.113.10:40002  (conntrack entry তৈরি/আপডেট)
  [host eth0 -> internet] src=203.0.113.10:40002 dst=93.184.216.34:443
  [internet] 93.184.216.34:443 প্যাকেট পেল, src দেখছে শুধু 203.0.113.10:40002
  [internet -> host eth0] reply আসছে dst=203.0.113.10:40002
  [conntrack reverse] 203.0.113.10:40002-এ আসা reply -> 172.17.0.3:34264-এ ফেরত রুট
  [host -> bridge -> veth -> cache eth0] reply cache-এ ফিরে গেল

---- চূড়ান্ত bridge MAC table ----
  02:42:ac:11:00:02 -> veth1a2b3c

---- চূড়ান্ত NAT table ----
  172.17.0.2:34729 <-> 203.0.113.10:40001
  172.17.0.3:34264 <-> 203.0.113.10:40002

দুইটা ফলাফল বিশেষভাবে লক্ষ করার মতো, দুটোই এই sandbox-এ সত্যিই দেখা গেছে, কল্পনা করা হয়নি:

প্রথম — web -> cache দুইবার পাঠানো হয়েছে, কিন্তু দুইবারই FLOOD হয়েছে, দ্বিতীয়বারও FORWARD না। কারণ: cache container কখনো নিজে থেকে কিছু transmit করেনি এই simulation-এ (শুধু receive করেছে), তাই bridge কখনো তার MAC শেখেইনি — এটা “hood” সেকশনের callout-এ যা বলা হয়েছিল তার সরাসরি, প্রমাণিত উদাহরণ।

দ্বিতীয় — চূড়ান্ত bridge MAC table-এ শুধু web-এর MAC আছে, cache-এর না — একই কারণে। bridge শেখে শুধু কে পাঠিয়েছে, কে পেয়েছে তা না।

নিজে বাড়ান:

  1. cache থেকে web-এ একটা reply পাঠানোর event যোগ করুন — দেখুন এরপর bridge টেবিলে cache-এর MAC-ও যোগ হয়, আর তৃতীয়বার web -> cache পাঠালে এবার FLOOD-এর বদলে FORWARD হয় কি না
  2. একটা দ্বিতীয় bridge (br1) আর একটা দ্বিতীয় subnet (10.0.0.0/24) যোগ করুন — দুই bridge-এর মধ্যে সরাসরি কোনো সংযোগ না থাকলে কী হয়, সেটা trace করুন
  3. db container একটা port-এ inbound connection accept করবে এমন দৃশ্যকল্প মডেল করুন — এতে DNAT (iptables -t nat -A PREROUTING, port mapping-এর ভিত্তি — docker run -p-এর মেকানিজম) যোগ করা লাগবে, যা এই লেসনের MASQUERADE-এর ঠিক বিপরীত দিক

বাস্তব সিস্টেমে

বাস্তব সিস্টেমে — veth/bridge/NAT/CNI কোথায় কাজ করছে

  • Docker-এর ডিফল্ট bridge network — এই পুরো লেসনের সবচেয়ে সরাসরি প্রয়োগ। docker network inspect bridge চালালে ঠিক docker0, তার 172.17.0.0/16 subnet, আর সংযুক্ত container-দের veth mapping দেখা যায়। docker run -p 8080:80 আসলে একটা DNAT rule (iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80) — এই লেসনের MASQUERADE (outbound) ঠিক বিপরীত দিক (inbound)।

  • bridge fdb show — এই কমান্ডটাই networking/switching-arp লেসনের experiment-এর সাথে সরাসরি যুক্ত করে এই লেসনকে। একটা Docker host-এ bridge fdb show br docker0 চালালে ঠিক এই লেসনের build সেকশনের simulation-এ দেখানো FDB-টাই, বাস্তব MAC address সহ, দেখা যায়।

  • Kubernetes Pod networking, CNI plugin-এর মাধ্যমে — প্রতিটা Pod একটা network namespace পায় (আসলে Pod-এর “pause container”-এর namespace, বাকি সব container সেটা share করে), আর kubelet একটা CNI plugin ডেকে সেই namespace-এ veth/IP/routing সেটআপ করায়। Flannel সাধারণত একটা overlay network তৈরি করে (VXLAN দিয়ে, একাধিক host-এর bridge-কে যুক্ত করে) — একই মূল primitive (veth, bridge), কিন্তু multi-host-এ প্রসারিত। Calico ভিন্ন পথ নেয় — bridge-এর বদলে সরাসরি routing (BGP দিয়ে প্রতিটা node-কে প্রতিটা Pod subnet-এর রুট জানানো), overlay এড়িয়ে। Cilium eBPF ব্যবহার করে, traditional iptables/bridge-এর অনেক ধাপ kernel-এর ভেতরে দ্রুততর program দিয়ে প্রতিস্থাপন করে — কিন্তু তিনটাই একই CNI contract মেনে চলে, তাই Kubernetes-এর কাছে “একই রকম” pluggable।

  • kube-proxy ও Service routing — Kubernetes Service-এর ClusterIP বাস্তবায়িত হয় iptables (বা IPVS) rule দিয়ে, এই লেসনের MASQUERADE-এর একই পরিবারের কৌশল — একটা virtual IP-কে বাস্তব Pod IP-তে rewrite করা, শুধু দিক ভিন্ন (external-to-internal, internal-to-internal)।

  • cloud provider VPC-networking (AWS VPC CNI, GCP-এর native routing) — কিছু cloud-native CNI plugin bridge/NAT সম্পূর্ণ এড়িয়ে যায়, প্রতিটা Pod-কে সরাসরি VPC-এর একটা real, routable IP দেয় (host-এর নিজের ENI/secondary IP থেকে) — performance-এর জন্য NAT layer বাদ দেওয়া, কিন্তু IP allocation-এর জটিলতা বাড়ানোর বিনিময়ে।

যে ভুলগুলো সবাই করে

“veth pair-এর দুই প্রান্ত সবসময় একটা physical NIC-এর মতো ধীর, কারণ এটা 'নেটওয়ার্ক ট্রাফিক'।”

উল্টো — veth pair-এর মধ্য দিয়ে যাওয়া traffic physical NIC-এর তুলনায় বহুগুণ দ্রুত, কারণ কোনো physical medium, কোনো hardware queue, কোনো interrupt জড়িত না। এক প্রান্তে dev_queue_xmit() কল হলে driver সরাসরি অন্য প্রান্তের netif_rx() কল করে দেয় — পুরো পথটা একটা synchronous kernel function call chain, একই CPU-র একই address space-এর ভেতরে। দুইটা container একই bridge-এ থাকলে তাদের মধ্যকার traffic কখনো physical network hardware ছোঁয়ই না।

“Docker container-রা internet-এ সরাসরি তাদের নিজস্ব IP (172.17.0.x) নিয়ে বেরিয়ে যায়, ঠিক যেমন host নিজের IP নিয়ে বেরোয়।”

সম্পূর্ণ ভুল, আর এটাই NAT-এর পুরো প্রয়োজনীয়তা। 172.17.0.x একটা RFC 1918 private range-এর অংশ — internet-এ routable না, কোনো reply এই address-এ ফিরে আসতে পারবে না। container-এর outbound traffic host-এর iptables POSTROUTING chain-এর একটা MASQUERADE rule-এ গিয়ে rewrite হয় — source IP আর port host-এর real, routable IP আর একটা ephemeral port দিয়ে প্রতিস্থাপিত হয়, আর conntrack টেবিলে সেই mapping সংরক্ষিত থাকে reply রাউট করার জন্য। internet কখনো container-এর private IP দেখেই না।

“Linux bridge একটা container-নির্দিষ্ট নতুন প্রযুক্তি, Docker-এর জন্য তৈরি।”

Linux bridge Docker-এর অনেক আগে থেকেই kernel-এর অংশ — সাধারণ Ethernet bridging-এর জন্য, physical NIC একত্র করার জন্য (যেমন একটা হোস্টে একাধিক NIC থাকলে একটা bridge network তৈরি করা, বা VM networking-এ, hardware-virtualization লেসনের tap device আলোচনার সাথে সম্পর্কিত)। Docker শুধু এই বিদ্যমান, decades-old primitive-টা পুনরায় ব্যবহার করেছে container networking-এর জন্য — নতুন কিছু আবিষ্কার করেনি। এই কারণেই networking/switching-arp লেসনের পুরো MAC learning মডেলটা এখানে কোনো পরিবর্তন ছাড়াই প্রযোজ্য — এটা আসলেই একই কোড।

“CNI একটা নির্দিষ্ট networking implementation বা library — যেমন Flannel বা Calico-এর মতোই একটা product।”

না — CNI নিজে কোনো networking বাস্তবায়ন করে না, এটা শুধু একটা interface/contract (JSON config stdin-এ, environment variable-এ parameter, JSON result stdout-এ)। Flannel, Calico, Cilium — এরা প্রত্যেকে CNI-এর একটা বাস্তবায়ন (plugin), যেমন একটা HTTP handler কোনো নির্দিষ্ট web framework না, বরং সেই framework-এর একটা interface মেনে লেখা কোড। CNI-এর মূল্য ঠিক এইখানেই — একটা runtime (Kubernetes-সহ) এই সব ভিন্ন বাস্তবায়নের মধ্যে কোনো কোড-পরিবর্তন ছাড়াই সুইচ করতে পারে, কারণ সবাই একই contract মেনে চলে।

বুঝেছেন কি না দেখুন

1

veth pair কেন সবসময় জোড়ায় তৈরি হয়? একটা veth pair-এর দুই প্রান্ত সাধারণত কোন দুই namespace-এ থাকে, container networking-এর প্রেক্ষাপটে?

স্মরণ

veth device driver নিজেই একটা connected pair হিসেবে ডিজাইন করা — এক প্রান্তে transmit করা frame সরাসরি, তাৎক্ষণিকভাবে অন্য প্রান্তে receive হিসেবে হাজির হয় (in-kernel function call, কোনো physical medium না), তাই একা একটা veth ইন্টারফেসের কোনো অর্থ নেই। container networking-এ এক প্রান্ত container-এর network namespace-এ থাকে (সেখানে eth0 নামে rename করা হয়), আর অন্য প্রান্ত host-এর (root) network namespace-এ থাকে (veth<hex>-জাতীয় নামে), যেটা bridge-এ সংযুক্ত হয়।

2

Linux bridge-এর MAC learning table-এ একটা container-এর MAC ঢুকতে হলে ঠিক কী ঘটতে হবে? একটা container যদি একটা long-running সময় ধরে শুধু অন্যদের কাছ থেকে data receive করে, কখনো নিজে থেকে কিছু না পাঠায়, তার MAC কি bridge-এর টেবিলে থাকবে?

যুক্তি

না, থাকবে না। networking/switching-arp লেসনের মূল নিয়মটাই এখানে প্রযোজ্য — bridge শুধু source MAC দেখে শেখে (একটা frame কোনো port থেকে ঢুকলে সেই port-কে সেই source MAC-এর সাথে যুক্ত করে), destination দেখে না। একটা container যদি কখনো নিজে থেকে কোনো frame transmit না করে (শুধু receive করে), bridge কখনো তার arrival port জানতেই পারবে না — তার MAC কখনো FDB-তে ঢুকবে না, আর তার দিকে যাওয়া প্রতিটা frame চিরকাল flood হতেই থাকবে, কোনো destination filtering ছাড়াই। এই লেসনের build সেকশনের simulation-এ ঠিক এই আচরণটাই বাস্তব output-এ দেখা গেছে।

3

একটা container (172.17.0.5) 8.8.8.8:53-এ একটা DNS query পাঠাচ্ছে। host-এর real IP 198.51.100.20। ধাপে ধাপে বলুন — container-এর ভেতর থেকে dst-MAC কী হবে, আর iptables MASQUERADE rule-এর পর internet কী src IP/port দেখবে।

প্রয়োগ

8.8.8.8 container-এর subnet-এর (172.17.0.0/16) বাইরে, তাই container-এর routing logic dst-MAC হিসেবে তার default gateway-র (bridge, docker0) MAC ব্যবহার করবে — সরাসরি 8.8.8.8-এর MAC না, কারণ container সেটা জানতেও পারে না (ভিন্ন subnet, ARP resolve সম্ভবই না)। bridge frame পেয়ে দেখবে dst-IP তার subnet-এ না, তাই host-এর routing stack-এ প্যাকেট উঠে যাবে। iptables POSTROUTING-এর MASQUERADE rule মিলবে, আর container-এর source (172.17.0.5:<ephemeral>) rewrite হয়ে host-এর real IP আর একটা নতুন ephemeral port হয়ে যাবে — internet (এখানে 8.8.8.8) দেখবে শুধু 198.51.100.20:<host-এর ephemeral port>, container-এর private IP কখনোই না।

4

CNI plugin কীভাবে জানে কোন network namespace-এ কাজ করতে হবে, আর সেই namespace-এর ভেতরে ইন্টারফেসের নাম কী রাখতে হবে? এই তথ্যগুলো stdin-এর JSON config-এ থাকে, নাকি অন্য কোথাও?

যুক্তি

অন্য কোথাও — environment variable-এ, JSON config-এ না। CNI_NETNS environment variable container-এর network namespace-এর filesystem path বহন করে (যেমন /var/run/netns/<container-id>), আর CNI_IFNAME environment variable বলে দেয় namespace-এর ভেতরে ইন্টারফেসের নাম কী হওয়া উচিত (প্রায় সবসময় eth0)। stdin-এর JSON config শুধু network-এর ধরন (bridge/overlay/ইত্যাদি), subnet, IPAM সেটিংস-এর মতো network-নির্দিষ্ট কনফিগারেশন বহন করে — কোন নির্দিষ্ট container এই মুহূর্তে setup হচ্ছে, সেই instance-নির্দিষ্ট তথ্য (namespace path, container id, ইন্টারফেসের নাম) সবসময় environment variable দিয়ে পাস করা হয়। এই বিভাজনটাই CNI-এর design-এর অংশ — config reusable, parameter প্রতিটা call-এ ভিন্ন।

5

একজন সহকর্মী দাবি করছেন docker run -p 8080:80 আর এই লেসনের MASQUERADE rule আসলে একই জিনিসের দুইটা নাম, শুধু direction ভিন্ন বলার দরকার নেই। তিনি ঠিক না ভুল? দুটোর মধ্যে iptables chain-এর পার্থক্য কী?

প্রয়োগ

আংশিক ঠিক (একই পরিবারের কৌশল — address/port rewrite, conntrack ব্যবহার) কিন্তু “direction ভিন্ন বলার দরকার নেই” অংশে ভুল — এই পার্থক্যটাই ব্যবহারিকভাবে গুরুত্বপূর্ণ। এই লেসনের MASQUERADE rule POSTROUTING chain-এ বসে, outbound traffic-এর জন্য (container বাইরে কিছুর সাথে connection শুরু করছে) — source rewrite করে। docker run -p 8080:80 একটা DNAT rule PREROUTING chain-এ বসায়, inbound traffic-এর জন্য (বাইরের কেউ host-এর 8080-এ connect করছে) — destination rewrite করে, host-এর 8080 কে container-এর 172.17.0.x:80-এ পাল্টে দেয়। দুটোই “address translation”, কিন্তু chain ভিন্ন (POSTROUTING বনাম PREROUTING), rewrite-এর দিক ভিন্ন (source বনাম destination), আর trigger করার শর্ত ভিন্ন (কে connection শুরু করছে তার উপর ভিত্তি করে)।

6

আপনাকে একটা ছোট, single-host container platform ডিজাইন করতে বলা হয়েছে (Docker-এর মতো, কিন্তু নিজের)। আপনি networking-এর জন্য দুইটা option-এর মধ্যে বেছে নিচ্ছেন: (ক) veth/bridge/iptables-এর কোড সরাসরি নিজের runtime-এর ভেতরে hardcode করা, নাকি (খ) CNI-এর মতো একটা plugin interface ডিজাইন করে networking logic আলাদা executable-এ রাখা। কোনটা বেছে নেবেন, আর কেন — বিশেষভাবে ভবিষ্যতের কোন চাহিদাগুলো এই সিদ্ধান্তকে প্রভাবিত করে?

ডিজাইন

যদি platform-টা শুধুই single-host, আর ভবিষ্যতে কখনো ভিন্ন networking approach (overlay, BGP-routing, eBPF) সমর্থনের প্রয়োজন না থাকে, hardcode করা (ক) সহজ এবং যুক্তিসঙ্গত — কম moving parts, debug করা সহজ, কোনো external process-spawn overhead নেই। কিন্তু বাস্তবে বেশিরভাগ container platform শেষ পর্যন্ত এমন চাহিদার মুখোমুখি হয় যা প্রথম দিনে predict করা যায়নি — multi-host cluster support (Kubernetes-এর মতো, যেখানে single-host bridge মডেল আর যথেষ্ট না, kubernetes-architecture লেসনের বিষয়), অথবা নির্দিষ্ট গ্রাহকের বিশেষ networking প্রয়োজন (একটা customer BGP চায়, আরেকটা compliance কারণে network isolation-এর ভিন্ন মডেল চায়)। CNI-এর মতো একটা plugin interface (খ) ঠিক এই ধরনের অননুমেয় ভবিষ্যৎ চাহিদার বিরুদ্ধে বীমা — নতুন networking approach মানে শুধু একটা নতুন plugin লেখা, runtime-এর core কোড স্পর্শ না করেই। ট্রেড-অফ: এখনই একটু বেশি জটিলতা (stdin/env-var/JSON contract ডিজাইন ও maintain করা) বিনিময়ে ভবিষ্যতের নমনীয়তা। একটা প্রকৃত প্রোডাক্ট-মানের সিদ্ধান্তে, বিশেষত যদি multi-tenant বা multi-host সম্প্রসারণের সামান্যতম সম্ভাবনাও থাকে, (খ) সাধারণত সঠিক পছন্দ — এটাই আসলে Docker নিজেও পরবর্তীতে করেছিল (libnetwork/CNM), আর Kubernetes শুরু থেকেই CNI বেছে নিয়েছিল।

এরপর কী

এরপর কী

এখন আমাদের কাছে একটা single Docker host-এর যা যা দরকার তার সবটাই আছে: container-runtime-oci লেসনের images ও layered filesystem, linux-namespaces-deep-dive-এর namespace/cgroup wiring, আর এই লেসনের networking — veth দিয়ে namespace-কে host-এর সাথে, bridge দিয়ে container-দের একে অপরের সাথে, NAT দিয়ে internet-এর সাথে সংযুক্ত করা।

কিন্তু বাস্তব production সিস্টেম একটা মেশিনে চলে না — শত শত container চলে অনেকগুলো মেশিন জুড়ে। আর তখন নতুন প্রশ্নগুলো আসে, যেগুলোর একটারও উত্তর এই একক-host মডেলে নেই: কোন container ঠিক কোন মেশিনে চলবে সেই সিদ্ধান্ত কে নেবে? একটা container crash করলে কে সেটা restart করবে? একটা mesh-এর মধ্যে একটা container অন্য মেশিনে চলা আরেকটা container-কে কীভাবে খুঁজে পাবে, যখন সেই দ্বিতীয় container যেকোনো মুহূর্তে move করতে পারে, নতুন IP পেতে পারে?

এই প্রশ্নগুলোর উত্তরের নাম Kubernetes — পরের লেসন, virtualization-cloud/kubernetes-architecture। আর সেই লেসনের networking অংশটা এই লেসনের উপরেই সরাসরি দাঁড়াবে: Kubernetes নিজে veth/bridge/NAT পুনরাবিষ্কার করে না — এটা এই লেসনের শেখা CNI contract-এর মাধ্যমে ঠিক এই primitive-গুলোই ব্যবহার করে, শুধু multi-host স্কেলে প্রসারিত করে।

আরও পড়ুন

  • veth(4) — Virtual Ethernet Device Pairs · veth pair-এর প্রামাণ্য kernel ডকুমেন্টেশন — দুই প্রান্তের semantics, namespace সরানোর নিয়ম
  • Container Network Interface (CNI) Specification · ADD/DEL/CHECK/VERSION command, stdin JSON config, env var parameter, JSON result — পুরো contract-এর প্রামাণ্য উৎস
  • Cluster Networking — Kubernetes Documentation · Kubernetes-এর 'প্রতিটা Pod-এর নিজস্ব IP' মডেল, আর CNI plugin-গুলো (Calico, Flannel, Cilium) এই মডেল কীভাবে বাস্তবায়ন করে তার আনুষ্ঠানিক আলোচনা
  • bridge(8) — show / manipulate bridge addresses and devices · Linux bridge FDB (forwarding database) পরিদর্শন ও পরিবর্তনের কমান্ড-লাইন টুল — `bridge fdb show` দিয়ে MAC learning table সরাসরি দেখা যায়