春雨日記 about me tags

ヘアピンNATが使えない環境でDockerコンテナから自分自身にアクセスするメモ

はじめに

私はLinuxカーネルの管理などにセルフホストのGiteaを利用しており、CI/CD環境としてGitea Actionsを利用しています。

で、実は最近サーバの引っ越しを行いまして、ホストOSがUbuntu 20.04からArch Linuxとなりました。 その結果、これまで動いていたDocker on systemd-nspawnが動作しなくなってしまったためホスト上にDockerをインストールしたのですが、そのままでは同一ホスト上のGiteaインスタンスに接続できませんでした。

具体的には、act_runnerがグローバルなIPアドレスにpingを打って失敗し、先に進む事ができません。

ヘアピンNATに対応したルータであれば、パケットが転送されてサーバに戻ってきますが、我が家の民生用ルータにそのような機能はありません。 そこで、DNAT(Destination Network Address Translation)を設定し、docker0からグローバルIPに向けて出ていくパケットをローカルアドレスに変換する事で対処します。

私の環境は

  • Arch Linux
  • Firewalld(nftables backend)
  • nginxを用いてsystemd-nspawn上のGiteaを公開
  • docker0(172.19.0.1/16)
  • グローバルIPはDDNSで可変

という構成です。

実験

DDNSの可変IPに対応するのは後回しとして、現在のアドレスに対してDNATを設定してみます。

  • nftables設定

    1
    2
    3
    
    nft create table ip docker_redirect
    nft add chain ip docker_redirect PREROUTING { type nat hook prerouting priority dstnat\; }
    nft add rule ip docker_redirect PREROUTING ip daddr <グローバルIP> iif docker0 dnat to 172.19.0.1
    
  • firewalldで許可

    1
    2
    3
    
    firewall-cmd --add-service=http --zone=docker --permanent
    firewall-cmd --add-service=https --zone=docker --permanent
    firewall-cmd --reload
    

    *Arch Linuxに限るかもしれませんが、Dockerを入れると自動的にdockerゾーンが作られるようです。

その結果

level=info msg="Registering runner, arch=amd64, os=linux, version=v0.2.6."
level=debug msg="Successfully pinged the Gitea instance server"

act_runnerは正常動作したようです。

しかし、act_runnerが起動する子コンテナでは相変わらずアドレス変換が動作しておらず、グローバルなアドレスに向けてcloneしようとして失敗しました。(ログ取り忘れ)

そこで、act_runnerを設定してネットワークをhostに変更します。

act_runnerの設定

子コンテナのネットワーク設定はデフォルトでbridgeのようですが、hostにしてDNATが効くようにします。

やり方は色々だと思いますが、私は/opt/gitea/act_runner/に設定ファイルを作成し、docker run時にマウントするようにしました。

  • /opt/gitea/act_runner/config.yaml
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    log:
      # The level of logging, can be trace, debug, info, warn, error, fatal
      level: warn
    
    runner:
      file: /.runner
      capacity: 1
      env_file: .env
      timeout: 3h
      insecure: false
      fetch_timeout: 5s
      fetch_interval: 2s
    
    cache:
      enabled: true
      dir: ""
      host: ""
      port: 0
    
    container:
      network: host
      privileged: false
      # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
      options: 
      workdir_parent:
    

そして開始

1
2
3
docker run -d --name act_runner -e GITEA_INSTANCE_URL=https://<インスタンスURL> \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<トークン> -v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/gitea/act_runner/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml gitea/act_runner:latest

正常に動作するようになりました。

コメントに残している通り設定のoptionsにホストを追加することもできるのですが、どのみちgo製のact_runnerの為に何かしら対策を打つ必要があるのでhostにするだけで良いと思います。

DDNSに対応

いつぞや作ったルーター用設定を流用してこんな感じにしました。

  • /usr/local/bin/docker_loopback.sh

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    #!/bin/bash
    # DNSからAレコードを取得しdiffにて前回のアドレスと比較
    dig -t A dynamic.haru3.me +short > /tmp/iptest.b.txt
    diff /tmp/iptest.a.txt /tmp/iptest.b.txt
    if [ $? -eq 0 ]; then
        echo "IP is same"
        rm /tmp/iptest.b.txt
        exit 0
    fi
    rm /tmp/iptest.a.txt
    mv /tmp/iptest.b.txt /tmp/iptest.a.txt
    GLOBAL_IP=$(cat /tmp/iptest.a.txt)
    
    # docker0
    export DOCKER_IF=docker0
    export DOCKER_IP=172.19.0.1
    
    # 既存のテーブル削除
    set +e
    nft flush chain ip docker_redirect PREROUTING
    nft delete chain ip docker_redirect PREROUTING
    nft flush table ip docker_redirect
    nft delete table ip docker_redirect
    set -e
    
    # テーブル・ルール追加
    nft create table ip docker_redirect
    nft add chain ip docker_redirect PREROUTING { type nat hook prerouting priority dstnat\; }
    nft add rule ip docker_redirect PREROUTING ip daddr $GLOBAL_IP iif $DOCKER_IF dnat to $DOCKER_IP
    
  • /etc/systemd/system/docker_loopback.service

    [Unit]
    Description=docker loopback
    RefuseManualStart=no
    RefuseManualStop=yes
    After=network.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/docker_loopback.sh
    
  • /etc/systemd/system/docker_loopback.timer

    [Unit]
    Description=Run docker_loopback hourly
    
    [Timer]
    OnCalendar=hourly
    Persistent=true    
    
    [Install]
    WantedBy=timers.target
    
  • 有効化

    1
    2
    
    systemctl daemon-reload
    systemctl enable --now docker_loopback.timer
    

おわりに

DNATを設定する事で違和感なくact_runnerが動作するようになりました。

最近Alpine Linuxなどでもnftablesを使っているのですが、firewalld等のフロントエンドが無くても十分設定しやすいのでありがたいです。

ところで、調べている途中でebtablesのbroute風に動作させる方法が出てきたのですが、この方法でブルータを更新したいですね…

https://bbs.archlinux.org/viewtopic.php?id=275783