Docker 容器非常轻量,系统开销非常少,比 VMware 或者 VirtualBox 用起来方便,部署起来也非常容易。官方推荐我们通过端口映射的方式把 Docker 容器的服务提供给宿主机或者局域网其他容器使用。一般过程是:
1、Docker 进程通过监听宿主机的某个端口,将该端口的数据包发送给 Docker 容器
2、宿主机可以打开防火墙让局域网其他设备通过访问宿主机的端口进而访问 docker 的端口
这里以 CDNS 为例,CDNS 是一个用于避免 DNS 污染的程序,通过 CDNS 可以把你的计算机变成一个抗污染的 DNS 服务器提供给局域网使用。Docker 镜像下载地址:https://hub.docker.com/r/alexzhuo/cdns/
原理是在 Docker 容器中启动 CDNS,监听 53 端口,Docker 容器的 IP 地址为 172.12.0.2,宿主机把 5053 端口映射到 Docker 容器上,访问宿主机的 127.0.0.1:5053 就相当于访问 Docker 的 53 端口,所以 Docker 的启动方法是:
- sudo docker run -itd -p 0.0.0.0:5053:53/udp --name=CureDNS alexzhuo/cdns cdns -c /etc/cdns.config.json
这样我们使用 dig 工具通过 5053 端口查询 DNS 就是无污染的 DNS 了,过程如下:
- alex@alex-Lenovo-U310:~$ dig www.facebook.com @127.0.0.1 -p 5053
- ; <<>> DiG 9.10.3-P4-Ubuntu <<>> www.facebook.com @127.0.0.1 -p 5053
- ;; global options: +cmd
- ;; Got answer:
- ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9522
- ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 5
- ;; OPT PSEUDOSECTION:
- ; EDNS: version: 0, flags:; udp: 4096
- ;; QUESTION SECTION:
- ;www.facebook.com. IN A
- ;; ANSWER SECTION:
- www.facebook.com. 1550 IN CNAME star-mini.c10r.facebook.com.
- star-mini.c10r.facebook.com. 30 IN A 31.13.95.36
- ;; AUTHORITY SECTION:
- c10r.facebook.com. 2010 IN NS a.ns.c10r.facebook.com.
- c10r.facebook.com. 2010 IN NS b.ns.c10r.facebook.com.
- ;; ADDITIONAL SECTION:
- a.ns.c10r.facebook.com. 2439 IN A 69.171.239.11
- a.ns.c10r.facebook.com. 2439 IN AAAA 2a03:2880:fffe:b:face:b00c:0:99
- b.ns.c10r.facebook.com. 3351 IN A 69.171.255.11
- b.ns.c10r.facebook.com. 1253 IN AAAA 2a03:2880:ffff:b:face:b00c:0:99
- ;; Query time: 47 msec
- ;; SERVER: 127.0.0.1#5053(127.0.0.1)
- ;; WHEN: Mon Apr 10 16:21:46 CST 2017
- ;; MSG SIZE rcvd: 213
这里假设我们的宿主机 IP 是 192.168.12.107
如果现在出现另外一台局域网计算机,IP 地址为 192.168.12.113,它想把宿主机当成 DNS 服务器,那么我们就需要在 192.168.12.113 这台计算机上访问 192.168.12.107:5053 来查询 DNS,dig 命令如下
- dig www.facebook.com @192.168.12.107 -p 5053
这样做显然是很不方便的,我们现在希望不经过宿主机这一套 NAT 和代理,想要直接在局域网内的任意一台计算机上直接通过 IP 访问 Docker 容器,让 Docker 容器完整的暴露在局域网里而不是仅单单暴露一个 53 端口。那么应该如何做呢?
首先通过观察发现,Docker 的默认启动方式中,会产生一块虚拟网卡,在这里我们可以理解这块网卡连接着一个虚拟交换机,然后每个 Docker 容器又会拥有自己单独的网卡和 IP,而且所有 Docker 容器也连接在这个虚拟交换机的下面。我们可以在宿主机上通过 ifconfig 命令看到这个虚拟网卡。
- alex@alex-Lenovo-U310:~$ ifconfig
- docker0 Link encap:以太网 硬件地址 02:42:cd:21:5c:81
- inet 地址:172.17.0.1 广播:0.0.0.0 掩码:255.255.0.0
- inet6 地址: fe80::42:cdff:fe21:5c81/64 Scope:Link
- UP BROADCAST RUNNING MULTICAST MTU:1500 跃点数:1
- 接收数据包:2892 错误:0 丢弃:0 过载:0 帧数:0
- 发送数据包:3517 错误:0 丢弃:0 过载:0 载波:0
- 碰撞:0 发送队列长度:0
- 接收字节:187022 (187.0 KB) 发送字节:4771886 (4.7 MB)
- lo Link encap:本地环回
- inet 地址:127.0.0.1 掩码:255.0.0.0
- inet6 地址: ::1/128 Scope:Host
- UP LOOPBACK RUNNING MTU:65536 跃点数:1
- 接收数据包:9993 错误:0 丢弃:0 过载:0 帧数:0
- 发送数据包:9993 错误:0 丢弃:0 过载:0 载波:0
- 碰撞:0 发送队列长度:1
- 接收字节:934304 (934.3 KB) 发送字节:934304 (934.3 KB)
- wlp3s0 Link encap:以太网 硬件地址 74:e5:43:b0:dd:b0
- inet 地址:192.168.12.107 广播:192.168.12.255 掩码:255.255.255.0
- inet6 地址: fe80::8adf:28f7:5ec:3a5d/64 Scope:Link
- UP BROADCAST RUNNING MULTICAST MTU:1500 跃点数:1
- 接收数据包:69760 错误:0 丢弃:0 过载:0 帧数:0
- 发送数据包:64718 错误:0 丢弃:0 过载:0 载波:0
- 碰撞:0 发送队列长度:1000
- 接收字节:41517057 (41.5 MB) 发送字节:9971762 (9.9 MB)
上面的 docker0 这块网卡就是虚拟网卡,它的 IP 地址是 172.17.0.1,它和其他的 docker 容器都连接在一个虚拟交换机上,网段为 172.17.0.0/16,下面我们登录到 Docker 容器里面,查看一下容器的网卡和 IP
- # ifconfig
- eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
- inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
- inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
- UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
- RX packets:3449 errors:0 dropped:0 overruns:0 frame:0
- TX packets:2811 errors:0 dropped:0 overruns:0 carrier:0
- collisions:0 txqueuelen:0
- RX bytes:4763490 (4.7 MB) TX bytes:219998 (219.9 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:1
- RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
可以看到这个容器的 IP 地址为 172.17.0.2,现在我们到宿主机里看看 ping 172.17.0.2 能不能 ping 通。
答案当然是能 ping 通,能 ping 通的原因就是我们的宿主机里知道目标地址为 172.17.0.1/16 的路由信息,不信我们可以查看一下
- alex@alex-Lenovo-U310:~$ ip route
- default via 192.168.12.1 dev wlp3s0 proto static metric 600
- 169.254.0.0/16 dev docker0 scope link metric 1000
- 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
- 192.168.12.0/24 dev wlp3s0 proto kernel scope link src 192.168.12.107 metric 600
从上面可以看出来,172.17.0.0/16 这个网段的数据包可以通过 docker0 这块网卡发送出去。也就是说,目前宿主机有两个 IP,一个是 192.168.12.107, 用于连接实体的局域网,一个是 172.17.0.1,用来和 Docker 容器通信,从这可以看出宿主机和路由器的作用是一致的。而 Docker 容器只有一个 IP 就是 172.17.0.2。如果 docker 容器想要访问外网,那么它就会把数据包发送到网关 172.17.0.1,然后由宿主机做 NAT 发送到 192.168.12.1/24 这个网段的网关上。
不光宿主机可以 ping 通容器,而且由于在 docker 容器中默认路由(网关)是 172.17.0.1, 所以 docker 容器不光可以 ping 主机的 172.17.0.1, 还能 ping 通主机的另一个 IP:192.168.12.107
此时我们的网络拓扑其实就变成了 192.168.12.0/24 这个网段里有个宿主机,为了方便理解,我们把这个宿主机看成一个路由器,路由器下面是 172.17.0.1/16 这个网段。我们把 Docker 容器看成实实在在的机器设备,连接在宿主机这个路由器的下面。这样 Docker 的拓扑结构就非常清晰了。我们可以发现这个拓扑结构其实非常的简单。就像家里上网的路由器一样。打个比方:我家里有两个路由器,一个路由器通过 PPPOE 拨号连接公网,内网地址为 192.168.12.1,另一个路由器连接在第一个路由器上面,WAN 口 IP 是 192.168.12.107,LAN 口地址是 172.17.0.1,我们的 Docker 容器看成一个个的电脑接在第二个路由器 LAN 上面,所以 Docker 容器的 IP 为 172.17.0.2。
第二个路由器(宿主机)通过 NAT 让我们的电脑们(Docker 容器)可以访问互联网。电脑们(Docker 容器们)可以互相 ping 通,也能 ping 通全部两个路由器。第二个路由器可以 ping 通电脑们,但是第一个路由器 ping 不同电脑们。如果还是不理解拓扑结构,可以自己在家里买两个路由器一前一后放上试试。
现在问题来了,如果有一个电脑连接在第一个路由器的下面,和第二个路由器(宿主机)平级,其 IP 为 192.168.12.113,现在它想 ping 通 172.17.0.2 这个 Docker 容器,发现是 ping 不通的。同样第一台路由器自己也是 ping 不通 Docker 容器的
原因很简单,这台新计算机只能 ping 通同网段的设备,比如路由器 2,因为他们同属于 192.168.12.1/24 这个网段。而 172.17.0.2/16 这个网段它并不知道怎么路由过去,只能把目标地址为 172.17.0.1/16 的数据包发给路由器一,可惜就连第一个路由器也不知道怎么个路由法。所以我们就 ping 不通了。
所以现在问题就很好解决了,我们只需要告诉这台新电脑或者第一个路由器到 172.17.0.2/16 这个网段的路径就可以了。
于是我们可以在新电脑或者路由器一中这样写
- route add -net 172.17.0.1/16 gw 192.168.12.107
或者是
- ip route add 172.17.0.1/16 via 192.168.12.107
普通路由器可以像这样设置
现在新电脑访问 172.17.0.2 的数据包就会先被发送到宿主机(第二个路由器)上,然后宿主机再转发到 Docker 容器上,我们就把 Docker 容器暴露到局域网里了。
但此时你会发现你在新计算机上还是 ping 不通,这是为什么呢。因为路由器二(宿主机)对它的内网机器也就是 Docker 容器们全部开启了 NAT,源 IP 为 172.17.0.2/16 的数据包不会出现在宿主机以外的网络中,因为他们被 NAT 了。这个 NAT 是 Docker 进程默认自动帮我们实现的,我们先看一下
- alex@alex-Lenovo-U310:~$ sudo iptables -t nat -L -n
- Chain PREROUTING (policy ACCEPT)
- target prot opt source destination
- DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
- Chain INPUT (policy ACCEPT)
- target prot opt source destination
- Chain OUTPUT (policy ACCEPT)
- target prot opt source destination
- DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
- Chain POSTROUTING (policy ACCEPT)
- target prot opt source destination
- MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
- MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:53
- MASQUERADE udp -- 172.17.0.2 172.17.0.2 udp dpt:53
- Chain DOCKER (2 references)
- target prot opt source destination
- RETURN all -- 0.0.0.0/0 0.0.0.0/0
- DNAT tcp -- 0.0.0.0/0 127.0.0.1 tcp dpt:5053 to:172.17.0.2:53
- DNAT udp -- 0.0.0.0/0 127.0.0.1 udp dpt:5053 to:172.17.0.2:53
注意那句 MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0 会导致所有 172.17.0.0/16 的数据包都不能到达 docker 以外的网络,所以我们要关掉这个 NAT,关掉很容易,我们只需删掉这一条 iptables 规则就可以了。然后源 IP 为 172.17.0.2 的数据包就可以出现在 192.168.12.1/24 的网络中了。
- sudo iptables -t nat -D POSTROUTING 3
但是把 NAT 关掉了以后,虽然内网可以互 ping 了,但是 Docker 容器可能上不去网呀。第一个路由器如果自动 NAT 了 172.17.0.2 还好,但要是没有人给 Docker 容器做 NAT,Docker 容器就不能上网了,那我们的 CDNS 也就没法用了。那么如何既保证 Docker 容器访问外网的数据包被 NAT,又保证内网通信不被 NAT 呢?只要稍微修改一下 iptables 规则就好了,如下
- sudo iptables -t nat -A POSTROUTING -s 172.17.0.2 ! -d 192.168.12.1/24 -j MASQUERADE
上面的 iptables 规则通过对内外网流量的分离实现区别的 NAT 对待,就可以既保证 Docker 容器正常上网,也可以被内网其他主机访问了。
可能会有这么一种情况,上面所说的第一台路由器不是什么智能路由器,或者你没有权限在那个路由器上配置路由条目,让目标为 172.17.0.0/16 的数据包通过路由器进行路由。同时你的局域网其他电脑是 XP 系统的,也没法手动配置路由规则,这该怎么办呢?
现在以要访问 Docker 容器的局域网主机为 Windows XP 系统为例,虽然 WinXP 不能手动配置路由规则,但是我们可以配置网关,只要我们把网关设置为 192.168.0.107 也就是我们的宿主机,目标地址为 172.17.0.0/16 的 IP 包就会发送到宿主机上,而宿主机不同于第一个路由器,它是知道如何路由这些 IP 包的。于是我们就可以在 WinXP 上 ping 通 Docker 容器了
来源: http://blog.csdn.net/lvshaorong/article/details/69950694