網絡的高級知識,包括網絡的啟動和配置參數、DNS的使用配置、容器訪問和端口映射的相關實現。
在一些具體場景中,Docker支持的網絡定制配置,通過Linux命令來調整、補充、甚至替換Docker默認的網絡配置。
網絡啟動與配置參數
Docker啟動時會在主機上自動創建一個docker0虛擬網橋,實際上是一個Linux網橋,可以理解為一個軟件交換機,它會在掛載其上的接口之間進行轉發。
同時,Docker隨機分配一個本地未占用的私有網段(在RFC1918中定義)中的一個地址給docker0接口。比如典型的172.17.42.1,掩碼為255.255.0.0。此后啟動的容器內的網口也會自動分配一個同一網段(172.17.0.0/16)的地址。
當創建一個Docker容器的時候,同時會創建了一對veth pair接口(當數據包發送到一個接口時,另外一個接口也可以收到相同的數據包)。這對接口一端在容器內,即eth0;另一端在本地並被掛載到docker0網橋,名稱以veth開頭(例如vethAQI2QT)。通過這種方式,主機可以跟容器通信,容器之間也可以相互通信。如此一來,Docker就創建了在主機和所有容器之間一個虛擬共享網絡。
下面是跟Docker網絡相關的命令參數。其中有些命令選項只有在Docker服務啟動的時候才能配置,而且不能馬上生效:
- -b BRIDGE or--bridge=BRIDGE——指定容器掛載的網橋;
- --bip=CIDR——定制docker0的掩碼;
- -H SOCKET...or--host=SOCKET...——Docker服務端接收命令的通道;
- --icc=true|false——是否支持容器之間進行通信;
- --ip-forward=true|false——啟用net.ipv4.ip_forward,即打開轉發功能;
- --iptables=true|false——禁止Docker添加iptables規則;
- --mtu=BYTES——容器網絡中的MTU。
下面2個命令選項既可以在啟動服務時指定,也可以Docker容器啟動(docker run)時候指定。在Docker服務啟動的時候指定則會成為默認值,后續執行docker run時可以覆蓋設置的默認值。
- --dns=IP_ADDRESS...——使用指定的DNS服務器;
- --dns-search=DOMAIN...——指定DNS搜索域。
最后這些選項只能在docker run執行時使用,因為它是針對容器的特性內容:
- -h HOSTNAME or--hostname=HOSTNAME——配置容器主機名;
- --link=CONTAINER_NAME:ALIAS——添加到另一個容器的連接;
- --net=bridge|none|container:NAME_or_ID|host|user_defined_network——配置容器的橋接模式;
- -p SPEC or--publish=SPEC——映射容器端口到宿主主機;
- -P or--publish-all=true|false——映射容器所有端口到宿主主機。
其中,--net選項支持五種模式,如下所示:
- --net=bridge——默認配置。為容器創建獨立的網絡命名空間,分配網卡、IP地址等網絡配置,並通過veth接口對將容器掛載到一個虛擬網橋(默認為docker0)上;
- --net=none——為容器創建獨立的網絡命名空間,但不進行網絡配置,即容器內沒有創建網卡、IP地址等;
- --net=container:NAME_or_ID——意味着新創建的容器共享指定的已存在容器的網絡命名空間,兩個容器內的網絡配置共享,但其他資源(進程空間、文件系統等)還是相互隔離的;
- --net=host——意味着不為容器創建獨立的網絡命名空間,容器內看到的網絡配置(網卡信息、路由表、Iptables規則等)均與主機上保持一致。注意其他資源還是與主機隔離的;
- --net=user_defined_network——用戶自行用network相關命令創建一個網絡,同一個網絡內的容器彼此可見,可以采用更多類型的網絡插件。
配置容器DNS和主機名
Docker支持自定義容器的主機名和DNS配置。
1.相關配置文件
實際上,容器中主機名和DNS配置信息都是通過三個系統配置文件來維護的:/etc/resolv.conf、/etc/hostname和/etc/hosts。
啟動一個容器,在容器中使用mount命令可以看到這三個文件掛載信息:
$ docker run -it ubuntu
root@75dbd6685305:/# mount
/etc/resolv.conf文件在創建容器時候,默認會與宿主機/etc/resolv.conf文件內容保持一致:
root@75dbd6685305:/# cat /etc/resolv.conf
/etc/hosts文件中默認只記錄了容器自身的一些地址和名稱:
root@75dbd6685305:/# cat /etc/hosts
/etc/hostname文件則記錄了容器的主機名。
root@75dbd6685305:/# cat /etc/hostname
2.容器內修改配置文件
Docker 1.2.0開始支持在運行中的容器里直接編輯/etc/hosts,/etc/hostname和/etc/resolve.conf文件。
但是這些修改是臨時的,只在運行的容器中保留,容器終止或重啟后並不會被保存下來。也不會被docker commit提交。
3.通過參數指定
如果用戶想要自定義容器的配置,可以在創建或啟動容器時利用下面的參數指定:
- 指定主機名-h HOSTNAME或者--hostname=HOSTNAME。設定容器的主機名,它會被寫到容器內的/etc/hostname和/etc/hosts。但這個主機名只有容器內能看到,在容器外部則看不到,既不會在docker ps中顯示,也不會在其他的容器的/etc/hosts看到。
- 記錄其他容器主機名--link=CONTAINER_NAME:ALIAS。選項會在創建容器的時候,添加一個所連接容器的主機名到容器內/etc/hosts文件中。這樣,新創建容器可以直接使用主機名來與所連接容器通信。
- 指定DNS服務器--dns=IP_ADDRESS。添加DNS服務器到容器的/etc/resolv.conf中,容器會用指定的服務器來解析所有不在/etc/hosts中的主機名。
- 指定DNS搜索域--dns-search=DOMAIN。設定容器的搜索域,當設定搜索域為.example.com時,在搜索一個名為host的主機時,DNS不僅搜索host,還會搜索host.example.com。
容器訪問控制
容器的訪問控制主要通過Linux上的iptables防火牆軟件來進行管理和實現。iptables是Linux系統流行的防火牆軟件,在大部分發行版中都自帶。
1.容器訪問外部網絡
我們知道容器默認指定了網關為docker0網橋上的docker0內部接口。docker0內部接口同時也是宿主機的一個本地接口。因此,容器默認情況下是可以訪問到宿主機本地的。更進一步,容器要想通過宿主機訪問到外部網絡,需要宿主機進行轉發。
在宿主機Linux系統中,檢查轉發是否打開:
$ sudo sysctl net.ipv4.ip_forward
如果為0,說明沒有開啟轉發,則需要手動打開:
$ sudo sysctl -w net.ipv4.ip_forward=1
更簡單的,在啟動Docker服務的時候設定--ip-forward=true,Docker服務會自動打開宿主機系統的轉發服務。
2.容器之間的訪問
容器之間相互訪問,需要兩方面的支持:
- 網絡拓撲是否已經連通。默認情況下,所有容器都會連接到docker0網橋上,這意味着默認情況下拓撲是互通的;
- 本地系統的防火牆軟件iptables是否允許訪問通過。這取決於防火牆的默認規則是允許(大部分情況)還是禁止。
下面分兩種情況介紹容器之間的訪問。
(1)訪問所有端口
當啟動Docker服務時候,默認會添加一條“允許”轉發策略到iptables的FORWARD鏈上。通過配置--icc=true|false(默認值為true)參數可以控制默認的策略。
為了安全考慮,可以在Docker配置文件中配置DOCKER_OPTS=--icc=false來默認禁止容器之間的相互訪問。
同時,如果啟動Docker服務時手動指定--iptables=false參數則不會修改宿主機系統上的iptables規則。
(2)訪問指定端口
在通過-icc=false禁止容器間相互訪問后,仍可以通過--link=CONTAINER_NAME:ALIAS選項來允許訪問指定容器的開放端口。
例如,在啟動Docker服務時,可以同時使用icc=false--iptables=true參數來配置容器間禁止訪問,並允許Docker自動修改系統中的iptables規則。
此時,系統中的iptables規則可能是類似如下規則,禁止所有轉發流量:
$ sudo iptables -nL
... Chain FORWARD (policy ACCEPT) target prot opt source destination DROP all -- 0.0.0.0/0 0.0.0.0/0 ...
之后,啟動容器(docker run)時使用--link=CONTAINER_NAME:ALIAS選項。Docker會在iptable中為兩個互聯容器分別添加一條ACCEPT規則,允許相互訪問開放的端口(取決於Dockerfile中的EXPOSE行)。
此時,iptables的規則可能是類似如下規則:
$ sudo iptables -nL
... Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80 ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80 DROP all -- 0.0.0.0/0 0.0.0.0/0
--link=CONTAINER_NAME:ALIAS中的CONTAINER_NAME目前必須是Docker自動分配的容器名,或使用--name參數指定的名字。不能為容器-h參數配置的主機名。
映射容器端口到宿主主機的實現
默認情況下,容器可以主動訪問到外部網絡的連接,但是外部網絡無法訪問到容器。
1.容器訪問外部實現
假設容器內部的網絡地址為172.17.0.2,本地網絡地址為10.0.2.2。容器要能訪問外部網絡,源地址不能為172.17.0.2,需要進行源地址映射(Source NAT,SNAT),修改為本地系統的IP地址10.0.2.2。映射是通過iptables的源地址偽裝操作實現的。
查看主機nat表上POSTROUTING鏈的規則。該鏈負責網包要離開主機前,改寫其源地址。
$ sudo iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 12 packets, 738 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 ...
其中,上述規則將所有源地址在172.17.0.0/16網段,且不是從docker0接口發出的流量(即從容器中出來的流量),動態偽裝為從系統網卡發出。MASQUERADE行動跟傳統SNAT行動相比,好處是它能從網卡動態獲取地址。
2.外部訪問容器實現
容器允許外部訪問,可以在docker run時候通過-p或-P參數來啟用。
不管用那種辦法,其實也是在本地的iptable的nat表中添加相應的規則,將訪問外部IP地址的網包進行目標地址DNAT,將目標地址修改為容器的IP地址。
以一個開放80端口的Web容器為例,使用-P時,會自動映射本地49000~49900范文內的端口隨機端口到容器的80端口:
$ iptables -t nat -nvL
... Chain PREROUTING (policy ACCEPT 236 packets, 33317 bytes) pkts bytes target prot opt in out source destination 567 30236 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80 ...
可以看到,nat表中涉及兩條鏈,PREROUTING鏈負責包到達網絡接口時,改寫其目的地址。其中規則將所有流量都扔到DOCKER鏈。而DOCKER鏈中將所有不是從docker0進來的網包(意味着不是本地主機產生),將目標端口為49153的,修改目標地址為172.17.0.2,目標端口修改為80。
使用-p 80:80時,與上面類似,只是本地端口也為80:
$ iptables -t nat -nvL
... Chain PREROUTING (policy ACCEPT 236 packets, 33317 bytes) pkts bytes target prot opt in out source destination 567 30236 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 ...
有兩點需要注意:
- 這里的規則映射了0.0.0.0,意味着將接受主機來自所有網絡接口上的流量。用戶可以通過-p IP:host_port:container_port或-p IP::port來指定綁定的外部網絡接口,以制定更嚴格的訪問規則;
- 如果希望映射永久綁定到某個固定的IP地址,可以在Docker配置文件/etc/default/docker中指定DOCKER_OPTS="--ip=IP_ADDRESS",之后重啟Docker服務即可生效。
配置docker0網橋
Docker服務默認會創建一個名稱為docker0的Linux網橋(其上有一個docker0內部接口),它在內核層連通了其他的物理或虛擬網卡,這就將所有容器和本地主機都放到同一個物理網絡。用戶使用Docker創建多個自定義網絡時可能會出現多個容器網橋。
Docker默認指定了docker0接口的IP地址和子網掩碼,讓主機和容器之間可以通過網橋相互通信,它還給出了MTU(接口允許接收的最大傳輸單元),通常是1500字節,或宿主主機網絡路由上支持的默認值。這些值都可以在服務啟動的時候進行配置:
- --bip=CIDR——IP地址加掩碼格式,例如192.168.1.5/24;
- --mtu=BYTES——覆蓋默認的Docker mtu配置。
也可以在配置文件中配置DOCKER_OPTS,然后重啟服務。
由於目前Docker網橋是Linux網橋,用戶可以使用brctl show來查看網橋和端口連接信息:
$ sudo brctl show
bridge name bridge id STP enabled interfaces docker0 8000.3a1d7362b4ee no veth65f9 vethdda6
brctl命令如果系統中沒有自帶,可以使用sudo apt-get install bridge-utils來安裝(Debian、Ubuntu系列系統)。
每次創建一個新容器的時候,Docker從可用的地址段中選擇一個空閑的IP地址分配給容器的eth0端口。並且使用本地主機上docker0接口的IP作為容器的默認網關:
$ docker run -i -t --rm base /bin/bash
$ ip addr show eth0
24: eth0: <broadcast,up,lower_up>mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 32:6f:e0:35:57:91 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::306f:e0ff:fe35:5791/64 scope link valid_lft forever preferred_lft forever
$ ip route
default via 172.17.42.1 dev eth0 172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.3
$ exit
目前,Docker不支持在啟動容器時候指定IP地址。實際上,Linux網橋自身功能已經十分完備,也可以替換為OpenvSwitch等功能更強大的網橋實現。
自定義網橋
除了默認的docker0網橋,用戶也可以指定網橋來連接各個容器。
在啟動Docker服務的時候,使用-b BRIDGE或--bridge=BRIDGE來指定使用的網橋。
如果服務已經運行,那需要先停止服務,並刪除舊的網橋:
$ sudo service docker stop
$ sudo ip link set dev docker0 down
$ sudo brctl delbr docker0
然后創建一個網橋bridge0:
$ sudo brctl addbr bridge0
$ sudo ip addr add 192.168.5.1/24 dev bridge0
$ sudo ip link set dev bridge0 up
查看確認網橋創建並啟動:
$ ip addr show bridge0
4: bridge0: <broadcast,multicast>mtu 1500 qdisc noop state UP group default link/ether 66:38:d0:0d:76:18 brd ff:ff:ff:ff:ff:ff inet 192.168.5.1/24 scope global bridge0 valid_lft forever preferred_lft forever
配置Docker服務,默認橋接到創建的網橋上:
$ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker
$ sudo service docker start
啟動Docker服務。新建一個容器,可以看到它已經橋接到了bridge0上。
可以繼續用brctl show命令查看橋接的信息。另外,在容器中可以使用ip addr和ip route命令來查看IP地址配置和路由信息。
使用OpenvSwitch網橋
Docker默認使用的是Linux自帶的網橋實現,實際上,OpenvSwitch項目作為一個成熟的虛擬交換機實現,具備更豐富的功能。將來會有越來越多的容器支持OpenvSwitch作為底層網橋實現。
1.環境
在Ubuntu 14.04系統中進行測試。操作流程也適用於RedHat/CentOS系列系統,但少數命令和配置文件可能略有差異。
2.安裝Docker
安裝最近版本的Docker並啟動服務。默認情況下,Docker服務會創建一個名為docker0的Linux網橋,作為連接容器的本地網橋。
可以通過如下命令查看:
$ sudo brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.000000000000 no
網橋docker0內部接口的默認地址可能為172.17.42.1。
$ ifconfig docker0
docker0 Link encap:Ethernet HWaddr 56:84:7a:fe:97:99 inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0 BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
3.安裝OpenvSwitch
通過如下命令安裝OpenvSwitch:
$ sudo aptitude install openvswitch-switch
測試添加一個網橋br0並查看:
$ sudo ovs-vsctl add-br br0
$ sudo ovs-vsctl show
4.配置容器連接到OpenvSwitch網橋
目前OpenvSwitch網橋還不能直接支持掛載容器,需要手動在OpenvSwitch網橋上創建虛擬網口並掛載到容器中。
(1)創建無網口容器
啟動一個ubuntu容器,並指定不創建網絡,后面我們手動添加網絡。較新版本的Docker默認不允許在容器內修改網絡配置,需要在run的時候指定參數--privileged=true:
$ docker run --net=none --privileged=true -it ubuntu:14.04 bash
root@298bbb17c244:/#
記住這里容器的id為298bbb17c244。
此時在容器內查看網絡信息,只能看到一個本地網卡lo:
root@298bbb17c244:/# ifconfig
lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
(2)手動為容器添加網絡
下載OpenvSwitch項目提供的支持Docker容器的輔助腳本ovs-docker:
$ wget https://github.com/openvswitch/ovs/raw/master/utilities/ovs-docker
$ sudo chmod a+x ovs-docker
為容器添加網卡,並掛載到br0上,命令為:
$ sudo ./ovs-docker add-port br0 eth0 298bbb17c244 --ipaddress=172.17.0.2/16
添加成功后,在容器內查看網絡信息,多了一個新添加的網卡eth0,對應添加的IP地址:
root@298bbb17c244:/# ifconfig
eth0 Link encap:Ethernet HWaddr ae:3d:75:2c:18:ba inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0 inet6 addr: fe80::ac3d:75ff:fe2c:18ba/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:187 errors:0 dropped:2 overruns:0 frame:0 TX packets:11 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:33840 (33.8 KB) TX bytes:1170 (1.1 KB) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
在容器外,配置OpenvSwitch的網橋br0內部接口地址為172.17.42.2/16(只要與所掛載容器IP在同一個子網內即可):
$ sudo ifconfig br0 172.17.42.2/16
(3)測試連通
經過上面步驟,容器已經連接到了網橋br0上了,拓撲如下所示:
容器(172.17.0.2/16)<-->br0
網橋<-->br0
內部端口(172.17.42.2/16)
此時,在容器內就可以測試是否連通到網橋br0上了:
root@298bbb17c244:/# ping 172.17.42.2
在容器內也可以配置默認網關為br0接口地址:
root@298bbb17c244:/# route add default gw 172.17.42.2
另外,刪除該接口的命令為:
$ sudo ./ovs-docker del-port br0 eth0<container_id>
實際上,Docker社區也已經討論對OpenvSwitch進行原生支持了。在Docker原生支持OpenvSwitch之前,用戶可以通過編寫腳本或更高級的工具來讓這一過程自動化。
創建一個點到點連接
默認情況下,Docker會將所有容器連接到由docker0提供的虛擬子網中。用戶有時候需要兩個容器之間可以直連通信,而不用通過主機網橋進行橋接。
解決辦法很簡單:創建一對peer接口,分別放到兩個容器中,配置成點到點鏈路類型即可。
下面這個過程我們將手動執行Docker配置容器網絡的大部分步驟。
首先啟動兩個容器:
$ docker run -i -t --rm --net=none base /bin/bash
$ docker run -i -t --rm --net=none base /bin/bash
找到進程號,然后創建網絡命名空間的跟蹤文件:
$ docker inspect -f '{{.State.Pid}}' 1f1f4c1f931a
2989
$ docker inspect -f '{{.State.Pid}}' 12e343489d2f
3004
$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/2989/ns/net /var/run/netns/2989
$ sudo ln -s /proc/3004/ns/net /var/run/netns/3004
創建一對peer接口。
$ sudo ip link add A type veth peer name B
添加IP地址和路由信息:
$ sudo ip link set A netns 2989
$ sudo ip netns exec 2989 ip addr add 10.1.1.1/32 dev A
$ sudo ip netns exec 2989 ip link set A up
$ sudo ip netns exec 2989 ip route add 10.1.1.2/32 dev A
$ sudo ip link set B netns 3004
$ sudo ip netns exec 3004 ip addr add 10.1.1.2/32 dev B
$ sudo ip netns exec 3004 ip link set B up
$ sudo ip netns exec 3004 ip route add 10.1.1.1/32 dev B
現在這兩個容器就可以相互ping通,並成功建立連接。點到點鏈路不需要子網和子網掩碼。
此外,也可以不指定--net=none來創建點到點鏈路。這樣容器還可以通過原先的網絡來通信。
利用類似的辦法,可以創建一個只跟主機通信的容器。但是一般情況下,更推薦使用--icc=false來關閉容器之間的通信。