1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[A2B攻撃対策]日本のスマホ回線だけを通す nginx 許可リストを作りたい

1
Last updated at Posted at 2026-10-09

筆者表記

工程 担当
課題提示・取捨選択・内容チェック 筆者
原案作成・編成・動作検証(nginx 1.24 での実機試験) Claude Fable 5.1

本記事は AI(Fable)に作成を依頼したものをそのまま掲載しています。未テストです。設定を採用する前に、各社の公開ページと nginx のドキュメント(原典)を確認してください。

ひとことで言うと

世界中からの不正アクセスをいったん止める「一時しのぎ」として、国内モバイル4社(docomo / au / SoftBank / 楽天モバイル)の IP 帯域からの接続だけを nginx で通す設定を作りました。

  • 帯域はキャリアの公開ページだけでは揃わないので、AS 番号から BGP 広告中の帯域を毎日自動取得し、公式ページの帯域で補完します
  • nginx 側は geo → map → if/return の変数連鎖にして、遮断しない「観測モード」で確認してから遮断モードへ切り替えます
  • 「日本のスマホ」だけに絞ると、同じスマホでも自宅 Wi-Fi 経由や MVNO 回線は通らなくなります。用途によっては日本割当全体(--country JP)で始める方が現実的です

スクリプト・設定ファイル・systemd ユニットの全文は文末の「付録:ファイル全文」に載せています。本文では要点と判断材料だけを書きます。

前提:「日本のスマホの IP 帯域」は 3 系統ある

入手元の比較

系統 内容 集約後の件数(2026-10-09) 通る範囲 向き不向き
キャリア公式ページ docomo spモード(IPv4 181・IPv6 28、ahamo も同帯域)、au LTE NET / 5G NET(IPv4 12・IPv6 7、2025年1月現在)。SoftBank・楽天モバイルはスマホ向けの現行公開ページが筆者には見つからず docomo 209、au 19 各社のスマホ・タブレット回線のみ 最も狭く正確。ただし 2 社分しかなく、更新追従も手作業
AS 番号(本記事の主線) AS9605 docomo、AS2516 KDDI、AS17676 / AS17505 SoftBank、AS138384 / AS23720 楽天モバイルが BGP で広告中の帯域。RIPEstat API または GitHub ミラー(ipverse/asn-ip)から取得 825(IPv4 約 5,950 万アドレス) 4 社の全顧客。固定回線(au ひかり・SoftBank 光など※)や法人向けも含む 自動更新できる。MVNO と他社固定回線は通らない
国別割当(JP) APNIC 委譲ファイル delegated-apnic-latest または GitHub ミラー(ipverse/rir-ip)。日本に割り当てられた全アドレス 3,872(IPv4 約 1 億 8,870 万アドレス) 国内 ISP・MVNO・自宅 Wi-Fi・国内 VPS まで 利用者を取りこぼさない。国内 VPS からの攻撃は通る。AWS 東京のような海外登録の国内拠点は含まれない
iCloud プライベートリレー(追加用) Apple 公開の出口 IP CSV(mask-api.icloud.com/egress-ip-ranges.csv)の JP 行 数百〜数千 iPhone Safari のリレー利用者 上記のいずれにも足せる。リレーは利用者の国を保つ設計なので JP 行だけでよい

公式ページと AS 広告の関係も確かめました。docomo の公開帯域 209 件のうち 187 件、au の 19 件は全件が、それぞれ AS9605 / AS2516 の広告帯域に含まれていました。docomo の残り 22 件は日本割当ではあるものの 2026-10-09 時点で AS9605 から広告されておらず(うち 1 件 202.229.178.32/27 は AS2514 = NTT PC コミュニケーションズから広告)、この穴は公式ページの帯域を carrier_published.txt として足して埋めます。逆に、docomo が「2026年9月〜2027年2月に追加予定」として告知している 11 件は、すでに AS9605 で広告済みでした。AS 方式は公式ページより先に新帯域を拾えることがある、ということです。

この設定で通らなくなるもの

「日本のスマホ」だけを通す設定にすると、次の利用者が締め出されます。遮断モードに入れる前に、自分のサービスにこれらが含まれないか確認してください。

  • 同じスマホでも自宅・職場の Wi-Fi 経由のアクセス(送信元は固定回線 ISP のアドレス)
  • MVNO 回線(IIJmio、mineo、NURO モバイル、BIGLOBE、日本通信 など)。L2 接続の MVNO は自社 AS のアドレスで出てくるため※。ahamo は docomo の公開ページに「同じ帯域」と明記されているので通ります。UQ mobile / povo(KDDI)、Y!mobile / LINEMO(SoftBank)もキャリア本体の網なので AS 単位の取得に含まれるはずですが、筆者未検証※
  • iPhone の Safari で iCloud+ の「プライベートリレー」を有効にしている利用者。docomo の公開ページにも「iCloud Private Relay の設定が ON の場合は異なります」とあります。スクリプトの --icloud-relay で救済できます
  • VPN、Cloudflare WARP、企業の MDM/クラウドプロキシ経由の端末
  • PC からのアクセス全般(管理者自身を含む)。管理用の固定 IP は手動ファイル extra.conf に必ず入れます

このため、利用者を「国内スマホ回線から直接」に限定できるサービス以外は、国別割当(--country JP)で始めてから絞る方が現実的です。以下は 4 社版を主線に書き、国別版への切替は同じ手順で済むようにしてあります。

全体像

判定は nginx の HTTP 層で行います。TLS ハンドシェイクを終えてからの判定なので CPU は消費しますが、設定・切り戻し・観測が nginx だけで完結します。

図1 判定の流れ。送信元 IP → キャリア名(geo)→ 免除パス(map)→ モード(map)→ 遮断(if + return)の順に変数を連鎖させる。

図2 更新の流れ。取得失敗・件数不足・構文エラーのどれでも前回のリストを使い続け、空のリストで全遮断しないようにしてある。

手順1:リスト生成スクリプトの設置と初回生成

付録の gen_nginx_jp_allowlist.py(Python 3.6 以上、標準ライブラリのみ)が AS 番号から帯域を取得し、nginx の geo ブロックで include できる形式(1.66.96.0/21 docomo;)に整形します。

# 付録のファイルを bin/ nginx/ systemd/ の構成で保存したディレクトリで実行する
cd nginx-jp-allowlist
install -m 755 bin/gen_nginx_jp_allowlist.py bin/update_jp_allowlist.sh /usr/local/sbin/
mkdir -p /etc/nginx/geo /etc/nginx/snippets
install -m 644 nginx/geo/extra.conf nginx/geo/mode.conf nginx/geo/carrier_published.txt /etc/nginx/geo/
install -m 644 nginx/snippets/jp-allowlist.conf  /etc/nginx/snippets/
install -m 644 nginx/conf.d/00-jp-allowlist.conf /etc/nginx/conf.d/
install -m 644 systemd/jp-allowlist.service systemd/jp-allowlist.timer /etc/systemd/system/

1-1. 自分の管理用 IP を extra.conf に書く

# /etc/nginx/geo/extra.conf — 手動管理の許可帯域("CIDR ラベル;")
203.0.113.10/32   admin;      # 事務所の固定IP
2001:db8::/32     admin;      # 同 IPv6
198.51.100.0/24   monitor;    # 外形監視サービスの送信元

これを忘れたまま遮断モードに入ると、自分がサーバから締め出されます。現在の送信元はサーバのアクセスログか curl -s https://ifconfig.me で確認できます。固定 IP がない場合は「限界と補完策」の Basic 認証フォールバックを併用してください。

