🌐

シェルスクリプト (sh) で Docker コンテナ内から Docker ネットワークの IPv4 アドレスをごにょごにょ計算する

2022/02/25に公開

調べても bash ばっかりで, Docker の alpine ベースのイメージでは利用できなかったので作った.

シナリオ

  • Docker コンテナ "app1" と "app2" が Docker ネットワーク "app_net" (driver: bridge) で接続されている.
  • "app_net" の各種アドレス設定は Docker によって自動的に行われる.
    すなわちネットワークアドレス, サブネットマスク / プレフィックス長, ゲートウェイアドレスおよび各コンテナの IP アドレスはホスト (Docker を実行しているコンピュータ) の環境により異なる場合がある.
  • "app2" は "app_net" のゲートウェイアドレスが欲しい.
  • "app2" は "app_net" 以外にも複数の Docker ネットワークと接続されている.

TL;DR

app1_addr=`host app1 | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
# e.g.) app_addr --> 172.24.0.13

linked_networks=`ip route list | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}' -o`
# e.g.) linked_networks --> (172.23.0.0/16 172.24.0.0/16 172.25.0.0/16)

for interface in $linked_networks; do
  interface_net_addr=`echo $interface | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
  interface_net_prefix_len=`echo $interface | grep -E '/([0-9]{1,2})' -o | cut -c 2-`
  # e.g.) interface_net_addr --> 172.23.0.0
  #       interface_net_prefix_len --> 16

  app_net_addr=`ipcalc -n "$app1_addr/$interface_net_prefix_len" | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
  # e.g.) app_net_addr --> 172.24.0.0
  
  if [ $interface_net_addr = $app_net_addr ]; then
    tmp1=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$1}'`
    tmp2=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$2}'`
    tmp3=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$3}'`
    tmp4=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$4}'`
    # e.g.) tmp1 --> 172
    #       tmp2 --> 24
    #       tmp3 --> 0
    #       tmp4 --> 0
    
    interface_net_addr_num=$(( (tmp1 << 24) + (tmp2 << 16) + (tmp3 << 8) + tmp4 ))
    # e.g.) interface_net_addr_num --> 2887254016
    
    interface_gateway_addr_num=$(( interface_net_addr_num + 1 ))
    # e.g.) interface_gateway_addr_num --> 2887254017
    
    interface_gateway_addr="$(( (interface_gateway_addr_num >> 24) % 256 )).$(( (interface_gateway_addr_num >> 16) % 256 )).$(( (interface_gateway_addr_num >> 8) % 256 )).$(( interface_gateway_addr_num % 256 ))"
    # e.g.) interface_gateway_addr --> 172.24.0.1
    
    # "app_net" のゲートウェイアドレスが $interface_gateway_addr であると計算できた.
    # ==> Do something....
    break
  fi
done

手順

"app2" から "app1" の IP アドレスを取得

Docker ネットワークはコンテナ名での名前解決を行うため, これと host コマンドを用いれば "app1" の IP アドレスは簡単に取得できる.

app1_addr=`host app1 | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
# e.g.) app_addr --> 172.24.0.13

"app2" が持つネットワークインターフェイス一覧から "app_net" のプレフィックス長を取得

Docker は基本的に "172.xxx.0.0/16" の形式でネットワークを構築するようである (経験則). よって上記手順で取得した app1_addr を文字列処理して下位 2 バイトを "0.0" と書き換えることでネットワークアドレスを計算できそうな気がする.
(2022/03/16 追記: "192.168.xxx.0/20" みたいなネットワークが作成されたケースを発見しました.)

しかしあらゆる環境でそうであるとは言い切れないため, 横着せずに

  1. ip route を用いてネットワークインターフェイス一覧を取得
  2. それぞれのプレフィックス長を取得
  3. それぞれのプレフィックス長と ipcalc コマンドを用いて "app1" の IP アドレスからネットワークアドレスを計算
  4. ネットワークアドレスが一致するか確認して "app_net" のネットワークインターフェイスを特定

することにする.

linked_networks=`ip route list | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}' -o`
# e.g.) linked_networks --> (172.23.0.0/16 172.24.0.0/16 172.25.0.0/16)

