プロジェクト

全般

プロフィール

機能 #1246

完了
NO NO

行儀の悪い bot からのアクセスを遮断する

機能 #1246: 行儀の悪い bot からのアクセスを遮断する

nop_thread さんが約11時間前に追加. 39分前に更新.

ステータス:
終了
優先度:
高め
担当者:
開始日:
期日:
進捗率:

100%

一時中断:
いいえ
pinned:
いいえ
リマインド予定日:
前回確認日:
管理外残件あり:
いいえ

説明

どうも海外 (特に VPS 系?) の IP アドレス範囲から PC やスマートフォンを装った (つまり User-Agent を bot とわかるようにしていない) アクセスが大量に来ている。

頻度としては (ある観測期間、ある程度のフィルタ後においては) 秒間4回程度のようなのだが、たとえば Redmine の PDF export 等のリンクも容赦なく辿るのでサーバ側の負荷が大きい (特にメモリ)。

こういった有害な bot による負荷を回避したい。

NO nop_thread さんが約11時間前に更新 操作 #1

  • トラッカー を バグ から 機能 に変更
  • 題名 を 行儀の悪い bot からのアクセスが激しい から 行儀の悪い bot からのアクセスを遮断する に変更
  • 説明 を更新 (差分)
  • 優先度 を 通常 から 高め に変更

NO nop_thread さんが約11時間前に更新 操作 #2

それぞれの IP アドレスは短い期間においてはせいぜい50回強程度 (ある程度のフィルタ後) しか使わないようにしていたり、 User-Agent も人間のクライアントと同様のものを使っていたりなど、それなりの精度のフィルタを用意するのが難しそうなのが困りもの。
人間を容赦なく弾いて良いのであれば有効そうなフィルタは書けるだろうが、その結果海外出張に行った自分や普段と異なる端末を使っている自分が弾かれるようでは困るので、過剰なフィルタはできるだけ避けたい。

NO nop_thread さんが約11時間前に更新 操作 #3

ASN とかで GCP や Alibaba を遮断するのはひとつの手だが、そういったフィルタは Caddy においては xcaddy で非デフォルトモジュールを含めてビルドしないと使えないようである。
野良ビルド&デプロイのインフラを整備していないので、できれば公式バイナリか Debian のパッケージリポジトリに入っているものだけでなんとかしたいのだが……。

NO nop_thread さんが約11時間前に更新 操作 #4

  • 担当者 を nop_thread にセット

NO nop_thread さんが約11時間前に更新 · 編集済み 操作 #5

メモ:

# /etc/caddy/Caddyfile
domain.example.com {
    # ...

    # このブロックを追加してアクセスログを標準エラー出力に吐き出す
    log {
        output stderr
        format json
    }
}
  • User-Agent 統計: ssh {host} journalctl -efu caddy | sed -ne 's/^.*\(systemd\|caddy\)\[[0-9]\+\]: {/{/p' ~homelab/redmine-potato-log2.log | jq --slurp --raw-output '.[].request.headers."User-Agent".[0]' | sort | uniq -c | sort -rn
  • source IP 統計: ssh {host} journalctl -efu caddy | sed -ne 's/^.*\(systemd\|caddy\)\[[0-9]\+\]: {/{/p' ~homelab/redmine-potato-log2.log | jq --slurp --raw-output '.[].request.headers."X-Forwarded-For"[0] | select(. != null) | split(", ")[0]' | sort | uniq -c | sort -rn

NO nop_thread さんが約11時間前に更新 操作 #6

fail2ban でブロックする手があった。なるほど?
しかし Caddy と組み合わせて使うのは若干手間かもしれない。カスタムモジュールが要るか、あるいはログファイルや形式の調整が必要そう。詳しく調べていないが。

NO nop_thread さんが約8時間前に更新 操作 #7

  • ステータス を 新規 から 進行中 に変更

ログをざっと確認して明らかに bot っぽいものを上位から ASN でまるごと弾いた。
偶然か必然か、すべて中国系のネットワークからのようだ。
caddy 向けに生成したスニペット (主に remote_ip 行だがコメントも含む) が32524行もあり、 caddy の軌道や reload が非常に遅くなっている。
諦めて xcaddy 的なものを使って geoblock すべきなのかもしれない。

