docker-proxy #01

dockerを使ってコンテナを外部ネットワークに公開する場合、例えば次のようなdocker-compose.yamlを使用するとホストの80と443を使用する。

version: '2.3'
services:
  httpd:
    image: nginx:1.19.0-alpine
    container_name: 'httpd'
    init: true
    networks:
      nw:
        ipv4_address: "192.168.10.10"
    ports:
      - 80:80/tcp
      - 443:443/tcp
    restart: unless-stopped

networks:
  nw:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet:  "192.168.10.0/24"
          gateway: "192.168.10.1"
    driver_opts:
      com.docker.network.bridge.name: docker_httpd

この際、ホスト上ではdocker-proxyがポートをバインドする。

# netstat -anp | grep docker-proxy
tcp6       0      0 :::443                  :::*                    LISTEN      3000/docker-proxy
tcp6       0      0 :::80                   :::*                    LISTEN      3027/docker-proxy
#
# lsof -i:80
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
docker-pr 3027 root    4u  IPv6  38568      0t0  TCP *:http (LISTEN)
#
# lsof -i:443
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
docker-pr 3000 root    4u  IPv6  39366      0t0  TCP *:https (LISTEN)

IPv6を有効にしたホストではIPv4のポートバインドが見えないが、dockerの場合はIPv4でもアクセスは可能。IPv6ソケットでIPv4の通信を行っていると思われる。(参考:https://www.geekpage.jp/blog/?id=2017-3-8-1


Netfilterの処理は次に大別できる。

  • NW-IF→PREROUTING→INPUT→Local Process
  • NW-IF→PREROUTING→FORWARD→POSTROUTING→NW-IF
  • Local Process→OUTPUT→POSTROUTING→NW-IF
Netfilter 処理メモ

Dockerでは通常、次のようなルールをiptablesに挿入する。

# iptables -nL -t nat (抜粋)
Chain PREROUTING (policy ACCEPT)
target                   prot opt source    destination
PREROUTING_direct        all  --  0.0.0.0/0 0.0.0.0/0
PREROUTING_ZONES_SOURCE  all  --  0.0.0.0/0 0.0.0.0/0
PREROUTING_ZONES         all  --  0.0.0.0/0 0.0.0.0/0
DOCKER                   all  --  0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
# iptables -nvL (抜粋)
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
target              prot opt in out source     destination
ACCEPT              all  --  *  *   0.0.0.0/0  0.0.0.0/0  ctstate RELATED,ESTABLISHED
ACCEPT              all  --  lo *   0.0.0.0/0  0.0.0.0/0
INPUT_direct        all  --  *  *   0.0.0.0/0  0.0.0.0/0
INPUT_ZONES_SOURCE  all  --  *  *   0.0.0.0/0  0.0.0.0/0
INPUT_ZONES         all  --  *  *   0.0.0.0/0  0.0.0.0/0
DROP                all  --  *  *   0.0.0.0/0  0.0.0.0/0  ctstate INVALID
REJECT              all  --  *  *   0.0.0.0/0  0.0.0.0/0  reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
target                    prot opt in      out       source     destination
DOCKER-USER               all  --  *       *         0.0.0.0/0  0.0.0.0/0
DOCKER-ISOLATION-STAGE-1  all  --  *       *         0.0.0.0/0  0.0.0.0/0
ACCEPT                    all  --  *       docker0   0.0.0.0/0  0.0.0.0/0  ctstate RELATED,ESTABLISHED
DOCKER                    all  --  *       docker0   0.0.0.0/0  0.0.0.0/0
ACCEPT                    all  --  docker0 !docker0  0.0.0.0/0  0.0.0.0/0
ACCEPT                    all  --  docker0 docker0   0.0.0.0/0  0.0.0.0/0
ACCEPT                    all  --  *       *         0.0.0.0/0  0.0.0.0/0  ctstate RELATED,ESTABLISHED
ACCEPT                    all  --  lo      *         0.0.0.0/0  0.0.0.0/0
FORWARD_direct            all  --  *       *         0.0.0.0/0  0.0.0.0/0
FORWARD_IN_ZONES_SOURCE   all  --  *       *         0.0.0.0/0  0.0.0.0/0
FORWARD_IN_ZONES          all  --  *       *         0.0.0.0/0  0.0.0.0/0
FORWARD_OUT_ZONES_SOURCE  all  --  *       *         0.0.0.0/0  0.0.0.0/0
FORWARD_OUT_ZONES         all  --  *       *         0.0.0.0/0  0.0.0.0/0
DROP                      all  --  *       *         0.0.0.0/0  0.0.0.0/0  ctstate INVALID
REJECT                    all  --  *       *         0.0.0.0/0  0.0.0.0/0  reject-with icmp-host-prohibited

PREROUTING chainに追加されるDOCKER chainによりコンテナ向けの通信はNATされ、FORWARD chianに渡される。FORWARD chainではDOCKER chain内で通信の転送が許可されるようになっているので、コンテナへの通信をホストで制限したい場合はDOCKER chainよりも先に処理されるDOCKER-USER chainを使用する。

IPv6ではPREROUTINGの処理が自動で挿入されないため、そのままだとINPUT chainでの処理になる。特に困らなければINPUT_direct chainで制御すればよいと思う。INPUT chainで許可された通信はホストのdocker-proxyプロセスに到達することになる。

尚、docker-proxyを経由させる分パフォーマンスは落ちると思われるので気にするようなら転送設定をした方が良い。

docker-proxyを停止したい場合はuserland-proxyをfalseに設定し、dockerdを再起動する。

# vi /etc/docker/daemon.json
{
  "userland-proxy": false,
  ~省略~
}
# systemctl restart docker