1-2. まず画面に出して中身を見る

python3 /usr/local/sbin/gen_nginx_jp_allowlist.py \
    --asn 9605:docomo --asn 2516:kddi --asn 17676:softbank --asn 17505:softbank \
    --asn 138384:rakuten --asn 23720:rakuten \
    --extra /etc/nginx/geo/carrier_published.txt --min-prefixes 500 | head -20
# 外部に出られるのが GitHub だけなら  --asn-source ipverse  を足す

stderr に「取得元: AS9605(docomo)=417; …」「出力件数: 812 (docomo=404, …)」のように件数が出ます。これを基準に --min-prefixes を 6 割程度(500)に置きます。取得元の障害で件数が減ったときに、空に近いリストで全遮断しないための下限です。

1-3. 本番ファイルを生成する

/usr/local/sbin/update_jp_allowlist.sh      # 生成 → nginx -t → 変更があれば reload
head -5 /etc/nginx/geo/jp_mobile.conf        # 先頭コメントに生成日時・取得元・件数
restorecon -Rv /etc/nginx/geo                 # SELinux 有効なら念のため

対象を変えるときは update_jp_allowlist.sh 内の ARGS を編集します。出力ファイル名は同じなので nginx 側の設定は変わりません。

やりたいこと ARGS に足す・変える MIN_PREFIXES の目安
キャリア4社 + 公式ページ帯域(既定) --asn … 6 件 + --extra carrier_published.txt 500
iPhone のプライベートリレー利用者も通す --icloud-relay を追加 500
日本国内全体(MVNO・自宅 Wi-Fi も通す) --country JP(--asn は不要) 2500
GitHub ミラーから取得 --asn-source ipverse / --country-source ipverse 同上

スクリプトの安全装置は 3 つです。取得失敗・0 件・件数不足のときは既存ファイルを触らず終了コード 2 で終わる。出力は一時ファイルに書いてから rename するので、書きかけのファイルを nginx が読むことはない。既存ファイルとの差分(追加/削除件数)を stderr に出す。

手順2:nginx 設定

設定は http コンテキストの変数定義(1 ファイル)と、保護する server ブロックへの 2 行追加だけです。変数にしておくことで、ログに判定結果を残せ、遮断しない「観測モード」が作れます。

2-1. http コンテキスト(/etc/nginx/conf.d/00-jp-allowlist.conf)

# 送信元 IP → キャリア名。リスト外は 0。最長一致。
geo $jp_carrier {
    default 0;
    include /etc/nginx/geo/jp_mobile.conf;   # スクリプト生成(AS 自動取得 + carrier_published.txt)
    include /etc/nginx/geo/extra.conf;       # 手動管理(管理用固定IP・監視元)
}

# 遮断してはいけないパス(証明書更新・死活監視)
map $uri $jp_exempt {
    default                            0;
    ~^/\.well-known/acme-challenge/    1;   # Let's Encrypt HTTP-01
    ~^/healthz$                        1;   # LB ヘルスチェック
}

# 「リスト外」かつ「免除でない」なら 1
map "$jp_carrier:$jp_exempt" $jp_block {
    default 0;
    "0:0"   1;
}

# 観測/遮断の切替(別ファイル。切替は 1 語の置換だけ)
include /etc/nginx/geo/mode.conf;

# 観測用ログ形式:末尾に carrier= block= deny= を足す
log_format jp_allowlist '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                        'carrier=$jp_carrier block=$jp_block deny=$jp_deny';
# /etc/nginx/geo/mode.conf
#   観測モード : default 0;           遮断モード : default $jp_block;
map $jp_block $jp_deny {
    default 0;
}

2-2. server ブロックへの組み込み(保護する server ごとに 2 行)

server {
    listen 443 ssl;
    listen [::]:443 ssl;          # IPv6 でも待ち受ける(docomo は IPv6 シングルスタック)
    server_name example.jp;

    include /etc/nginx/snippets/jp-allowlist.conf;      # ← 追加(server 直下・最上部)
    access_log /var/log/nginx/access.log jp_allowlist;  # ← 追加(観測用ログ形式)
    # ... 既存の設定 ...
}
# /etc/nginx/snippets/jp-allowlist.conf
if ($jp_deny) {
    return 444;    # 応答を返さず切断。403 を返したければ return 403;
}

設計上の要点です。

  • geo はラジックス木で最長一致するので、数千行でも起動・判定ともに負荷は無視できます(3,872 行の国別リストで nginx -t 0.01 秒)。同じ CIDR が重複すると warn が出ますが起動はします
  • ファイルの「値」(docomo / kddi / softbank / rakuten / jp / admin)がそのまま $jp_carrier に入るので、ログでどのキャリアから来たかが見えます
  • if は server 直下で return だけを書く安全な使い方に限定しています。location 内で他のディレクティブと混ぜないこと
  • return 444 はスキャナに何も返さず切るので、帯域とログ量を節約でき、nginx であることも見せません。利用者向けに説明ページを出したい場合は 403 にします
  • docomo は 2022年2月から IPv6 シングルスタックです。サーバが IPv6 で待ち受けていれば 240a:61:… から、IPv4 のみなら NAT64 経由で spモードの IPv4 帯域から来ます。IPv6 帯域をリストから落とさないこと
  • より単純な allow / deny 方式も選べます(スクリプトの --format allow)。生成ファイルを server か location で include するだけですが、観測モードとキャリア別ログが作れません

手順3:前段にプロキシ・CDN・LB がある場合の実 IP 取得

nginx が直接インターネットに面しているならこの手順は不要です。ALB / CloudFront / Cloudflare / 社内 LB などが前にあると $remote_addr はその装置の IP になり、許可リストは「全部遮断」か、装置の IP を extra.conf に入れてしまえば「全部許可」になります。realip モジュールで $remote_addr を実クライアントの IP に置き換えてから geo に渡します。

# /etc/nginx/conf.d/00-jp-allowlist.conf の先頭(コメントを外す)
set_real_ip_from  10.0.0.0/8;          # 信頼する前段の IP だけ(ALB のサブネット、LB のアドレス など)
set_real_ip_from  127.0.0.1;           # 同一ホストのリバースプロキシ経由の場合
real_ip_header    X-Forwarded-For;     # Cloudflare は CF-Connecting-IP
real_ip_recursive on;                  # 多段プロキシのとき、信頼済みを右から飛ばして最後の信頼外アドレスを採る
前段 real_ip_header set_real_ip_from に入れるもの
AWS ALB / NLB(プロキシ) X-Forwarded-For ALB を置いた VPC サブネット
CloudFront X-Forwarded-For AWS 公開の ip-ranges.json で service=CLOUDFRONT の帯域
Cloudflare CF-Connecting-IP Cloudflare 公開の IP 一覧
社内 LB / 同一ホストのプロキシ X-Forwarded-For(LB の仕様に従う) LB のアドレス、127.0.0.1

set_real_ip_from は必ず前段のアドレスに限定します。0.0.0.0/0 のように広げると、誰でも X-Forwarded-For: 1.66.96.1 を付けて素通りできます。信頼外の送信元からのヘッダは nginx が無視するので、オリジンへの直接接続はそのまま送信元 IP で判定されます。realip はアクセスログの $remote_addr も置き換えるので、設定後にログの先頭がプロキシの IP ではなく利用者の IP になっていることを確認してください。

手順4:観測モードで検証してから遮断モードへ切替

