0
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?

f24: ubuntu + 海外アクセス抑止 + 自動更新(3) ipset 高速版

0
Last updated at Posted at 2026-05-05

.

以前の記事

(1) f16: xserver vps + ubuntu + 海外アクセス抑止 + 自動更新
https://qiita.com/cxfgp/items/ff18545adb3c1dc1bec2

(2) f23: ubuntu + 海外アクセス抑止 + 自動更新(2) ufw 更新版
https://qiita.com/cxfgp/items/37029713088114f22a73

の派生メモです

上記 自動更新(2) ufw 更新版 の設定から少しして、
再起動時にエラーが多発するようになりました

[FAILED] Failed to start Wait for Network to be Configured.
See 'systemctl status systemd-networkd-wait-online.service' for details.

「まれに適用時エラーになることがあります」という記載を
いたしましたが、この多発は、そこの記載で想定していた
「何かの処理時のエラー」ということではなく、

国内許可IPリストの読み込みに時間がかかりすぎて
タイムアウトエラーが頻発するようになりました
(そのため、再起動時に ssh 接続が不可になります)

ということで、

・systemd-networkd-wait-online を無効化
・systemd-networkd-wait-online.service Timeout値を延長
・UFW の遅延ロード (起動完了後にルール適用)
・cloud-init の待機解除 (VPSの場合不要のようで意味なし)

など、いくつか苦肉も含めた対応策はありますが、

本来、起動時に CIDR (約数千〜数万行)を
iptables にロードする処理は推奨されていないようで、
そもそもの根底を変えなければあまり改善にならないため、

・ufw backend + ipset
 (ipset: IPリストをハッシュテーブルで管理)

という手法に切り替えました
(結論としては、現時点ではこの方法一択です)


[ 00. メニュー ]

01. キッチン
02. 概要
03. 環境設定
04. スクリプト(1) drop_except_jp.sh
05. スクリプト(2) check_ufw.sh
06. 自動更新 (cron) 設定
07. ipset 適用確認
08. デザート
09. 参考


[ 01. キッチン ]

 ・Xserver VPS / 2GB プラン
  -> Ubuntu 22.04 (64bit)

$ which ufw
/usr/sbin/ufw
$ which iptables
/usr/sbin/iptables
$ which ipset
/usr/sbin/ipset

$ systemctl is-enabled ufw
-> enabled
$ systemctl is-enabled ipset-restore-jp.service
-> enabled

※ ufw / iptables はよほどないと思いますが、
 ipset は、docker などを利用されていないと
 入っていないケースがありますので、その際は以下で入れてください

$ sudo apt update
$ sudo apt install ipset

[ 02. 概要 ]

 自動更新(2) ufw 更新版 からの変更点

  ============================
  [1] drop_except_jp.sh にて、
  ・cidr.txt の入れ替え
  ・ufw (after.init にルール適用-> iptables 更新)
   「A. ポートの許可」
   「B. 例外の許可」
   の部分はそのままに、以下を追加
  ・cidr.txt の ipset への仕込み (save to file)

  ============================
  [2]「C. 国内許可IP設定」 の部分のみ、
  drop_except_jp.sh から切り離し
  サービス ipset (ipset-restore-jp.service) を作成し、
  再起動後のサービス ipset での設定に切り替えます

  ============================

