Day 29的最後我們提到,如果想在我們的 container (net namespace)中啟動一個伺服器,讓其他人來存取的話是可以的嗎?
我先在我的實驗環境: Ubuntu 20.04 的 host 上安裝了 Nodejs 16,並且簡單地寫了一個 web server,這邊大家可以任意使用熟悉的語言或工具去啟動一個 web server:
const http = require('http');
const SERVER_PORT = 3000;
const server = http.createServer((req, res) => {
res.end('Hello container!');
});
server.listen(SERVER_PORT, '0.0.0.0', () => {
console.log(`start to listen ${SERVER_PORT}`);
});
我先在 host 啟動這個小 server:
ubuntu@ip-xxx:~/websrv$ node server.js
start to listen 3000
用 netstat
觀察,host 有在 listen 3000 port:
ubuntu@ip-xxx:~$ netstat -ano | grep :3000
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN off (0.00/0/0)
接著用瀏覽器開啟這台 EC2 的 http://[public IP]:3000
,沒問題,是可以開啟的:
如果我們進入 ns1 後再啟動呢?讓我們關掉 host 上的 web server,然後進入 ns1 後啟動 web server:
ubuntu@ip-xxx:~/websrv$ sudo ip netns exec ns1 bash
root@ip-xxx:/home/ubuntu/websrv# node server.js
start to listen 3000
會發現瀏覽器無法開啟網頁了,且在 host 用 netstat
查看,會發現 host 並沒有在聽 3000 port 的資料。但如果是在 ns1 中用 netstat
觀察,則會發現是有在對 3000 port listen 的:
root@ip-xxx:/home/ubuntu/websrv# netstat -ano | grep :3000
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN off (0.00/0/0)
到目前為止還蠻合理的,不同的 net namespace 嘛,隔離開來了。那所以,Docker 是怎麼做到的呢?
關於這個問題,如果有用過 Docker,我想第一個直覺的反應就是在 docker run 的時候要加上 -p
,把 port publish 出來,例如:
$ docker run -d --rm -p 8080:80 nginx:alpine
nginx:alpine
這個 image 裡會啟動一個 listen 80 port 的 nginx server,在 docker run 時透過 -p
把 container 的 80 port 綁定到 host 的 8080 port,之後我們就可以透過 host 的 public IP:8080 來連線這個位於 container 內的 nginx web server 了,那這個 -p
指令又是做了什麼事呢?
在執行完上述指令後,我們先來 host 用 netstat
觀察一下:
ubuntu@ip-xxx:~$ netstat -ano | grep :8080
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN off (0.00/0/0)
tcp6 0 0 :::8080 :::* LISTEN off (0.00/0/0)
host 有在聽 8080 port 喔,那 host 從 8080 port 接收到資訊後,封包是怎麼跑的呢?一樣,讓我們透過 tcpdump
來錄錄看封包,我會分別對 ens5
(host 原本的網路介面) 及 docker0
(Docker 安裝完成後,預設的 bridge 介面)錄,而且會對 ens5
過濾 8080 port,對 docker0
同時過濾 80 及 8080,藉此來觀察封包的轉變:
tcpdump for ens5
: 這裡沒什麼問題
tcpdump for docker0
: 這邊可以清楚地觀察到,到 docker0
時,dst IP 已經從 host 的 172.31.59.121
變成 container 裡的 172.17.0.2
,除了 IP 之外,可以看到 port 也變了,是誰做了這些轉換呢?
我想大家可能有推測到,也許會跟 iptables 有關?讓我們來看看,在啟動 nginx 這個 container 且有加上 -p
,我們 iptables 的規則有沒有什麼變化:
首先我檢查了預設的 filter table,看起來沒有什麼變化,接著,我再來看看 nat table,果然發現了可疑的線索:
可以把目前啟動的 nginx container 關掉看看,當這個 container stop
之後,上圖標示出來的那兩條規則也會隨之移除。
先注意一下,最下方的 DOCKER chain 會在 PREROUTING chain 裡被引用到:
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 10886 512K DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
這邊簡單地解釋一下:就是符合條件(match) 的,在 NAT 的 Prerouting 時,「跳」(jump) 到 DOCKER 這個 chain 去,那符合什麼條件呢?這邊會用 iptables
的一個擴展模組 ADDRTYPE
,我們根據這份文件 可以查到 addrtype 可以根據封包的 address type 來做比對,參數 --dst-type
就是要比對的是 destination address,至於 LOCAL
文件中就是只有說 local address
,那整條規則翻譯起來就是「destination 符合 local address 的,就跳到 DOCKER 這個 chain 去」。
既然已經跳到 DOCKER chain 了,那我們就來看一下 DOCKER chain 上新增的這條:
num pkts bytes target prot opt in out source destination
...略
2 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
這條規則解釋起來大概會是:只要不是從 docker0
這個介面進來的,不管出去的介面(out),也不管來源或目的位址,只要是 tcp protocol,且目標 port 是 8080,那就跳到 DNAT
去。DNAT
是什麼呢?這也是一個 iptables
的擴充模組,一樣可以在這份文件裡找到說明,根據這份文件,DNAT
只能在 nat table 的 PREROUTING、OUTPUT chain 或使用者自己建立的 chain 使用,他可以用來修改目的地位址 (destination address),他的參數 --to-destination
就是用來設定新的 destination IP 跟 port 的。
如果用 list-rules 來觀察會是這樣,自己覺得這樣看起來更容易理解一些,你們覺得呢?
ubuntu@ip-xxx:~$ sudo iptables -t nat --list-rules
...略
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
...略
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
在 list-rules 中還有一條我們沒有解釋到,那就是 OUTPUT 裡的這條規則:
Chain OUTPUT (policy ACCEPT 4 packets, 333 bytes)
num pkts bytes target prot opt in out source destination
1 3 180 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
這邊我們再補充一下,我們看了好幾次 iptables 了,但好像一直沒有解釋 PREROUTING, INPUT, OUTPUT..這些,Red Hat 上有一張不錯的圖:
還蠻清楚的對吧,當封包進來時,我們決定這個封包要往哪裡走之前,先經過 pre-routing,再來就判斷是要自己收下來,還是轉送,如果是自己收下來,就進到 input,處理完、由本機出去的時候就走 output,如果是轉送,就進入 forward,不管是收下處理還是轉送,最後都要過 post-routing。
Red Hat 的這份文件在往上看一點,找到 nat 這個區塊,可以看到他對 OUTPUT 的解釋:
OUTPUT — Applies to locally-generated network packets before they are sent out.
也就是說本地產生的網路封包送出去之前,會套用此規則。
這條規則就先到這邊,大家先放在心裡一下,等明天我們來做個實驗,等觀察完實驗的結果後,我們再來回頭看這條規則!(欸,所以是會有 Day 32 嗎?)