最初は遮断せずにログだけ取り、「遮断されるはずの通信」に自分と正規利用者が含まれていないことを確かめてから切り替えます。切替は mode.conf の 1 語と reload だけなので、戻すのも同じ手順で済みます。

  1. 観測モードで反映する(配布時点の mode.conf は default 0; = 観測)
    nginx -t && systemctl reload nginx
    
  2. 半日〜数日分のログを集計する。block=1 の行が遮断モードで落ちる通信
    # 遮断されるはずの送信元 上位 30
    grep ' block=1 ' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -30
    # 通る通信のキャリア別内訳
    grep -o 'carrier=[a-z0-9_]*' /var/log/nginx/access.log | sort | uniq -c | sort -rn
    # 自分の IP が block=1 になっていないか
    grep '^203.0.113.10 ' /var/log/nginx/access.log | grep -c ' block=1 '
    
  3. 追加すべき送信元(自分、監視、連携先システム、正規利用者の固定回線)を extra.conf に足して reload。免除パスが必要なら map $uri $jp_exempt に足す
  4. 遮断モードに切り替える
    sed -i 's/^    default 0;$/    default $jp_block;/' /etc/nginx/geo/mode.conf
    nginx -t && systemctl reload nginx
    
  5. 切替直後に、スマホのモバイル通信からサイトが開けること、PC の Wi-Fi からは切断されること、管理用 IP からは開けることを確認する。ログでは deny=1 かつ $status 444 の行が遮断実績
  6. 切り戻しは default $jp_block; を default 0; に戻して reload。締め出された場合は VM のコンソールか SSH(nginx の対象外なので影響しない)から同じ操作をする

本番を触る前に試すなら、テスト用の server ブロックで set_real_ip_from 127.0.0.1; と real_ip_header X-Forwarded-For; を有効にすると、curl -H 'X-Forwarded-For: 8.8.8.8' http://127.0.0.1/ のように任意の送信元を装って判定を確認できます。本記事の「検証記録」はこの方法で取りました。確認後は必ず外してください。

手順5:定期更新(systemd timer)と安全装置

キャリアの帯域は月単位で増えます。docomo の公開ページには 2024〜2026 年に 12 回の追加履歴があり、2027 年 2 月分までの追加予定も載っています。日次で再生成し、変更があり構文検証に通ったときだけ reload します。

systemctl daemon-reload
systemctl enable --now jp-allowlist.timer
systemctl list-timers jp-allowlist.timer          # 次回実行時刻の確認
systemctl start jp-allowlist.service && journalctl -u jp-allowlist.service -n 20   # 手動実行とログ
状況 振る舞い 終了コード
取得成功・内容に変更なし 何もしない(「no change」) 0
取得成功・変更あり・nginx -t 成功 ファイル更新 → systemctl reload nginx 0
取得失敗・0 件・件数が MIN_PREFIXES 未満 ファイルを書き換えず終了。前回リストを継続 2
取得成功・nginx -t 失敗 jp_mobile.conf.prev から巻き戻して終了。reload しない 1

運用上の注意です。

  • MIN_PREFIXES は初回の出力件数の 6 割程度に置く。キャリアが帯域を大きく集約して件数が減ったときは、journal の「generation failed」を見て手動で下げる
  • 終了コード 1 / 2 を監視する(systemctl --failed や既存の監視で jp-allowlist.service の failed を拾う)。数週間更新されないと、新しい帯域の利用者が静かに締め出される
  • タイマーは 04:17 + 最大 20 分のランダム遅延。公開 API への同時刻集中を避けるため
  • プロキシ経由で外に出るサーバでは service ファイルの Environment=HTTPS_PROXY= を有効にする。GitHub しか通らない場合は ARGS に --asn-source ipverse を足す
  • jp_mobile.conf は構成管理(Ansible 等)の配布対象から外す。サーバ側で日次生成されるファイルなので、配布すると上書き合戦になる

限界と補完策

IP 許可リストは「国外からの無差別スキャンを消す」対策であって、標的型の攻撃や国内踏み台からの攻撃は止めません。

通ってしまうもの 理由 補完策
国内の踏み台(侵害された家庭用ルータ・IoT、国内 VPS) キャリア・国内の帯域そのもの limit_req / limit_conn によるレート制限、fail2ban、認証の強化(パスキー・MFA)
国内レジデンシャルプロキシ・スマホ回線を使った攻撃 本物のキャリア帯域から来る 同上。アプリ層の対策を省けない
キャリア CGNAT の共有 IP を 1 件ひっくると左右される正規利用者 スマホの IPv4 は NAT で大勢が同じ IP を共有 キャリア帯域の個別 IP を長期的に deny しない。fail2ban の bantime は短めに

通らなくなる正規利用者への救済策です。

  • Basic 認証フォールバック:satisfy any; と allow/deny 方式を組み合わせると、リスト内はそのまま、リスト外はパスワードで通せます。管理者が出先から入る用途に向きます
    location /admin/ {
        satisfy any;
        include /etc/nginx/geo/jp_mobile_allow.conf;   # --format allow で生成(末尾 deny all;)
        auth_basic           "restricted";
        auth_basic_user_file /etc/nginx/.htpasswd;
    }
    
  • 国別リストへの切替:自宅 Wi-Fi や MVNO の利用者から苦情が出たら --country JP に変えます。国外スキャンを消す効果はほぼ同じです
  • iCloud プライベートリレー:--icloud-relay を足します

より前段で落とす選択肢もあります。nginx での判定は TLS ハンドシェイクを終えてからになるので、大量の接続で CPU が足りないときは、スクリプトの --format plain --family 4(IPv6 は --family 6)で CIDR だけのリストを出し、firewalld の ipset(firewall-cmd --permanent --new-ipset=jp4 --type=hash:net → --ipset=jp4 --add-entries-from-file=)か nftables の set で 443/tcp をカーネルで落とします。ただしカーネル層では観測モードとパス単位の免除が作りにくいので、まず nginx で許可リストを落ち着かせ、負荷が問題になってから下げる順が安全です。

検証記録(2026-10-09、nginx 1.24.0)

本記事の設定とスクリプトは、nginx 1.24.0 を別ポートで起動し、set_real_ip_from 127.0.0.1 + X-Forwarded-For で送信元を装って動作を確認しました。許可リストは GitHub ミラー(ipverse)から取得した当日の実データ(4 社 6AS で 825 件、公式公開帯域を足して集約した版で 812 件)です。

装った送信元 意味 観測モード 遮断モード
1.66.96.1 docomo spモード IPv4 200(carrier=docomo block=0) 200
240a:61:8000::1 docomo IPv6 200(carrier=docomo) 200
49.97.1.1 / 202.229.178.40 docomo 公開ページのみに載る帯域(carrier_published.txt 経由) 200(carrier=docomo) 200
27.81.160.1 / 2001:268:7200::1 au IPv4 / IPv6 200(carrier=kddi) 200
126.0.0.1 SoftBank 200(carrier=softbank) 200
101.102.0.1 楽天モバイル 200(carrier=rakuten) 200
203.0.113.10 extra.conf の admin 200(carrier=admin) 200
8.8.8.8 / 2001:4860:4860::8888 国外 200(carrier=0 block=1 deny=0) 444(応答なし、curl 終了コード 52)
8.8.8.8 → /.well-known/acme-challenge/x 国外から免除パス 200(block=0) 200
127.0.0.1(ヘッダなし) リスト外の直接接続 200(block=1) 444