for interface in $linked_networks; do
  interface_net_addr=`echo $interface | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
  interface_net_prefix_len=`echo $interface | grep -E '/([0-9]{1,2})' -o | cut -c 2-`
  # e.g.) interface_net_addr --> 172.23.0.0
  #       interface_net_prefix_len --> 16

  app_net_addr=`ipcalc -n "$app1_addr/$interface_net_prefix_len" | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o`
  # e.g.) app_net_addr --> 172.24.0.0
  
  if [ $interface_net_addr = $app_net_addr ]; then
    # "app_net" のネットワークインターフェイスを特定できた. つまり "app_net" のネットワークアドレスが $interface_net_addr で, プレフィックス長が $interface_net_prefix_len あるとわかった.
    # ==> Do something....
    break
  fi
done

IPv4 アドレスを 1 バイトごとに分解して数値に変換する

プレフィックス長が 8 の倍数であれば文字列のままでも良いかもしれないが, そうでない場合はアドレスの計算が複雑である. そこで "." 区切りの文字列を数値に変換する.

  if [ $interface_net_addr = $app_net_addr ]; then
    tmp1=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$1}'`
    tmp2=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$2}'`
    tmp3=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$3}'`
    tmp4=`echo $WEB_SERVER_NETWORK_ADDR | awk -F\. '{printf"%d",$4}'`
    # e.g.) tmp1 --> 172
    #       tmp2 --> 24
    #       tmp3 --> 0
    #       tmp4 --> 0
    
    interface_net_addr_num=$(( (tmp1 << 24) + (tmp2 << 16) + (tmp3 << 8) + tmp4 ))
    # e.g.) interface_net_addr_num --> 2887254016
    
    # 1 バイトごとに "." で区切られた文字列である $interface_net_addr を数値に変換できた. よって IP アドレスを単純な 4 バイトの整数として扱える.
    # ==> Do something....
    break
  fi
done

アドレスを計算して "." 区切りの文字列に戻す.

ゲートウェイアドレスはネットワークアドレスに 1 を足すことで計算できる. 他のアドレスが知りたい場合も単純な数値計算で求められる.

    interface_gateway_addr_num=$(( interface_net_addr_num + 1 ))
    # e.g.) interface_gateway_addr_num --> 2887254017
    
    interface_gateway_addr="$(( (interface_gateway_addr_num >> 24) % 256 )).$(( (interface_gateway_addr_num >> 16) % 256 )).$(( (interface_gateway_addr_num >> 8) % 256 )).$(( interface_gateway_addr_num % 256 ))"
    # e.g.) interface_gateway_addr --> 172.24.0.1
    
    # "app_net" のゲートウェイアドレスが $interface_gateway_addr であると計算できた.
    # ==> Do something....
    break
  fi
done

実際に使った場面

  • Docker Compose で Nginx, Moodle, MySQL を起動.
  • Nginx と Moodle は Docker ネットワーク "moodle_web_net" で接続
  • Moodle と MySQL は "moodle_db_net" で接続
  • MySQL は初期起動時に, 事前に mysqldump した Moodle のデータベース, テーブルを復元

このとき, mysqldump したときと実際に起動したときの "moodle_web_net" のネットワークアドレスが異なる場合に error/admin/installhijacked エラーが発生する.

これは Moodle のインストール時に "mdl_user" テーブルの "lastip" カラムに "moodle_web_net" のゲートウェイアドレスを記録しており, これが実際のアクセス時に異なる場合には管理者が想定していないネットワークからのアクセスとみなすためである.

よって, 初期起動時に Nginx を "app1", Moodle コンテナを "app2", "moodle_web_net" を "app_net" と見立てて上記処理を行い, 以下のような SQL 文を発行する必要があった.

mysql \
  -u $MYSQL_USER -p $MYSQL_PASSWORD -h $MYSQL_ADDR \
  -e "UPDATE moodle.mdl_user set lastip='$interface_gateway_addr' where username='admin';"

Discussion