処理の流れ

  ============================
  [1] (例: 03:00am) cron -> drop_except_jp.sh

   1. cidr.txt を DL(JP 行を含む元データ)
   2. ufw after.init にルール適用
    A. ポートの許可 (国外でも許可するポート)
    B. 例外の許可 (固定 IP 許可)
   3. cidr.txt から JP IP を取得して ipset にセットし、
     再起動後のリストア用にファイルに保存

  ============================
  [2] (例: 03:10am) cron -> OS reboot

   (ipset 適用の流れ)
   systemd 起動
    └ network-pre.target
    └ network 起動
    └ network-online.target
    └ ipset-restore-jp.service 起動
     ・ExecStart 実行
      └ (存在すれば) ipset destroy JP-CIDR
      └ ipset restore < /etc/ipset/jp-cidr.restore
      (ファイル -> RAM 上の ipset へ JP-CIDR(IPリスト)を復元)
    └ ufw.service 起動
     └ before.rules 読み込み
      (-A ufw-before-input -m set --match-set JP-CIDR src -j ACCEPT)
     └ user.rules 読み込み
     └ after.rules 読み込み
     └ iptables-restore
     └ firewall(iptables) ルールを RAM にロード
     └ JP-CIDR ルールの有効化
     -> iptables で ipset 参照可能となる

  ============================
  [3] OS 起動後の処理(@reboot) -> check_ufw.sh

   # ufw ステータスチェック
   systemctl status ufw

   # ipset-restore-jp.service ステータスチェック
   systemctl status ipset-restore-jp.service

   # iptables (JP-CIDR) 設定チェック
   iptables -S ufw-before-input | grep JP-CIDR

  ============================


[ 03. 環境設定 ]

 ============================
 (01) ディレクトリ構成

  (スクリプトの実行デイレクトリ例 -> /home/aaa/)

  /home/aaa/
   └─ drop_except_jp.sh ( スクリプト1 )
   └─ check_ufw.sh ( スクリプト2 )
   └─ cidr.txt.gz ( cidr ダウンロード )
   └─ cidr.txt ( cidr 展開 )

  /home/aaa/logs/
    └─ reboot.log ( リブートログ )
    └─ exjp.log ( スクリプト処理ログ )
    └─ ufws.log ( ufw ステータス確認ログ )
    └─ ipse.log ( ipset サービス スターテス確認ログ )

  /home/aaa/iptables/
    └─ backup.日付 ( 念の為の iptables backup )

  /etc/ufw/
    └─ after.init.drop_except_jp ( after.init 設定ファイル )
    └─ before.rules ( before.rules 設定ファイル )

  /etc/systemd/system/
    └─ ipset-restore-jp.service ( ipset リストアサービス )

  /etc/ipset/
    └─ jp-cidr.restore ( ipset restore ファイル )

 ============================
 (02) 構築順序、設定項目

  '---------------------------------------------
  1. ipset restore 用ファイル作成

[ 作成 ]

# mkdir -p /etc/ipset
# vi /etc/ipset/jp-cidr.restore
jp-cidr.restore
create JP-CIDR hash:net family inet hashsize 32768 maxelem 200000

  '---------------------------------------------
  2. systemd サービス (ipset-restore-jp.service) 作成

[ 作成 ]

# vi /etc/systemd/system/ipset-restore-jp.service
ipset-restore-jp.service
[Unit]
Description=Restore-JP-CIDR-ipset
After=network.target
Before=ufw.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c '/sbin/ipset list JP-CIDR >/dev/null 2>&1 && /sbin/ipset destroy JP-CIDR; /sbin/ipset restore < /etc/ipset/jp-cidr.restore'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
# chown root:root /etc/systemd/system/ipset-restore-jp.service
# chmod 644 /etc/systemd/system/ipset-restore-jp.service

[ service 有効化 ]

# systemctl daemon-reload
# systemctl enable ipset-restore-jp.service

[ 確認 ]

# systemctl is-enabled ipset-restore-jp.service
-> enabled

  '---------------------------------------------
  3. after.init の追記

  (自動更新(2) ufw 更新版と同じ設定です
  A. ポートの許可、B. 例外の許可 用の設定です)

  /etc/ufw/after.init の start に以下追記します
  ( /etc/ufw/after.init.drop_except_jp )

[ 追記 ]

# vi /etc/ufw/after.init
after.init
start)
    # typically required
    /etc/ufw/after.init.drop_except_jp
    ;;

  以下を実施し、after.init と after.init.drop_except_jp を
  実行可能状態にしておきます

# chmod +x /etc/ufw/after.init
# touch /etc/ufw/after.init.drop_except_jp
# chmod +x /etc/ufw/after.init.drop_except_jp

  '---------------------------------------------
  4. before.rules の追記

  /etc/ufw/before.rules に 以下を追記します
  ( -A ufw-before-input -m set --match-set JP-CIDR src -j ACCEPT )
  -> JP-CIDR に含まれるIP(=日本IP)からの通信を許可するルール