そのほかに確認したことです。

  • スクリプト:集約(1.66.104.0/23 + /24 → /23)、ラベルをまたぐ重複の先勝ち、APNIC 委譲ファイルの非 2 のべき乗サイズ(3072 アドレス → /21 + /22)、reserved 行と他国の除外、iCloud CSV の JP 行抽出、--min-prefixes 未満・API エラー・到達不能の各ケースで既存ファイルが保たれること
  • update_jp_allowlist.sh:変更なし・変更あり reload・生成失敗・nginx -t 失敗時の巻き戻しの 4 経路
  • 3,872 行の国別リストを geo に入れても nginx -t は 0.01 秒。include ファイルの構文エラーは nginx -t が行番号付きで検出する

未検証の点が 2 つあります。RIPEstat API・APNIC FTP・Apple の CSV への実通信は作成環境から到達できず、応答形式を模したデータで確認しました(GitHub ミラー経由は実通信で確認済み)。carrier_published.txt の内容は各社ページからの転記で、転記ミスの可能性があります※。いずれも初回は手順1の 1-2 のとおり画面出力で件数と内容を確かめてから本番ファイルを作ってください。

参考資料

付録:AS 番号の対応

BGP の公開情報によるもので、キャリアが「スマホ向け」と定義したものではありません。

AS 組織 備考
AS9605 NTT DOCOMO, INC. spモード・ahamo(公開ページに明記)
AS2516 KDDI CORPORATION au・UQ mobile・povo※。固定回線・法人も含む
AS17676 / AS17505 SoftBank Corp. SoftBank・Y!mobile・LINEMO※。固定回線も含む
AS138384 / AS23720 Rakuten Mobile Network, Inc. 楽天モバイル

付録:スクリプトの主なオプション

オプション 意味
--asn N[:ラベル] AS 番号(複数可)。ラベルが $jp_carrier の値になる
--asn-source ripestat または ipverse AS 広告帯域の取得元(既定 ripestat)
--country CC / --country-source apnic または ipverse 国別割当全体(例 JP)
--icloud-relay [CC] iCloud Private Relay の出口 IP を追加(既定 JP)
--extra FILE 手動追記ファイル(CIDR ラベル を 1 行 1 件、# はコメント)
--format geo allow plain 出力形式。geo(既定)/allow + deny all/CIDR のみ
--family 4 6 all 出力するアドレスファミリ
--min-prefixes N 件数が N 未満なら書き換えずに終了コード 2
--out PATH 出力先(省略時は標準出力)。既存ファイルとの差分を表示

付録:ファイル全文

Qiita にはファイルを添付できないため、全文を載せます。見出しの配置先パスに同じ内容で保存すれば、手順1の install コマンドどおりに配置できます(付録内の相対パスは nginx-jp-allowlist/ 直下からのもの)。長いものは折りたたんであります。

bin/gen_nginx_jp_allowlist.py → /usr/local/sbin/gen_nginx_jp_allowlist.py

許可リスト生成スクリプト。AS 番号(RIPEstat / GitHub ミラー)、国別割当(APNIC / GitHub ミラー)、iCloud Private Relay CSV、手動ファイルから nginx の geo / allow / plain 形式を生成する。

bin/gen_nginx_jp_allowlist.py 全文(クリックで展開)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gen_nginx_jp_allowlist.py
  国内モバイルキャリア(AS番号ベース)または日本国内(国別割当ベース)の
  IPアドレス帯域を取得し、nginx の geo / allow 用 include ファイルを生成する。

依存: Python 3.6 以上(標準ライブラリのみ。AlmaLinux/RHEL 同梱の python3 で動作)

取得元(いずれも公開データ。--asn-source / --country-source で切替)
  ripestat : RIPE NCC の RIPEstat API。AS が BGP で広告中のプレフィックス(最新・未集約)
  ipverse  : GitHub (ipverse/asn-ip, ipverse/rir-ip) の日次更新ミラー(集約済み)。
             社内プロキシで GitHub しか通らない環境向け
  apnic    : APNIC 委譲ファイル delegated-apnic-latest(国コード別の割当。--country 用)

使い方:
  # 方式A: 4キャリアの AS 番号から取得(RIPEstat)
  gen_nginx_jp_allowlist.py \
      --asn 9605:docomo --asn 2516:kddi --asn 17676:softbank --asn 17505:softbank \
      --asn 138384:rakuten --asn 23720:rakuten \
      --extra /etc/nginx/geo/extra.txt --min-prefixes 500 \
      --out /etc/nginx/geo/jp_mobile.conf

  # 方式A' : 同上を GitHub ミラーから取得
  gen_nginx_jp_allowlist.py --asn-source ipverse --asn 9605:docomo ... --out ...

  # 方式B: 日本 (JP) に割り当てられたアドレス全体(APNIC 委譲ファイル)
  gen_nginx_jp_allowlist.py --country JP --min-prefixes 2500 --out /etc/nginx/geo/jp_all.conf

  # iCloud Private Relay の日本向け出口 IP を併せて許可(Safari + iCloud+ 利用者の取りこぼし対策)
  gen_nginx_jp_allowlist.py --asn ... --icloud-relay --out ...

  # 動作確認(画面出力のみ)
  gen_nginx_jp_allowlist.py --asn-source ipverse --asn 138384:rakuten --min-prefixes 1

出力形式 (--format):
  geo    (既定) "1.66.96.0/21 docomo;"  → nginx の geo ブロック内で include する
  allow         "allow 1.66.96.0/21;" ... 末尾に "deny all;" → server/location 内で include する
  plain         CIDR を 1 行 1 件(firewalld の ipset 等に流用。--family 4|6 で分離可)

--extra FILE の書式:
  1 行 1 件 "CIDR [ラベル]"。'#' 以降はコメント。
  例)  203.0.113.10/32 admin      # 管理用固定IP
       2001:db8::/32   monitor

安全装置:
  --min-prefixes N  取得できたプレフィックス総数が N 未満なら、既存ファイルを書き換えずに
                    終了コード 2 で終了する(API 障害時に空リストで全遮断しないため)。
  出力は一時ファイルに書いてから rename するため、生成途中の壊れたファイルが残らない。
  --out 指定時は、既存ファイルとの差分(追加/削除件数)を stderr に表示する。

