A clumsy Docker network notes
Table of contents
A clumsy Docker networking notes
Nguồn:
- https://github.com/ntk148v/til/tree/master/docker/networking
- https://argus-sec.com/docker-networking-behind-the-scenes/
- https://docs.docker.com/network/
- https://github.com/KamranAzeem/learn-docker/blob/master/docs/docker-networking-deep-dive.md
- https://docs.docker.com/engine/tutorials/networkingcontainers/
- https://docs.docker.com/network/iptables/
- https://dev.to/polarbit/how-docker-container-networking-works-mimic-it-using-linux-network-namespaces-9mj
Khái niệm
Các khái niệm:
- Trước khi đi vào network namespace, cần hiểu về Namespace. Namespace là một tính năng của Linux kernel để phân vùng tài nguyên của kernel sao cho một tập hợp các process chỉ nhìn thấy tập hợp các tài nguyên (trong namespace đó) trong khi một tập hợp các process khác nhìn thấy tập các tài nguyên khác (trong namespace khác). Tính năng này hoạt động bằng cách đưa tập các tài nguyên và process vào cùng một namespace, các namespace khác nhau sẽ tham chiếu đến các tài nguyên riêng biệt. Tài nguyên có thể tồn tại trong nhiều không gian. Ví dụ về các tài nguyên như vậy là process ID, hostname, user ID, tên file và một số tên (name) liên quan đến quyền truy cập mạng và giao tiếp giữa các process.
- Networking namespace là một trong các loại namespace. Mỗi network namespace có network stack riêng: địa chỉ IP, network interface, route table, …
- Bạn có thể xem thao tác các network namespace bằng câu lệnh sau:
$ sudo ip netns <sub-command>
Network bridge: một thiết bị mạng ảo chuyển tiếp các gói giữa hai hoặc nhiều phân đoạn mạng.
Veth pair: hiểu đơn giản là virtual network cable nối giữa 2 network interface device (NIC).
Về cơ bản, Docker sử dụng các tính năng trên như sau:
Docker và network namespace
Ví dụ sau lấy theo các thông số, cấu hình mặc định khi chạy Docker container.
- Khi tạo và chạy 1 container, Docker đồng thời tạo ra một network namespace riêng cho container đó. Sau đó, Docker kết nối network của container đó với Linux bridge docker0 bằng veth pair.
- Như ở trên đã nói, để xem danh sách network namespace, gõ câu lệnh sau:
$ sudo ip netns list
# Start a container
$ docker run --name nstest -it busybox
# Open another terminal session
$ sudo ip netns list
# nothing return
- Hmm, không có gì trả về. Mặc định Docker không thêm container network namespace vào trong Linux runtime data, bạn có thể tạo softlink cho network namespace đó:
# Get container process id.
$ pid="$(docker inspect -f '{{.State.Pid}}' nstest)"
# Soft link the network namespace
$ sudo mkdir -p /var/run/netns/
$ sudo ln -sf /proc/$pid/ns/net /var/run/netns/nstest
$ sudo ip netns list
nstest (id: 0)
# ^ this, it works.
- Chạy vào network namespace, như đã nói, mỗi network namespace có network stack riêng:
$ docker exec nstest ip a
docker exec nstest ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
$ sudo ip netns exec nstest ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# ^ result is the same as docker exec command.
$ ip a | grep docker
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
8: veth4866e91@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
- Ở đây, eth0@if8 trong container namespace đang kết nối với interface veth4866e91@if7 trên docker0 bridge.
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242f0c749bd no veth4866e91 # this
lxdbr0 8000.00163e2afe62 no
Tản mạn qua Docker networking subsystem
Kiến trúc Docker networking subsystem dạng pluggable, có thể mở rộng sử dụng drivers. Mặc định, có những drivers sau: bridge
, host
, none
, overlay
, macvlan
, ipvlan
.
Trong bài viết này, chủ yếu nói đến bridge
driver.
docker0 - default bridge network
- Mặc định, tất cả container đều kết nối với docker0.
- Docker engine khi start up sẽ tìm một subnet không được sử dụng (bắt đầu trong một danh sách fixed trong code) và gán IP đầu tiên cho docker0. Như trong trường hợp của tui, đó là
172.17.0.1/16
. - Nếu không có containers nào chạy và kết nối với docker0, trạng thái của docker0 là DOWN.
- Các containers kết nối với docker0, kế thừa cấu hình DNS của host:
/etc/hosts
and/etc/resolv.conf
. - Lưu ý, ở docker0 bridge, không có tính năng service discovery, do vậy, các containers phải giao tiếp thông qua địa chỉ IP.
# Start a second container
$ docker run --name nstest2 -it busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
9: eth0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ping nstest
ping: bad address 'nstest'
/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.263 ms
^C
--- 172.17.0.2 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.263/0.263/0.263 ms
User-defined bridge network
- Người dùng có thể tự tạo Docker network.
$ docker network create mynet
ed7da300a506e6e9be68b8f69d1b857dd7cc8db2a9d51c4b85dba37976780293
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
935865833d99 bridge bridge local
f753aeafa1a1 host host local
ed7da300a506 mynet bridge local
6b0488ceccc8 none null local
$ docker network inspect mynet
[
{
"Name": "mynet",
"Id": "ed7da300a506e6e9be68b8f69d1b857dd7cc8db2a9d51c4b85dba37976780293",
"Created": "2023-12-12T09:37:34.358888862+07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
# Check with ip addr show, it should show up as a network
# inteface on the host with the name br-<Network ID substring>
$ ip addr show
# ...
11: br-ed7da300a506: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:f8:36:9b:e0 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-ed7da300a506
valid_lft forever preferred_lft forever
$ brctl show
bridge name bridge id STP enabled interfaces
br-ed7da300a506 8000.0242f8369be0 no
docker0 8000.0242f0c749bd no
lxdbr0 8000.00163e2afe62 no
- Không giống docker0 bridge, network mới có DNS-based service discovery:
- Ở đây, sử dụng custom image
praqma/network-multitool
để có đủ commands và tools, có thể sử dụng image bất kỳ và cài đặt tool tương tự.
- Ở đây, sử dụng custom image
$ docker run --name=web --network=mynet -d wbitt/network-multitool
e2d841b7a02916aa25a2fa5ceace0427ad7c0ccc967ad01fb5cc423460f0e990
$ docker run --name=db --network=mynet -e MYSQL_ROOT_PASSWORD=secret -d mysql
492ba8a9277d96876f0a605d9434723aed774978a40023e778a97815216754f0
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e2d841b7a029 wbitt/network-multitool "/bin/sh /docker/ent…" 15 seconds ago Up 14 seconds 80/tcp, 443/tcp, 1180/tcp, 11443/tcp web
492ba8a9277d mysql "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp, 33060/tcp db
# Ping each other
$ docker exec -it web bash
e2d841b7a029:/# ping -c 1 db
PING db (172.18.0.2) 56(84) bytes of data.
64 bytes from db.mynet (172.18.0.2): icmp_seq=1 ttl=64 time=0.096 ms
--- db ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.096/0.096/0.096/0.000 ms
e2d841b7a029:/# dig db
; <<>> DiG 9.18.16 <<>> db
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57615
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;db. IN A
;; ANSWER SECTION:
db. 600 IN A 172.18.0.2
;; Query time: 0 msec
;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
;; WHEN: Tue Dec 12 03:18:39 UTC 2023
;; MSG SIZE rcvd: 38
e2d841b7a029:/#
- Tại sao có thể thao tác phân giải tên container như vậy? Đó là nhờ một DNS nhúng ở trong Docker service.
e2d841b7a029:/# cat /etc/resolv.conf
nameserver 127.0.0.11
options edns0 trust-ad ndots:0
e2d841b7a029:/# netstat -ntlup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.11:45541 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 1/nginx: master pro
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1/nginx: master pro
udp 0 0 127.0.0.11:42758 0.0.0.0:* -
/etc/resolv.conf
cho thấy container đang trỏ về nameserver 127.0.0.11:53 nhưng rõ ràng không có tiến trình nào lắng nghe port 53 cả. Vậy DNS này hoạt động như thế nào?- Tiếp tục, tạo thêm 1 container khác, nhưng có nhiều quyền hơn.
iptables
chỉ chạy khi có capabilities-cap-add=NET_ADMIN and --cap-add=NET_RAW
.
$ docker run \
--name tool \
--network mynet \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-it wbitt/network-multitool /bin/bash
ccd72fe225c8:/# dig -c db
;; Warning, ignoring invalid class db
; <<>> DiG 9.18.16 <<>> -c db
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17778
;; flags: qr rd ra; QUERY: 1, ANSWER: 13, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;. IN NS
;; ANSWER SECTION:
. 31133 IN NS d.root-servers.net.
. 31133 IN NS l.root-servers.net.
. 31133 IN NS k.root-servers.net.
. 31133 IN NS i.root-servers.net.
. 31133 IN NS j.root-servers.net.
. 31133 IN NS e.root-servers.net.
. 31133 IN NS h.root-servers.net.
. 31133 IN NS g.root-servers.net.
. 31133 IN NS a.root-servers.net.
. 31133 IN NS f.root-servers.net.
. 31133 IN NS c.root-servers.net.
. 31133 IN NS b.root-servers.net.
. 31133 IN NS m.root-servers.net.
;; Query time: 8 msec
;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
;; WHEN: Tue Dec 12 03:20:07 UTC 2023
;; MSG SIZE rcvd: 239
ccd72fe225c8:/# netstat -ntlup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.11:39527 0.0.0.0:* LISTEN -
udp 0 0 127.0.0.11:35031 0.0.0.0:* -
ccd72fe225c8:/# iptables-nft-save
# Generated by iptables-nft-save v1.8.9 (nf_tables) on Tue Dec 12 03:20:27 2023
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER_OUTPUT - [0:0]
:DOCKER_POSTROUTING - [0:0]
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
# Queries for DNS:
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:39527
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:35031
# Response from DNS:
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 39527 -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 35031 -j SNAT --to-source :53
COMMIT
# Completed on Tue Dec 12 03:20:27 2023
- iptables does magic thing! Mọi DNS query đến
127.0.0.11:53
thực tế được điều hướng sang127.0.0.11:39527
(TCP) và127.0.0.11:35031
(UDP).- Thông thường, DNS sử dụng UDP cho queries nhỏ hơn 512 bytes, trên 512 bytes -> TCP.
Lưu ý: Port trên hình có thể khác với thực tế.
- Chi tiết về implementation có thể tham khảo ở đây:
case container.HostConfig.NetworkMode.IsUserDefined():
// The container uses a user-defined network. We use the embedded DNS
// server for container name resolution and to act as a DNS forwarder
// for external DNS resolution.
// We parse the DNS server(s) that are defined in /etc/resolv.conf on
// the host, which may be a local DNS server (for example, if DNSMasq or
// systemd-resolvd are in use). The embedded DNS server forwards DNS
// resolution to the DNS server configured on the host, which in itself
// may act as a forwarder for external DNS servers.
// If systemd-resolvd is used, the "upstream" DNS servers can be found in
// /run/systemd/resolve/resolv.conf. We do not query those DNS servers
// directly, as they can be dynamically reconfigured.
*sboxOptions = append(
*sboxOptions,
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
)
default:
Một số command hay ho
- Join container vào process namespace của container khác.
- Có thể thấy process mysqld của container db.
$ docker run --name busybox \
--pid container:db \
--rm -it busybox /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 999 0:25 mysqld
237 root 0:00 /bin/sh
243 root 0:00 ps aux
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
48: eth0@if49: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
- Join thêm network namespace.
- Địa chỉ IP đang là 172.18.0.2/16 (db network namespace).
docker run --name busybox \
--pid container:db \
--network container:db --rm -it busybox /bin/sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
38: eth0@if39: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ps aux
PID USER TIME COMMAND
1 999 0:26 mysqld
245 root 0:00 /bin/sh
252 root 0:00 ps aux
/ #