[ 追記 ]

# vi /etc/ufw/before.rules
before.rules
-A ufw-before-input -m set --match-set JP-CIDR src -j ACCEPT

  追記箇所のポイントは「ufw-before-input」かつ
 「RELATED,ESTABLISHED」の場所です

 (以下 例1 と 例2 の間)
 例1: -A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
 例2: -A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
 例3: -A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

  '---------------------------------------------
  5. OS reboot

  上記 1. - 4. の設定が完了しましたら、OS 空リブートを行っても
  エラーが発生することなく OS 起動されるようになります

  ( 3. after.init, 4. before.rules の追記の設定を
  有効にするために、OS reboot ( または ufw reload ) が
  必要になりますが、この段階で一度 OS reboot するか、
  cron での OS reboot でも、どちらのタイミングでも大丈夫です )

  '---------------------------------------------

 ============================

※ 以降 スクリプト設定、自動更新設定 に続きます


[ 04. スクリプト(1) drop_except_jp.sh ]

( chmod 604 drop_except_jp.sh )

drop_except_jp.sh
#!/bin/bash

# set script dir (例 /home/aaa/)
s_dir=$(cd $(dirname $0) && pwd)

log1=${s_dir}/logs/exjp.log

# JP-CIDR の作業用ファイルを設定 (.tmp)
IPSET_RESTORE="/etc/ipset/jp-cidr.restore"
TMP_RESTORE="${IPSET_RESTORE}.tmp"

# xt_multiport は、複数ポートを一括指定する際に必要
# -> 現段階では単一で80や443を指定してるので無くても動作する
/sbin/modprobe xt_multiport 2>/dev/null || true

#----------------------------------------
# バックアップ ( iptables backup )
#----------------------------------------
iptables-save > /home/aaa/iptables/backup.$(date +%F)

AFTER_INIT=/etc/ufw/after.init.drop_except_jp

cid1=${s_dir}/cidr.txt.gz
cid2=${s_dir}/cidr.txt
cid2tmp=${cid2}.tmp

#----------------------------------------
# 古い CIDR ファイル削除
#----------------------------------------
for fe2 in "$cid1" "$cid2"; do
	if [ -f "$fe2" ]; then
		rm -rf "$fe2"
		sleep 1s; dates1=$(date +"%Y/%m/%d %T")
		echo "$dates1 : dr1: delete OK: ${fe2##*/}" | tee -a "$log1" > /dev/null
	else
		sleep 1s; dates1=$(date +"%Y/%m/%d %T")
		echo "$dates1 : dr1: delete NG: not found ${fe2##*/}" | tee -a "$log1" > /dev/null
	fi
done

#----------------------------------------
# 新しいCIDRファイル (cidr.txt.gz) 取得
#----------------------------------------
wget -q -N http://nami.jp/ipv4bycc/cidr.txt.gz -O "$cid1"
wget_status=$?
if [ $wget_status -eq 0 ] && [ -f "$cid1" ]; then
	sleep 2s; dates2=$(date +"%Y/%m/%d %T")
	echo "$dates2 : dr1: dl: OK: cidr.txt.gz" | tee -a "$log1" > /dev/null
else
	sleep 2s; dates2=$(date +"%Y/%m/%d %T")
	echo "$dates2 : dr1: dl: NG: wget failed ($wget_status)" | tee -a "$log1" > /dev/null
	exit 1
fi

#----------------------------------------
# gunzipして cidr.txt.gz を解凍
#----------------------------------------
if gunzip -f -c "$cid1" > "$cid2tmp"; then
	mv "$cid2tmp" "$cid2"
	sleep 2s; dates3=$(date +"%Y/%m/%d %T")
	echo "$dates3 : dr1: OK: to gunzip cidr.txt.gz" | tee -a "$log1" > /dev/null