終了コード: 0 成功 / 2 失敗(取得失敗・件数不足・引数エラー)
"""

import argparse
import csv
import io
import ipaddress
import json
import os
import re
import sys
import tempfile
import time
import urllib.error
import urllib.request
from collections import OrderedDict

URL_RIPESTAT = ("https://stat.ripe.net/data/announced-prefixes/data.json"
                "?resource=AS{asn}&sourceapp=nginx-jp-allowlist")
URL_IPVERSE_ASN = "https://raw.githubusercontent.com/ipverse/asn-ip/master/as/{asn}/{family}-aggregated.txt"
URL_IPVERSE_CC = "https://raw.githubusercontent.com/ipverse/rir-ip/master/country/{cc}/{family}-aggregated.txt"
URL_APNIC = "https://ftp.apnic.net/stats/apnic/delegated-apnic-latest"
URL_ICLOUD = "https://mask-api.icloud.com/egress-ip-ranges.csv"
USER_AGENT = "nginx-jp-allowlist/1.1 (python-urllib)"


def log(msg):
    sys.stderr.write(msg + "\n")


def fail(msg, code=2):
    log("ERROR: " + msg)
    sys.exit(code)


def fetch(url, timeout=120):
    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return resp.read().decode("utf-8", "replace")


def sanitize_label(label):
    label = re.sub(r"[^A-Za-z0-9_]", "_", label.strip())
    return label or "x"


def parse_network(text):
    return ipaddress.ip_network(text.strip(), strict=False)


def parse_cidr_lines(text):
    """'# コメント' と空行を除き、1 行 1 CIDR として解釈"""
    nets = []
    for line in text.splitlines():
        line = line.split("#", 1)[0].strip()
        if line:
            nets.append(parse_network(line))
    return nets


# ---------------------------------------------------------------- sources

def from_ripestat(asn, template):
    """RIPEstat announced-prefixes API から AS が広告中のプレフィックスを取得"""
    data = json.loads(fetch(template.format(asn=asn)))
    if data.get("status", "ok") != "ok":
        raise RuntimeError("RIPEstat status=%s messages=%s"
                           % (data.get("status"), data.get("messages")))
    prefixes = data.get("data", {}).get("prefixes", [])
    return [parse_network(p["prefix"]) for p in prefixes if p.get("prefix")]


def from_ipverse_asn(asn, template):
    """GitHub ipverse/asn-ip の集約済みリスト(IPv4/IPv6 別ファイル)を取得"""
    nets = []
    for family in ("ipv4", "ipv6"):
        try:
            nets.extend(parse_cidr_lines(fetch(template.format(asn=asn, family=family))))
        except urllib.error.HTTPError as e:
            if e.code == 404:          # その AS に当該ファミリの広告が無い場合は 404
                continue
            raise
    return nets


def from_ipverse_country(cc, template):
    nets = []
    for family in ("ipv4", "ipv6"):
        try:
            nets.extend(parse_cidr_lines(fetch(template.format(cc=cc.lower(), family=family))))
        except urllib.error.HTTPError as e:
            if e.code == 404:
                continue
            raise
    return nets


def from_apnic(url, cc):
    """APNIC 委譲ファイルから国コード cc の allocated/assigned を取得"""
    nets = []
    for line in fetch(url).splitlines():
        if not line or line.startswith("#"):
            continue
        f = line.split("|")
        if len(f) < 7 or f[1] != cc:
            continue
        typ, start, value, status = f[2], f[3], f[4], f[6]
        if status not in ("allocated", "assigned"):
            continue
        if typ == "ipv4":
            first = ipaddress.IPv4Address(start)
            last = first + (int(value) - 1)          # value = アドレス数(2のべき乗とは限らない)
            nets.extend(ipaddress.summarize_address_range(first, last))
        elif typ == "ipv6":
            nets.append(ipaddress.IPv6Network("%s/%s" % (start, value), strict=False))
    return nets


def from_icloud(url, cc):
    """Apple 公開の iCloud Private Relay 出口 IP (CSV) から国コード cc の行を取得"""
    nets = []
    for row in csv.reader(io.StringIO(fetch(url))):
        if not row or not row[0].strip() or row[0].startswith("#"):
            continue
        if len(row) >= 2 and row[1].strip().upper() == cc:
            nets.append(parse_network(row[0]))
    return nets


def from_extra(path):
    """手動追記ファイル: 'CIDR [label]' を 1 行 1 件"""
    items = []
    with open(path, encoding="utf-8") as fh:
        for lineno, raw in enumerate(fh, 1):
            text = raw.split("#", 1)[0].strip()
            if not text:
                continue
            parts = text.split()
            try:
                net = parse_network(parts[0])
            except ValueError as e:
                raise ValueError("%s:%d: %s" % (path, lineno, e))
            label = sanitize_label(parts[1]) if len(parts) > 1 else "extra"
            items.append((net, label))
    return items


# ---------------------------------------------------------------- processing

def collapse(nets):
    v4 = [n for n in nets if n.version == 4]
    v6 = [n for n in nets if n.version == 6]
    return list(ipaddress.collapse_addresses(v4)) + list(ipaddress.collapse_addresses(v6))


def read_existing(path):
    """既存の出力ファイルから CIDR 集合を読む(差分表示用)"""
    found = set()
    try:
        with open(path, encoding="utf-8") as fh:
            for raw in fh:
                text = raw.split("#", 1)[0].strip().rstrip(";").strip()
                if not text:
                    continue
                parts = text.split()
                cand = parts[1] if parts[0] in ("allow", "deny") and len(parts) > 1 else parts[0]
                try:
                    found.add(parse_network(cand).with_prefixlen)
                except ValueError:
                    pass
    except FileNotFoundError:
        return None
    return found


def render(rows, fmt, deny_all, header_lines):
    out = []
    for h in header_lines:
        out.append("# " + h)
    if fmt == "geo":
        for net, label in rows:
            out.append("%s %s;" % (net.with_prefixlen, label))
    elif fmt == "allow":
        for net, label in rows:
            out.append("allow %s;  # %s" % (net.with_prefixlen, label))
        if deny_all:
            out.append("deny all;")
    else:  # plain
        for net, _label in rows:
            out.append(net.with_prefixlen)
    return "\n".join(out) + "\n"


def write_atomic(path, text):
    directory = os.path.dirname(os.path.abspath(path)) or "."
    fd, tmp = tempfile.mkstemp(prefix=".jp_allowlist.", dir=directory)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            fh.write(text)
        os.chmod(tmp, 0o644)
        os.replace(tmp, path)
    except Exception:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


# ---------------------------------------------------------------- main

def main():
    ap = argparse.ArgumentParser(
        description="nginx 用の国内モバイル/日本国内 IP 許可リスト生成",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__)
    ap.add_argument("--asn", action="append", default=[], metavar="ASN[:LABEL]",
                    help="広告プレフィックスを取得する AS 番号(複数指定可)例: 9605:docomo")
    ap.add_argument("--asn-source", choices=["ripestat", "ipverse"], default="ripestat",
                    help="--asn の取得元(既定 ripestat)")
    ap.add_argument("--country", metavar="CC",
                    help="国コード(例 JP)に割り当てられたアドレス全体を取得")
    ap.add_argument("--country-source", choices=["apnic", "ipverse"], default="apnic",
                    help="--country の取得元(既定 apnic)")
    ap.add_argument("--icloud-relay", nargs="?", const="JP", metavar="CC",
                    help="iCloud Private Relay の出口 IP を追加(国コード省略時 JP)")
    ap.add_argument("--extra", action="append", default=[], metavar="FILE",
                    help="手動追記ファイル(複数指定可)")
    ap.add_argument("--format", choices=["geo", "allow", "plain"], default="geo")
    ap.add_argument("--family", choices=["4", "6", "all"], default="all",
                    help="出力するアドレスファミリ(既定 all)")
    ap.add_argument("--no-deny-all", action="store_true",
                    help="--format allow で末尾の 'deny all;' を出力しない")
    ap.add_argument("--out", metavar="PATH", help="出力先(省略時は標準出力)")
    ap.add_argument("--min-prefixes", type=int, default=50, metavar="N",
                    help="プレフィックス総数が N 未満なら書き換えずに異常終了(既定 50)")
    ap.add_argument("--ripestat-url", default=URL_RIPESTAT, help="{asn} を置換する URL テンプレート")
    ap.add_argument("--ipverse-asn-url", default=URL_IPVERSE_ASN, help="{asn},{family} を置換")
    ap.add_argument("--ipverse-country-url", default=URL_IPVERSE_CC, help="{cc},{family} を置換")
    ap.add_argument("--apnic-url", default=URL_APNIC)
    ap.add_argument("--icloud-url", default=URL_ICLOUD)
    args = ap.parse_args()

    if not (args.asn or args.country or args.icloud_relay or args.extra):
        ap.error("--asn / --country / --icloud-relay / --extra のいずれかを指定してください")

    started = time.strftime("%Y-%m-%dT%H:%M:%S%z")
    groups = OrderedDict()        # label -> [networks]
    sources = []                  # ヘッダ表示用

    def add(label, nets):
        groups.setdefault(label, []).extend(nets)

    for item in args.asn:
        asn, _sep, label = item.partition(":")
        asn = re.sub(r"(?i)^AS", "", asn.strip())
        if not asn.isdigit():
            ap.error("AS 番号の形式が不正です: %s" % item)
        label = sanitize_label(label or "as" + asn)
        try:
            if args.asn_source == "ripestat":
                nets = from_ripestat(asn, args.ripestat_url)
            else:
                nets = from_ipverse_asn(asn, args.ipverse_asn_url)
        except Exception as e:
            fail("AS%s の取得に失敗 (%s): %s" % (asn, args.asn_source, e))
        if not nets:
            fail("AS%s の広告プレフィックスが 0 件でした(取得元の応答を確認してください)" % asn)
        add(label, nets)
        sources.append("AS%s(%s)=%d" % (asn, label, len(nets)))

    if args.country:
        cc = args.country.upper()
        try:
            if args.country_source == "apnic":
                nets = from_apnic(args.apnic_url, cc)
            else:
                nets = from_ipverse_country(cc, args.ipverse_country_url)
        except Exception as e:
            fail("国別リスト %s の取得に失敗 (%s): %s" % (cc, args.country_source, e))
        if not nets:
            fail("国コード %s の割当が見つかりません (%s)" % (cc, args.country_source))
        add(sanitize_label(cc.lower()), nets)
        sources.append("%s(%s)=%d" % (cc, args.country_source, len(nets)))

    if args.icloud_relay:
        cc = args.icloud_relay.upper()
        try:
            nets = from_icloud(args.icloud_url, cc)
        except Exception as e:
            fail("iCloud Private Relay 出口 IP の取得に失敗: %s" % e)
        if not nets:
            fail("iCloud Private Relay CSV に %s の行が見つかりません" % cc)
        add("icloud_relay", nets)
        sources.append("iCloudRelay(%s)=%d" % (cc, len(nets)))

    for path in args.extra:
        try:
            items = from_extra(path)
        except (OSError, ValueError) as e:
            fail("--extra の読み込みに失敗: %s" % e)
        for net, label in items:
            add(label, [net])
        sources.append("extra:%s=%d" % (os.path.basename(path), len(items)))

    # ラベルごとに集約し、完全一致の重複は先勝ちで除去(ラベルをまたぐ包含関係は geo の最長一致に任せる)
    entries = OrderedDict()
    for label, nets in groups.items():
        for net in collapse(nets):
            entries.setdefault(net.with_prefixlen, (net, label))
    rows = sorted(entries.values(), key=lambda t: (t[0].version, t[0]))
    if args.family != "all":
        rows = [r for r in rows if str(r[0].version) == args.family]

    per_label = OrderedDict()
    for _net, label in rows:
        per_label[label] = per_label.get(label, 0) + 1
    summary = ", ".join("%s=%d" % kv for kv in per_label.items())
    log("取得元: %s" % "; ".join(sources))
    log("出力件数: %d (%s)" % (len(rows), summary))

    if len(rows) < args.min_prefixes:
        fail("プレフィックス数 %d が --min-prefixes %d を下回ったため出力しません"
             % (len(rows), args.min_prefixes))

    header = [
        "generated by gen_nginx_jp_allowlist.py at %s" % started,
        "sources: %s" % "; ".join(sources),
        "entries: %d (%s)" % (len(rows), summary),
    ]
    text = render(rows, args.format, not args.no_deny_all, header)

    if not args.out:
        sys.stdout.write(text)
        return

    previous = read_existing(args.out)
    if previous is not None:
        current = set(net.with_prefixlen for net, _ in rows)
        log("既存ファイルとの差分: 追加 %d / 削除 %d" % (len(current - previous), len(previous - current)))
    write_atomic(args.out, text)
    log("書き込み完了: %s" % args.out)


if __name__ == "__main__":
    main()

bin/update_jp_allowlist.sh → /usr/local/sbin/update_jp_allowlist.sh

日次更新ラッパー。再生成 → 差分確認 → nginx -t → reload。失敗時は前回リストを維持、構文エラー時は巻き戻す。

#!/bin/bash
# /usr/local/sbin/update_jp_allowlist.sh
# 許可リストを再生成し、変更があり、かつ nginx -t を通過した場合だけ reload する。
# 生成失敗・件数不足・構文エラーのいずれでも、直前のリストをそのまま使い続ける。
set -euo pipefail

GEN=${GEN:-/usr/local/sbin/gen_nginx_jp_allowlist.py}
OUT=${OUT:-/etc/nginx/geo/jp_mobile.conf}
NGINX_BIN=${NGINX_BIN:-nginx}
RELOAD=${RELOAD:-"systemctl reload nginx"}
MIN_PREFIXES=${MIN_PREFIXES:-500}        # 初回実行時の出力件数の 6 割程度を目安に調整する

# 取得対象。--asn-source ipverse にすると GitHub ミラーから取得(プロキシ環境向け)
ARGS=(
  --asn 9605:docomo
  --asn 2516:kddi
  --asn 17676:softbank --asn 17505:softbank
  --asn 138384:rakuten --asn 23720:rakuten
  --extra /etc/nginx/geo/carrier_published.txt   # キャリア公式ページの掲載帯域で AS 取得分を補完
  # --icloud-relay          # Safari + iCloud+ (Private Relay) 利用者も通す場合
  # --country JP            # 国内全体に広げる場合(MVNO・自宅Wi-Fi も通る)
)

BAK="$OUT.prev"
[ -f "$OUT" ] && cp -p "$OUT" "$BAK"

if ! python3 "$GEN" "${ARGS[@]}" --min-prefixes "$MIN_PREFIXES" --out "$OUT"; then
    echo "jp-allowlist: generation failed; previous list kept" >&2
    exit 2
fi

if [ -f "$BAK" ] && cmp -s <(grep -v '^#' "$OUT") <(grep -v '^#' "$BAK"); then
    echo "jp-allowlist: no change"
    exit 0
fi

if $NGINX_BIN -t; then
    $RELOAD
    echo "jp-allowlist: updated and reloaded"
else
    echo "jp-allowlist: nginx -t failed; rolling back" >&2
    [ -f "$BAK" ] && cp -p "$BAK" "$OUT"
    exit 1
fi

nginx/conf.d/00-jp-allowlist.conf → /etc/nginx/conf.d/00-jp-allowlist.conf

http コンテキストの変数定義(realip は前段がある場合のみコメントを外す)。

# /etc/nginx/conf.d/00-jp-allowlist.conf
# 国内モバイル/国内 IP 許可リスト(http コンテキスト)
# 他の conf.d/*.conf より先に読まれるよう "00-" を付けている(変数定義を先に済ませるため)。

# --- (1) 前段にリバースプロキシ/CDN/LB がある場合のみ有効化 -----------------
# 直接インターネットに面している場合はこのブロックは不要(コメントのまま)。
# 信頼する前段の IP だけを列挙すること。ここを広げすぎると X-Forwarded-For 偽装で素通りする。
#set_real_ip_from  10.0.0.0/8;          # 例: ALB / 内部 LB のサブネット
#set_real_ip_from  203.0.113.0/24;      # 例: 前段プロキシ
#real_ip_header    X-Forwarded-For;     # CloudFront/ALB は X-Forwarded-For、Cloudflare は CF-Connecting-IP
#real_ip_recursive on;                  # 多段プロキシのとき、信頼済みを右から飛ばして最後の信頼外アドレスを採る

# --- (2) 送信元 IP → キャリア名(または 0)--------------------------------------
# 最長一致。ファイル内の値(docomo / kddi / softbank / rakuten / jp / admin ...)がそのまま変数値になる。
geo $jp_carrier {
    default 0;
    include /etc/nginx/geo/jp_mobile.conf;   # スクリプト生成(AS 自動取得 + carrier_published.txt)
    include /etc/nginx/geo/extra.conf;       # 手動管理(管理用固定IP・監視元など)
}

# --- (3) 遮断免除パス(証明書更新・死活監視など)----------------------------------
map $uri $jp_exempt {
    default                            0;
    ~^/\.well-known/acme-challenge/    1;   # Let's Encrypt HTTP-01
    ~^/healthz$                        1;   # LB ヘルスチェック(使っている場合)
}

# --- (4) 「許可リスト外」かつ「免除パスでない」なら 1 ------------------------------
map "$jp_carrier:$jp_exempt" $jp_block {
    default 0;
    "0:0"   1;
}

# --- (5) 観測モード/遮断モードの切替 ---------------------------------------------
include /etc/nginx/geo/mode.conf;

# --- (6) ログ(観測モードで「遮断されるはずの通信」を確認するために使う)-------------
# $jp_carrier=0 かつ $jp_block=1 の行が、遮断モードで落とされる通信。
log_format jp_allowlist '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                        'carrier=$jp_carrier block=$jp_block deny=$jp_deny';

nginx/geo/mode.conf → /etc/nginx/geo/mode.conf

観測/遮断の切替。

# /etc/nginx/geo/mode.conf — 許可リストの動作モード
#   観測モード : default 0;           (ログに carrier/block を記録するだけで遮断しない)
#   遮断モード : default $jp_block;   (許可リスト外を遮断する)
# 切替後は  nginx -t && systemctl reload nginx
map $jp_block $jp_deny {
    default 0;
}

nginx/geo/extra.conf → /etc/nginx/geo/extra.conf

手動管理の許可帯域(必ず自分の管理用 IP を入れる)。

# /etc/nginx/geo/extra.conf — 手動管理の許可帯域(geo 形式: "CIDR ラベル;")
# 自分の管理用固定IP・社内プロキシ・外形監視サービスの送信元などを必ず入れておくこと。
# ここに自分の IP が無いまま遮断モードにすると自分が締め出される。
203.0.113.10/32   admin;      # 例: 自宅/事務所の固定IP
#2001:db8::/32    admin;      # 例: IPv6
#198.51.100.0/24  monitor;    # 例: 外形監視サービスの送信元

nginx/geo/carrier_published.txt → /etc/nginx/geo/carrier_published.txt

docomo / au の公式ページ掲載帯域(2026-10-09 転記※)。スクリプトの --extra に渡す。

nginx/geo/carrier_published.txt 全文(クリックで展開)
# /etc/nginx/geo/carrier_published.txt — キャリア公式ページ掲載のスマホ向け帯域(スクリプトの --extra 形式: "CIDR ラベル")
# AS 番号ベースの自動取得を補完するための固定リスト。許可にしか使わないので、古い行が残っても害は小さい。
# ※ 2026-10-09 に各社ページから筆者が転記。転記ミスの可能性があるため、本番投入前に原典と照合すること。
#   docomo: https://www.docomo.ne.jp/service/developer/smart_phone/spmode/index.html (「インターネットアクセス時」。ahamo・IoT NET も同帯域)
#   au    : https://www.au.com/developer/android/kaihatsu/network/ (LTE NET / 5G NET、2025年1月現在。for DATA は非公開)
#   SoftBank・楽天モバイル: スマホ向け帯域の現行公開ページが見当たらず未収録(AS 17676/17505、138384/23720 の自動取得に依存)

# ---- docomo spモード(掲載済み。IPv4 181 件・IPv6 28 件)----
1.66.96.0/21 docomo
1.66.104.0/23 docomo
1.72.0.0/21 docomo
1.72.8.0/23 docomo
1.72.10.0/24 docomo
1.73.0.0/19 docomo
1.73.32.0/22 docomo
1.73.128.0/24 docomo
1.73.129.0/24 docomo
1.73.130.0/24 docomo
1.73.131.0/24 docomo
1.73.132.0/24 docomo
1.73.133.0/24 docomo
1.73.134.0/24 docomo
1.73.135.0/24 docomo
1.73.136.0/24 docomo
1.73.137.0/24 docomo
1.73.138.0/24 docomo
1.73.139.0/24 docomo
1.73.140.0/24 docomo
1.73.141.0/24 docomo
1.73.142.0/24 docomo
1.73.143.0/24 docomo
1.73.144.0/24 docomo
1.73.145.0/24 docomo
1.73.146.0/24 docomo
1.73.147.0/24 docomo
1.73.148.0/24 docomo
1.73.149.0/24 docomo
1.73.150.0/24 docomo
1.73.151.0/24 docomo
1.73.152.0/24 docomo
1.73.153.0/24 docomo
1.73.154.0/24 docomo
1.73.155.0/24 docomo
1.75.0.0/21 docomo
1.75.8.0/22 docomo
1.75.12.0/23 docomo
1.75.14.0/24 docomo
1.75.16.0/20 docomo
1.75.152.0/21 docomo
1.75.196.0/22 docomo
1.75.202.0/23 docomo
1.75.204.0/22 docomo
1.75.208.0/21 docomo
1.75.216.0/21 docomo
1.75.224.0/19 docomo
1.78.0.0/19 docomo
1.78.32.0/21 docomo
1.78.40.0/22 docomo
1.78.64.0/19 docomo
1.78.96.0/22 docomo
1.79.6.0/23 docomo
1.79.8.0/21 docomo
1.79.16.0/21 docomo
1.79.24.0/22 docomo
1.79.28.0/23 docomo
1.79.32.0/21 docomo
1.79.66.0/23 docomo
1.79.68.0/22 docomo
1.79.72.0/21 docomo
1.79.80.0/20 docomo
1.79.96.0/21 docomo
1.79.176.0/21 docomo
27.230.95.0/24 docomo
27.230.96.0/23 docomo
49.96.0.0/18 docomo
49.96.216.0/21 docomo
49.96.224.0/21 docomo
49.96.232.0/21 docomo
49.96.242.0/23 docomo
49.96.244.0/23 docomo
49.97.0.0/24 docomo
49.97.1.0/24 docomo
49.97.2.0/23 docomo
49.97.4.0/24 docomo
49.97.5.0/24 docomo
49.97.6.0/23 docomo
49.97.8.0/21 docomo
49.97.16.0/22 docomo
49.97.20.0/22 docomo
49.97.24.0/22 docomo
49.97.28.0/23 docomo
49.97.30.0/24 docomo
49.97.31.0/24 docomo
49.97.32.0/23 docomo
49.97.34.0/24 docomo
49.97.35.0/24 docomo
49.97.36.0/23 docomo
49.97.38.0/24 docomo
49.97.39.0/24 docomo
49.97.40.0/23 docomo
49.97.42.0/24 docomo
49.97.43.0/24 docomo
49.97.44.0/23 docomo
49.97.46.0/24 docomo
49.97.47.0/24 docomo
49.97.48.0/23 docomo
49.97.50.0/24 docomo
49.97.51.0/24 docomo
49.97.55.0/24 docomo
49.97.56.0/21 docomo
49.97.64.0/20 docomo
49.97.88.0/22 docomo
49.97.92.0/22 docomo
49.97.96.0/19 docomo
49.98.0.0/22 docomo
49.98.4.0/23 docomo
49.98.6.0/24 docomo
49.98.7.0/24 docomo
49.98.8.0/21 docomo
49.98.16.0/23 docomo
49.98.18.0/24 docomo
49.98.32.0/19 docomo
49.98.64.0/18 docomo
49.98.128.0/17 docomo
49.104.0.0/18 docomo
49.104.64.0/20 docomo
49.104.80.0/21 docomo
49.105.12.0/23 docomo
49.105.64.0/21 docomo
49.105.72.0/24 docomo
49.105.74.0/23 docomo
49.105.76.0/23 docomo
49.105.82.0/23 docomo
49.105.84.0/23 docomo
49.105.90.0/23 docomo
49.105.92.0/22 docomo
49.105.96.0/21 docomo
49.106.80.0/21 docomo
49.106.104.0/21 docomo
49.106.112.0/20 docomo
49.106.128.0/22 docomo
49.106.200.0/21 docomo
49.106.208.0/20 docomo
49.106.224.0/22 docomo
49.106.228.0/23 docomo
49.109.0.0/24 docomo
49.109.1.0/24 docomo
49.109.2.0/24 docomo
49.109.3.0/24 docomo
49.109.4.0/24 docomo
49.109.5.0/24 docomo
49.109.6.0/24 docomo
49.109.7.0/24 docomo
110.163.6.0/23 docomo
110.163.8.0/22 docomo
110.163.12.0/23 docomo
110.163.216.0/21 docomo
110.163.224.0/22 docomo
183.74.0.0/21 docomo
183.74.8.0/23 docomo
183.74.192.0/20 docomo
202.229.178.32/27 docomo
240a:61::/37 docomo
240a:61:800::/38 docomo
240a:61:c00::/40 docomo
240a:61:1000::/37 docomo
240a:61:1800::/38 docomo
240a:61:1c00::/40 docomo
240a:61:2000::/37 docomo
240a:61:2800::/38 docomo
240a:61:2c00::/40 docomo
240a:61:3000::/37 docomo
240a:61:3800::/38 docomo
240a:61:3c00::/40 docomo
240a:6b::/32 docomo
49.109.8.0/21 docomo
49.109.16.0/21 docomo
49.109.24.0/22 docomo
49.109.96.0/19 docomo
240a:61:4000::/37 docomo
240a:61:4800::/38 docomo
240a:61:4c00::/40 docomo
49.109.128.0/19 docomo
49.109.160.0/20 docomo
49.109.176.0/22 docomo
240a:61:5000::/37 docomo
240a:61:5800::/38 docomo
240a:61:5c00::/40 docomo
160.249.0.0/19 docomo
160.249.32.0/20 docomo
160.249.48.0/22 docomo
240a:61:6000::/37 docomo
240a:61:6800::/38 docomo
240a:61:6c00::/40 docomo
1.75.64.0/19 docomo
1.75.96.0/20 docomo
1.75.112.0/22 docomo
240a:61:7000::/37 docomo
240a:61:7800::/38 docomo
240a:61:7c00::/40 docomo
49.109.224.0/20 docomo
1.73.242.0/24 docomo
1.73.243.0/24 docomo
1.75.84.0/22 docomo
1.75.88.0/22 docomo
1.75.92.0/22 docomo
49.109.68.0/22 docomo
49.109.164.0/22 docomo
49.109.168.0/22 docomo
49.109.240.0/22 docomo
160.249.36.0/22 docomo
160.249.40.0/22 docomo
49.96.64.0/20 docomo
49.96.80.0/21 docomo
240a:61:8000::/37 docomo
240a:61:8800::/38 docomo
240a:61:8c00::/40 docomo

# ---- docomo spモード 追加予定分(2026年9月〜2027年2月とページに記載。2026-10-09 時点で AS9605 から広告済み)----
49.109.80.0/21 docomo
27.230.0.0/19 docomo
27.230.160.0/22 docomo
240a:61:9a00::/39 docomo
240a:61:9da0::/43 docomo
240a:61:9dc0::/42 docomo
240a:61:aa00::/39 docomo
240a:61:ada0::/43 docomo
240a:61:adc0::/42 docomo
49.105.192.0/19 docomo
49.105.224.0/22 docomo

# ---- au LTE NET / 5G NET(2025年1月現在。106.134.0.0/16・106.135.0.0/16 は au が「除く」としている)----
27.81.160.0/20 kddi
27.81.208.0/20 kddi
27.91.0.0/21 kddi
27.91.16.0/20 kddi
27.91.32.0/19 kddi
59.132.0.0/19 kddi
59.132.32.0/20 kddi
59.132.48.0/21 kddi
59.132.84.0/22 kddi
59.132.88.0/21 kddi
59.132.96.0/19 kddi
59.132.128.0/21 kddi
2001:268:7200::/42 kddi
2001:268:7240::/43 kddi
2001:268:7260::/43 kddi
2001:268:7280::/42 kddi
2001:268:7600::/40 kddi
2001:268:7700::/40 kddi
2001:268:7800::/40 kddi

nginx/snippets/jp-allowlist.conf → /etc/nginx/snippets/jp-allowlist.conf

保護する server ブロックで include する遮断 2 行。

# /etc/nginx/snippets/jp-allowlist.conf
# 保護したい server {} ブロック(または location {})の先頭で include する。
#   444 = 応答を返さず切断(スキャナ向け。帯域とログ量を節約)
#   403 を返したい場合は "return 403;" に変更する。
if ($jp_deny) {
    return 444;
}

nginx/snippets/example-server.conf → (組み込み例)

server ブロックへの組み込み例。

# 組み込み例(既存の server ブロックに 2 行足すだけ)
server {
    listen 443 ssl;
    listen [::]:443 ssl;                 # IPv6 でも待ち受ける(docomo は IPv6 シングルスタック)
    server_name example.jp;

    include /etc/nginx/snippets/jp-allowlist.conf;      # ← 追加(server 直下・最上部)
    access_log /var/log/nginx/access.log jp_allowlist;  # ← 追加(観測用ログ形式)

    # ... 既存の設定 ...
}

systemd/jp-allowlist.service → /etc/systemd/system/jp-allowlist.service

日次更新の oneshot サービス。

# /etc/systemd/system/jp-allowlist.service
[Unit]
Description=Regenerate nginx JP mobile allowlist
After=network-online.target nginx.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update_jp_allowlist.sh
# 社内プロキシ経由で外に出る場合
#Environment=HTTPS_PROXY=http://proxy.example.jp:8080

systemd/jp-allowlist.timer → /etc/systemd/system/jp-allowlist.timer

日次タイマー。

# /etc/systemd/system/jp-allowlist.timer
[Unit]
Description=Daily regeneration of nginx JP mobile allowlist

[Timer]
OnCalendar=*-*-* 04:17:00
RandomizedDelaySec=20m
Persistent=true

[Install]
WantedBy=timers.target
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?