iT邦幫忙

2022 iThome 鐵人賽

2
DevOps

那些關於 docker 你知道與不知道的事系列 第 31

Day 31: Docker 是怎麼解封國境的呢?

  • 分享至 

  • xImage
  •  

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,沒問題,是可以開啟的:
https://ithelp.ithome.com.tw/upload/images/20221017/201518576hhSpsMDXQ.png

如果我們進入 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,藉此來觀察封包的轉變:

  1. tcpdump for ens5: 這裡沒什麼問題
    https://ithelp.ithome.com.tw/upload/images/20221017/20151857VJPeaAAIcy.png

  2. tcpdump for docker0: 這邊可以清楚地觀察到,到 docker0時,dst IP 已經從 host 的 172.31.59.121 變成 container 裡的 172.17.0.2,除了 IP 之外,可以看到 port 也變了,是誰做了這些轉換呢?
    https://ithelp.ithome.com.tw/upload/images/20221017/20151857XuMc4X1uNy.png

我想大家可能有推測到,也許會跟 iptables 有關?讓我們來看看,在啟動 nginx 這個 container 且有加上 -p,我們 iptables 的規則有沒有什麼變化:

首先我檢查了預設的 filter table,看起來沒有什麼變化,接著,我再來看看 nat table,果然發現了可疑的線索:
https://ithelp.ithome.com.tw/upload/images/20221018/201518577o6VKMgHA2.png

可以把目前啟動的 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 上有一張不錯的圖:
https://ithelp.ithome.com.tw/upload/images/20221018/20151857y7P18M11Z0.png

還蠻清楚的對吧,當封包進來時,我們決定這個封包要往哪裡走之前,先經過 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 嗎?)


上一篇
Day 30: 讓我們關掉無條件轉發吧 & 完賽感想
下一篇
Day 32: 換我們的 namespace 解封國境
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言