else
	rm -rf "$cid2tmp"
	sleep 2s; dates3=$(date +"%Y/%m/%d %T")
	echo "$dates3 : dr1: NG: failed to gunzip cidr.txt.gz" | tee -a "$log1" > /dev/null
	exit 1
fi

if [ -s "$cid2" ]; then

	# ufw/after.init (after.init.drop_except_jp) に書き出し (A. と B. )
	{
		echo "#!/bin/sh"
		echo "# generate by drop_except_jp.sh on $(date +"%Y/%m/%d %T")"
		echo ""

		# ===== A. 国外でも許可するポート =====

		# 国外でも許可するポート (80,443)
		echo "ufw allow 80/tcp"
		echo "ufw allow 443/tcp"
		echo ""

		# ===== B. 固定IP許可 =====

		FIXED_IPS=(

			# 例 google
			"35.190.247.0/24" "35.191.0.0/16" "64.233.160.0/19" "66.102.0.0/20"

		)

		for ip2 in "${FIXED_IPS[@]}"; do
			echo "ufw allow from $ip2"
		done

	} > "$AFTER_INIT"

	chmod +x "$AFTER_INIT"

	dates4=$(date +"%Y/%m/%d %T")
	echo "$dates4 : dr1: OK: ufw after.init file updated" | tee -a "$log1" > /dev/null


	# ===== C. 国内 CIDR を ipset にロードし、リストア用ファイルに書き出し =====

	NEW_SET_JP="JP-CIDR-NEW"
	OLD_SET_JP="JP-CIDR"

	if ! ipset list "$OLD_SET_JP" >/dev/null 2>&1; then
		ipset create "$OLD_SET_JP" hash:net family inet hashsize 32768 maxelem 200000
	fi
	ipset destroy "$NEW_SET_JP" 2>/dev/null
	ipset create "$NEW_SET_JP" hash:net family inet hashsize 32768 maxelem 200000

	# cidr.txt から JP 行のみ抽出して ipset に追加
	# 国内IP許可
	while read -r cid3; do
		[ -z "$cid3" ] && continue
		# 重複 IP は無視して skip
		ipset add "$NEW_SET_JP" "$cid3" -exist
	done < <(sed -n 's/^JP[[:space:]]\+//p' "$cid2")

	# 入れ替え
	ipset swap "$NEW_SET_JP" "$OLD_SET_JP"

	# 書き出し
	ipset save "$OLD_SET_JP" > "$TMP_RESTORE" || exit 1
	mv "$TMP_RESTORE" "$IPSET_RESTORE"
	chmod 644 "$IPSET_RESTORE"

	echo "$dates4 : dr1: OK: cidr to ipset is ready" | tee -a "$log1" > /dev/null

	# 不要になった側を削除
	ipset destroy "$NEW_SET_JP"

else

	dates4=$(date +"%Y/%m/%d %T")
	echo "$dates4 : dr1: NG: not found cidr.txt or not a file" | tee -a "$log1" > /dev/null
	exit 1

fi

[ 05. スクリプト(2) check_ufw.sh ]

( chmod 604 check_ufw.sh )

check_ufw.sh
#!/bin/bash

# set script dir (例 /home/aaa/)
s_dir=$(cd $(dirname $0) && pwd)

log1=${s_dir}/logs/exjp.log
log2=${s_dir}/logs/ufws.log
log6=${s_dir}/logs/ipse.log

#----------------------------------------
# ufw ステータスチェック
#----------------------------------------
sleep 20s; systemctl status ufw -l > "$log2"

sleep 2s; dates3=`date +"%Y/%m/%d %T"`

# for check ufw status date
dates4=`date +"%Y-%m-%d"`