ブロックリスト生成スクリプト (2026-10-20 版)
#!/bin/sh
set -eu

cd "$(dirname "$(readlink -f "$0")")"

DEST_DIR="../ansible/playbooks/files/ip-block/"
LIST_FILE="${DEST_DIR}/ip-block-list-with-comment"

get_asn_prefix_list() {
	if [ $# -lt 1 ] ; then
		echo "# AS${1}: ${2}"
	else
		echo "# AS${1}"
	fi
	curl -s "https://asn.ipinfo.app/api/text/list/AS${1}"
}

gen_plain_list_with_comment() {
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 45102 'ALIBABA-CN-NET Alibaba US Technology Co., Ltd.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 9808 'CHINAMOBILE-CN China Mobile Communications Group Co., Ltd.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 136191 'CHINATELECOM-HUBEI-YICHANG-IDC YICHANG, Hubei Province, P.R.China.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 4134 'CHINANET-BACKBONE No.31,Jin-rong Street'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56040 'CMNET-GUANGDONG-AP China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 142608 'CHINANET-ANHUI-BENGBU-IDC China Telecom'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 4837 'CHINA169-BACKBONE CHINA UNICOM China169 Backbone'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 134765 'CHINANET-YUNNAN-IDC1 CHINANET Yunnan province IDC1 network'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 131516 'JHWA Jinhua Weian InfoTech Co., Ltd'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 134810 'CMNET-JILIN-AS-AP China Mobile Group JiLin communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56046 'CMNET-JIANGSU-AP China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 4837 'CHINA169-BACKBONE CHINA UNICOM China169 Backbone'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 17816 'CHINA169-GZ China Unicom IP network China169 Guangdong province'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 24445 'CMNET-V4HENAN-AS-AP Henan Mobile Communications Co.,Ltd'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56044 'CMNET-AS-LIAONING China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 136190 'CHINATELECOM-ZHEJIANG-JINHUA-IDC JINHUA, ZHEJIANG Province, P.R.China.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 63835 'CT-HUNAN-CHANGSHA-IDC No.293,Wanbao Avenue'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 134764 'CT-FOSHAN-IDC CHINANET Guangdong province network'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 136188 'CHINATELECOM-ZHEJIANG-NINGBO-IDC NINGBO, ZHEJIANG Province, P.R.China.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 63838 'CT-HUNAN-HENGYANG-IDC Hengyang'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 133774 'CHINATELECOM-FUJIAN-FUZHOU-IDC1 Fuzhou'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 133776 'CHINATELECOM-FUJIAN-QUANZHOU-IDC1 Quanzhou'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 151185 'CT-XIANGYANG-IDC2 China Telecom'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 139887 'CHINANET-YANTAI-IDC ChinaNet Shandong Yantai IDC network'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 140061 'CHINANET-QINGHAI-AS-AP Qinghai Telecom'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56047 'CMNET-HUNAN-AP China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 24444 'CMNET-V4SHANDONG-AS-AP Shandong Mobile Communication Company Limited'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 24400 'CMNET-V4SHANGHAI-AS-AP Shanghai Mobile Communications Co.,Ltd.'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 132525 'CMNET-HEILONGJIANG-CN HeiLongJiang Mobile Communication Company Limited'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56045 'CMNET-JIANGXI-AP China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 38019 'CMNET-V4TIANJIN-AS-AP tianjin Mobile Communication Company Limited'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 4808 'CHINA169-BJ China Unicom Beijing Province Network'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56048 'CMNET-BEIJING-AP China Mobile Communicaitons Corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 17621 'CNCGROUP-SH China Unicom Shanghai network'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56042 'CMNET-SHANXI-AP China Mobile communications corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 58519 'CHINATELECOM-CTCLOUD Cloud Computing Corporation'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 58540 'CHINATELECOM-SHANDONG-JINAN-IDC Jinan,250000'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 58540 'CHINATELECOM-SHANDONG-JINAN-IDC Jinan,250000'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 148981 'CHINANET-HUBEI-SHIYAN-IDC China Telecom'
	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 56041 'CMNET-ZHEJIANG-AP China Mobile communications corporation'
}

gen_caddy_remote_ip_snip() {
	sed -e '/^\s*#/! s/^/remote_ip /' "$LIST_FILE"
}

gen_caddy_client_ip_snip() {
	sed -e '/^\s*#/! s/^/client_ip /' "$LIST_FILE"
}

gen_plain_list_with_comment >"$LIST_FILE"
gen_caddy_remote_ip_snip >"${DEST_DIR}/ip-block-caddy-remote-ip.caddy"
# Use this behind reverse proxies.
gen_caddy_client_ip_snip >"${DEST_DIR}/ip-block-caddy-client-ip.caddy"

# vim: set ft=sh noexpandtab :

NO nop_thread さんが約8時間前に更新 · 編集済み 操作 #8

nop_thread さんは #note-7 で書きました:

ログをざっと確認して明らかに bot っぽいものを上位から ASN でまるごと弾いた。
偶然か必然か、すべて中国系のネットワークからのようだ。
caddy 向けに生成したスニペット (主に remote_ip 行だがコメントも含む) が32524行もあり、 caddy の軌道や reload が非常に遅くなっている。
諦めて xcaddy 的なものを使って geoblock すべきなのかもしれない。

  • 一定期間内 (数十分か数時間かは数えていない) でそれぞれの IP アドレスからのアクセスが52回になるように抑制されている
    • ratelimit 対策と思われる
    • 使い捨てというわけではなく、時間をおいて更に52回アクセスするような挙動がみられた
  • User-Agent は人間が使うブラウザと同一のものを使っており、偏りはあるがバリエーションは豊富。人間を巻き添えにせずブロックすることは難しい
  • IPv4 も IPv6 も使われている
  • 以上のような特徴があるにもかかわらず、多様な ASN (ただし全て中国系のネットワーク業者) からのアクセスがある

botnet 的な何かなのか、あるいはこのような挙動をするアプリが普及しているのかはわからないが、とにかく国単位でブロックするのが一番安全かつ確実そう。

NO nop_thread さんが約7時間前に更新 操作 #9

nop_thread さんは #note-8 で書きました:

とにかく国単位でブロックするのが一番安全かつ確実そう。

https://ftp.apnic.net/stats/apnic/delegated-apnic-latest から CN のアドレス範囲を、 https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest から RU のアドレス範囲を抽出し、 https://asn.ipinfo.app/api/text/list/AS45102 (Alibaba のアドレス範囲) を持ってきて iprange の v2.0.0 以上か aggregate6 で統合してやればいい感じのリストになりそう。

なお ::gentoo リポジトリには iprange 1.0.4 しかなかった。

NO nop_thread さんが約6時間前に更新 · 編集済み 操作 #10

nop_thread さんは #note-9 で書きました:

https://ftp.apnic.net/stats/apnic/delegated-apnic-latest から CN のアドレス範囲を、 https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest から RU のアドレス範囲を抽出し、 https://asn.ipinfo.app/api/text/list/AS45102 (Alibaba のアドレス範囲) を持ってきて iprange の v2.0.0 以上か aggregate6 で統合してやればいい感じのリストになりそう。

結構うまくいっていそう。
ブロックリストはコメント抜きで18705行。

なお AS45102 は country が US であり CN をブロックしても該当しないので、個別に追加する必要があった。
他にも AS132203, AS136907 など delegated-apnic-latest の country code だけではカバーしきれていない部分がある模様。

スクリプト
#!/bin/sh
set -eu

cd "$(dirname "$(readlink -f "$0")")"

DEST_DIR="../ansible/playbooks/files/ip-block/"
LIST_FILE="${DEST_DIR}/ip-block-list-with-comment"
OPTIM_LIST_FILE="${DEST_DIR}/ip-block-list"

get_asn_prefix_list() {
	if [ $# -lt 1 ] ; then
		echo "# AS${1}: ${2}"
	else
		echo "# AS${1}"
	fi
	curl -s "https://asn.ipinfo.app/api/text/list/AS${1}"
}

gen_plain_list_with_comment() {
	# From `delegated-apnic-latest` file.
	echo '# From delegate-apnic-latest'
	curl -s 'https://ftp.apnic.net/stats/apnic/delegated-apnic-latest' | awk -F\| -e '!/^\s*(#.*)?$/&&/CN\|ipv4/{print $4 "/" 32-log($5)/log(2)}' -e '!/^\s*(#.*)?$/&&/CN\|ipv6/{print $4 "/" $5}'

	# From `delegated-ripencc-latest` file.
	echo '# From delegate-ripencc-latest'
	curl -s 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest' | awk -F\| -e '!/^\s*(#.*)?$/&&/RU\|ipv4/{print $4 "/" 32-log($5)/log(2)}' -e '!/^\s*(#.*)?$/&&/RU\|ipv6/{print $4 "/" $5}'

	#
	# From ASN
	#

	# Bad manners bots (2026-10-20)
	get_asn_prefix_list 45102 'ALIBABA-CN-NET Alibaba US Technology Co., Ltd.'
	# From china, but not marked as CN in delegated-apnic-latest for some reason (2026-10-20)
	get_asn_prefix_list 132203 'TENCENT-NET-AP-CN Tencent Building, Kejizhongyi Avenue'
	# From china, but not marked as CN in delegated-apnic-latest for some reason (2026-10-20)
	get_asn_prefix_list 136907 'HWCLOUDS-AS-AP HUAWEI CLOUDS'
}

gen_optimized_list() {
	grep -v '^\s*#' "$LIST_FILE" | aggregate6 >"$OPTIM_LIST_FILE"
}

gen_caddy_remote_ip_snip() {
	sed -e '/^\s*#/! s/^/remote_ip /' "$OPTIM_LIST_FILE"
}

gen_caddy_client_ip_snip() {
	sed -e '/^\s*#/! s/^/client_ip /' "$OPTIM_LIST_FILE"
}

gen_plain_list_with_comment >"$LIST_FILE"
gen_optimized_list >"$OPTIM_LIST_FILE"
gen_caddy_remote_ip_snip >"${DEST_DIR}/ip-block-caddy-remote-ip.caddy"
# Use this behind reverse proxies.
gen_caddy_client_ip_snip >"${DEST_DIR}/ip-block-caddy-client-ip.caddy"

# vim: set ft=sh noexpandtab :

NO nop_thread さんが約5時間前に更新 · 編集済み 操作 #11

Caddy のアクセスログ用のクエリ例

UUoC は許して。

User-Agent を集計する

$ cat log.jsonl | jq --slurp --raw-output '.[].request.headers."User-Agent"[0]' | sort | uniq -c | sort -nr | head -5
   1665 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
...
$

ソース IP アドレスごとのアクセス数を集計する

$ cat log.jsonl | jq --slurp --raw-output '.[].request.headers."X-Forwarded-For"[0] | select(. != null) | split(", ")[0]' | sort | uniq -c | sort -nr | head -30
     52 59.47.239.223
     52 58.255.249.30
     52 58.255.247.132
     52 39.129.125.248
     52 36.250.107.38
...
$

sort -nr は sort -Vr とかにすると IP アドレスのソートがいい感じになるかも。

ソース IP アドレスを指定して User-Agent を集計する

$ cat log.jsonl | jq --slurp --raw-output '.[] | select(.request.headers."X-Forwarded-For"[0] // empty | split(", ")[0] == "195.201.163.148") | .request.headers."User-Agent"[0]' | sort | uniq -c
    897 Mozilla/5.0 (compatible; SERankingBacklinksBot/1.0; +https://seranking.com/backlinks-crawler)
$

User-Agent を指定してソース IP アドレスを集計する

$ cat log.jsonl | jq --slurp --raw-output '.[] | select(.request.headers."User-Agent"[0] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36") | .request.headers."X-Forwarded-For"[0] // empty | split(", ")[0]' | sort | uniq -c | sort -Vr | head -5
...
$

NO nop_thread さんが39分前に更新 操作 #12

  • ステータス を 進行中 から 終了 に変更
  • 進捗率 を 0 から 100 に変更
  • 管理外残件あり を いいえ にセット

中露と User-Agent が Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 のもの (加えて ... Chrome/1{2,3,4}* ... も) をブロックするだけでかなり平和になった。
今の時点では解決したといえそう。

これからもたまにログを集計してブロックリストに追加していくかもしれない。

他の形式にエクスポート: PDF Atom