# ufw active check
if grep -q "active (exited)" "$log2"; then

	comp4=`grep "Active:" $log2 | { IFS= read -r lines1 && trimmed_lines1=$(echo "$lines1" | awk '{$1=$1};1'); echo "$trimmed_lines1"; }`

	echo "$dates3 : ck1: $comp4" | tee -a $log1 > /dev/null

	rid_date2=$(echo "$comp4" | awk '{print $6}')

	if [ "$rid_date2" = "$dates4" ]; then
		echo "$dates3 : ck1: OK: ufw status: active (exited) + $rid_date2" | tee -a $log1 > /dev/null
	else
		echo "$dates3 : ck1: NG: ufw status: active (exited) + date NOT-FOUND: $dates4" | tee -a $log1 > /dev/null
		exit 1
	fi

else
	grep "Active:" $log2 | { IFS= read -r lines1 && trimmed_lines1=$(echo "$lines1" | awk '{$1=$1};1'); echo "$dates3 : ck1: NG: NOT-FOUND ufw active: $trimmed_lines1"; } | tee -a $log1 > /dev/null
	exit 1
fi

#----------------------------------------
# ipset-restore-jp.service ステータスチェック
#----------------------------------------
sleep 10s; systemctl status ipset-restore-jp.service -l > "$log6"

sleep 2s; dates5=`date +"%Y/%m/%d %T"`

# for check ipset status date
dates6=`date +"%Y-%m-%d"`

# ipset active check
if grep -q "active (exited)" "$log6"; then

	comp6=`grep "Active:" $log6 | { IFS= read -r lines2 && trimmed_lines2=$(echo "$lines2" | awk '{$2=$2};1'); echo "$trimmed_lines2"; }`
	echo "$dates5 : ck1: $comp6" | tee -a $log1 > /dev/null
	rid_date4=$(echo "$comp6" | awk '{print $6}')

	if [ "$rid_date4" = "$dates6" ]; then
		echo "$dates5 : ck1: OK: ipset status: active (exited) + $rid_date4" | tee -a $log1 > /dev/null
	else
		echo "$dates5 : ck1: NG: ipset status: active (exited) + date NOT-FOUND: $dates6" | tee -a $log1 > /dev/null
		exit 1
	fi

else
	grep "Active:" $log6 | { IFS= read -r lines2 && trimmed_lines2=$(echo "$lines2" | awk '{$2=$2};1'); echo "$dates5 : ck1: NG: NOT-FOUND ipset active: $trimmed_lines2"; } | tee -a $log1 > /dev/null
	exit 1
fi

#----------------------------------------
# iptables (JP-CIDR) 設定チェック
#----------------------------------------
sleep 2s; dates7=$(date +"%Y/%m/%d %T")
echo "$dates7 : ck1: check set JP-CIDR from before.rules to iptables" | tee -a $log1 > /dev/null
iptables -S ufw-before-input | grep JP-CIDR | tee -a $log1 > /dev/null


[ 06. 自動更新 (cron) 設定 ]

cron (root user) で、以下の 3つの設定を行います

[ 設定 ]

# crontab -u root -e
# a. 例. 火曜 03:00am update cidr
00 3 * * 2 bash /home/aaa/drop_except_jp.sh

# b. 例. 火曜 03:10am os reboot
10 3 * * 2 /bin/echo "$(date '+\%Y-\%m-\%d \%H:\%M:\%S') - reboot started" >> /home/aaa/logs/reboot.log 2>&1; /usr/sbin/shutdown -r now

# c. after os reboot
@reboot /bin/echo "$(date '+\%Y-\%m-\%d \%H:\%M:\%S') - done reboot as run @reboot" >> /home/aaa/logs/reboot.log 2>&1 && bash /home/aaa/check_ufw.sh

[ 確認 ]

# crontab -l

 ( よくいわれます、crontab -r の削除オプションと
 間違えないようご注意ください )

※ この 自動更新設定 にて、環境構築は完了です
  あとは、cron の実行を待ちます

 (補足)

 # b. 例. 火曜 03:10am os reboot
 # c. after os reboot

 において、

 cron で、エスケープシーケンスを用いて
 %Y-%m-%d %H:%M:%S を使用している関係でログに

 \2026-\05-\01 \21:\39:\04 - reboot started
 2026-05-01 21:39:24 - done reboot as run @reboot

 のように、日付の前に \ が出力される場合があり、
 これを、printf に変更して回避してみたりしましたが、
 どうも不安定で、処理が実行されなかったりしましたので、
 現状は、\2026-\05-\01 \21:\39:\04 のように
 出力される場合もありますが、echo にしてます


[ 07. ipset 適用確認 ]

上記 [ 06. 自動更新 (cron) 設定 ] までの設定で、
ufw backend + ipset が動作する環境となり、
cron 実行より、最終的に OS再起動 で、
ipset にロード(リストア)され機能する流れですが、
「設定は有効」で「海外IP抑止ができているのか」
の確認方法をいくつか記載します

(本来は実際の日本IP、海外IPからのアクセスで、
許可/不許可のテストを行えば確実ですが、
その環境の用意が難しいケースもありますので、以下の確認を行ないます)

==============================================
(1) ipset が正しくロードされているか確認 [ 設定確認 ]

 '---------------------------------------------
 1. JP-CIDR の中身が存在するか確認します

# ipset list JP-CIDR | head

  [結果例]

  Name: JP-CIDR
  Type: hash:net
  Revision: 7
  Header: family inet hashsize 32768 maxelem 200000 bucketsize 12 initval 0x44a452b1
  Size in memory: 145680
  References: 1
  Number of entries: 3174
  Members:
  123.456.789.0/24
  12.345.67.0/24

  ※ head をつけることで、上部のみ出力されます

 '---------------------------------------------
 2. JP-CIDR の登録件数確認(Members以降のIP件数を表示)

# ipset list JP-CIDR | sed '1,/Members:/d' | wc -l

  [結果例]

  3174 (3174件のIP登録があり)

 '---------------------------------------------

==============================================
(2) iptables が ipset を参照しているかの確認 [ 設定確認 ]

# iptables -S ufw-before-input | grep JP-CIDR

  [結果例]

  -A ufw-before-input -m set --match-set JP-CIDR src -j ACCEPT

  -> ufw → iptables → ipset の接続が成立している

==============================================
(3) 実際にパケットがヒットしているか [ 実トラフィック確認 ]

# iptables -L ufw-before-input -n -v | grep JP-CIDR

  [結果例]

  12345 987K ACCEPT all -- * * 0.0.0.0/0 match-set JP-CIDR src

  [項目]          [意味]
  12345          -> ヒット回数
  987K           -> 通過バイト
  ACCEPT         -> 許可されている
  match-set JP-CIDR src   -> JPのIPにヒット

  -> 数値が増えていれば 実際に JP IP の通信がヒットしている

==============================================
(4) 海外IPが拒否されているか確認 [ ログ確認 ]

# dmesg | grep UFW

 (結構な件数が表示される可能性がありますのでご注意)

 [結果例]

 [UFW BLOCK] IN=eth0 SRC=45.xx.xx.xx

 JP以外のIPが BLOCK されていれば成功
 (=海外IPが実際に遮断されている)

 [念のため確認 (必要の場合) ]

$ whois 45.xx.xx.xx
(sudo apt install whois 後)

==============================================
(5) 上記 (3) JPヒット + (4) ブロック の件数表示 [ 実トラフィック + ログ確認 ]

# echo "JP HIT:" $(iptables -L ufw-before-input -n -v | grep JP-CIDR | awk '{print $1}') " / BLOCK:" $(dmesg | grep -c "UFW BLOCK")

 [結果例]

 JP HIT: 73201 / BLOCK: 1543

==============================================


[ 08. デザート ]

これで、OS 再起動時、30分以上かかってタイムアウトしてた状態から
数分で設定完了 ( + SSH 接続 OK ) の状態になりました

/home/aaa/logs/reboot.log
2026-04-07 03:10:01 - reboot started
2026-04-07 03:11:12 - done reboot as run @reboot

2026-04-21 03:10:01 - reboot started
2026-04-21 03:11:00 - done reboot as run @reboot

2026-05-03 20:14:02 - reboot started
2026-05-03 20:16:26 - done reboot as run @reboot

[ 09. 参考させていただいた記事 ]


.

0
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
0
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?