tableを使って、わかりやすく制限をかけてみた。どうせ制限するなら、IPレベルで制限したほうが安心度が高いから。
環境
- FreeBSD 14.1
- ipfw
テーブル用ファイル作成
テーブル用ファイル作成スクリプト
次のファイルをgenaclconf.py
という名前で保存する。
#!/usr/bin/env python3
"""
Nginx用に、アクセス制限を行うためのconfファイルを作成する。
アクセス制限の種類は、国別とクローラー別の2種類。
"""
from pathlib import Path
import argparse
import requests
import yaml
CONFDIR = '/usr/local/etc'
APNICSRC = [
'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest',
'https://ftp.apnic.net/stats/ripe-ncc/delegated-ripencc-extended-latest',
'https://ftp.apnic.net/stats/arin/delegated-arin-extended-latest',
'https://ftp.apnic.net/stats/lacnic/delegated-lacnic-extended-latest',
'https://ftp.apnic.net/stats/afrinic/delegated-afrinic-extended-latest'
]
COUNTRY = Path(__file__).parent / 'country_code.yaml'
VERBOSE = True
def download_apnic_file(savedir, no_download):
"""
指定されたパスからファイルをダウンロードして、CONFDIRに
保存する
"""
files = []
for url in APNICSRC:
filename = Path(url).name
filepath = Path(savedir) / filename
files.append(filepath)
if no_download:
continue
vprint(f'Downloading {filename}...')
response = requests.get(url)
if response.status_code == 200:
with filepath.open('wb') as f:
f.write(response.content)
vprint('Download done.')
else:
vprint(f'Download error: {response.status_code}')
return files
def vprint(text):
"""
VORBOSE指定のとき、テキストを標準出力
"""
if not VERBOSE:
return None
print(text)
return None
def convert_ip_to_country(files):
"""
delegated-apnic-latestファイルからIPv4アドレスと国名を取得し、
国別にCIDR表記を配列にする。
Returns: 国をキー、CIDR表記の配列をデータとする辞書
"""
cidr_list = {}
for filename in files:
with open(filename, 'r') as f:
for line in f:
if '|ipv4|' in line and not 'summary' in line:
ip_info = line.split('|')
country = ip_info[1]
ip_address = ip_info[3]
tmp_cidr = int(ip_info[4])
cidr = 32
if country == '' or country == 'ZZ':
continue
while tmp_cidr != 1:
tmp_cidr //= 2
cidr -= 1
data = f'{ip_address}/{cidr}'
if not country in cidr_list.keys():
cidr_list[country] = [data]
else:
cidr_list[country].append(data)
return cidr_list
def save_country_files(savedir, geolist, ext):
"""
指定されたディレクトリに、指定の拡張子をつけて、ファイルに国別データを保存する。
Returns: None
"""
if not Path(savedir).exists():
Path(savedir).mkdir(parents=True)
for country, list in geolist.items():
savefile = Path(savedir) / f'{country}.{ext}'
with savefile.open('w') as f:
print(*list, sep='\n', file=f)
return None
def generate_nginx_deny_list(cidr_list, country_code):
geolist = {}
for country, addrlist in cidr_list.items():
if not country in geolist.keys():
geolist[country] = [f'# {country_code[country]}']
for subnet in addrlist:
data = f'deny {subnet};'
geolist[country].append(data)
return geolist
def generate_nginx_allow_list(cidr_list, country_code):
geolist = {}
for country, addrlist in cidr_list.items():
if not country in geolist.keys():
geolist[country] = [f'# {country_code[country]}']
for subnet in addrlist:
data = f'allow {subnet};'
geolist[country].append(data)
return geolist
def generate_geo_list(cidr_list, country_code):
geolist = {}
for country, addrlist in cidr_list.items():
if not country in geolist.keys():
geolist[country] = [f'# {country_code[country]}']
for subnet in addrlist:
data = f'{subnet} {country};'
geolist[country].append(data)
return geolist
def generate_ipfw_table(cidr_list, country_code):
geolist = {}
for country, addrlist in cidr_list.items():
tablename = f'{country}'
if not country in geolist.keys():
geolist[country] = [
'#!/bin/sh',
f'# {country_code[country]}',
f'ipfw table {tablename} create type addr',
f'ipfw -q table {tablename} \\']
for subnet in addrlist:
data = f'add {subnet} \\'
geolist[country].append(data)
return geolist
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Simple geo file generator for nginx')
parser.add_argument('-n', '--no_download', action='store_true',
help='Do not download data before generating conf files')
parser.add_argument('-v', '--verbose', action='store_true',
help='Show downloading status')
parser.add_argument('--savedir', type=str, default=CONFDIR,
help='The path to store conf files')
args = parser.parse_args()
no_download = args.no_download
VERBOSE = args.verbose
savedir = Path(args.savedir)
# 国コード定義読み込み
with COUNTRY.open('r') as f:
country_code = yaml.safe_load(f)
# APNICファイルのダウンロード
files = download_apnic_file(savedir, no_download)
cidr_list = convert_ip_to_country(files)
geolist = generate_ipfw_table(cidr_list, country_code)
ipfwdir = savedir / 'ipfw' / 'country'
save_country_files(ipfwdir, geolist, 'sh')
geolist = generate_geo_list(cidr_list, country_code)
ipfwdir = savedir / 'nginx' / 'geo' / 'country'
save_country_files(ipfwdir, geolist, 'conf')
geolist = generate_nginx_allow_list(cidr_list, country_code)
ipfwdir = savedir / 'nginx' / 'geo' / 'allow'
save_country_files(ipfwdir, geolist, 'conf')
geolist = generate_nginx_deny_list(cidr_list, country_code)
ipfwdir = savedir / 'nginx' / 'geo' / 'deny'
save_country_files(ipfwdir, geolist, 'conf')
ファイルに実行権を付与する。
# chmod 755 genaclconf.py
次のファイルを、genaclconf.py
と同じディレクトリにcountry_code.yaml
という名前で保存する。
AD: アンドラ
AE: アラブ首長国連邦
AF: アフガニスタン
AG: アンティグア・バーブーダ
AI: アンギラ
AL: アルバニア
AM: アルメニア
AO: アンゴラ
AQ: 南極
AR: アルゼンチン
AS: アメリカ領サモア
AT: オーストリア
AU: オーストラリア
AW: アルバ
AX: オーランド諸島
AZ: アゼルバイジャン
BA: ボスニア・ヘルツェゴビナ
BB: バルバドス
BD: バングラデシュ
BE: ベルギー
BF: ブルキナファソ
BG: ブルガリア
BH: バーレーン
BI: ブルンジ
BJ: ベナン
BL: サン・バルテルミー
BM: バミューダ
BN: ブルネイ・ダルサラーム
BO: ボリビア多民族国
BQ: ボネール、シント・ユースタティウスおよびサバ
BR: ブラジル
BS: バハマ
BT: ブータン
BV: ブーベ島
BW: ボツワナ
BY: ベラルーシ
BZ: ベリーズ
CA: カナダ
CC: ココス(キーリング)諸島
CD: コンゴ民主共和国
CF: 中央アフリカ共和国
CG: コンゴ共和国
CH: スイス
CI: コートジボワール
CK: クック諸島
CL: チリ
CM: カメルーン
CN: 中華人民共和国
CO: コロンビア
CR: コスタリカ
CU: キューバ
CV: カーボベルデ
CW: キュラソー
CX: クリスマス島
CY: キプロス
CZ: チェコ
DE: ドイツ
DJ: ジブチ
DK: デンマーク
DM: ドミニカ国
DO: ドミニカ共和国
DZ: アルジェリア
EC: エクアドル
EE: エストニア
EG: エジプト
EH: 西サハラ
ER: エリトリア
ES: スペイン
EU: EU連合
ET: エチオピア
FI: フィンランド
FJ: フィジー
FK: フォークランド(マルビナス)諸島
FM: ミクロネシア連邦
FO: フェロー諸島
FR: フランス
GA: ガボン
GB: イギリス
GD: グレナダ
GE: ジョージア
GF: フランス領ギアナ
GG: ガーンジー
GH: ガーナ
GI: ジブラルタル
GL: グリーンランド
GM: ガンビア
GN: ギニア
GP: グアドループ
GQ: 赤道ギニア
GR: ギリシャ
GS: サウスジョージア・サウスサンドウィッチ諸島
GT: グアテマラ
GU: グアム
GW: ギニアビサウ
GY: ガイアナ
HK: 香港
HM: ハード島とマクドナルド諸島
HN: ホンジュラス
HR: クロアチア
HT: ハイチ
HU: ハンガリー
ID: インドネシア
IE: アイルランド
IL: イスラエル
IM: マン島
IN: インド
IO: イギリス領インド洋地域
IQ: イラク
IR: イラン・イスラム共和国
IS: アイスランド
IT: イタリア
JE: ジャージー
JM: ジャマイカ
JO: ヨルダン
JP: 日本
KE: ケニア
KG: キルギス
KH: カンボジア
KI: キリバス
KM: コモロ
KN: セントクリストファー・ネイビス
KP: 朝鮮民主主義人民共和国
KR: 大韓民国
KW: クウェート
KY: ケイマン諸島
KZ: カザフスタン
LA: ラオス人民民主共和国
LB: レバノン
LC: セントルシア
LI: リヒテンシュタイン
LK: スリランカ
LR: リベリア
LS: レソト
LT: リトアニア
LU: ルクセンブルク
LV: ラトビア
LY: リビア
MA: モロッコ
MC: モナコ
MD: モルドバ共和国
ME: モンテネグロ
MF: サン・マルタン(フランス領)
MG: マダガスカル
MH: マーシャル諸島
MK: 北マケドニア
ML: マリ
MM: ミャンマー
MN: モンゴル
MO: マカオ
MP: 北マリアナ諸島
MQ: マルティニーク
MR: モーリタニア
MS: モントセラト
MT: マルタ
MU: モーリシャス
MV: モルディブ
MW: マラウイ
MX: メキシコ
MY: マレーシア
MZ: モザンビーク
NA: ナミビア
NC: ニューカレドニア
NE: ニジェール
NF: ノーフォーク島
NG: ナイジェリア
NI: ニカラグア
NL: オランダ
'NO': ノルウェー
NP: ネパール
NR: ナウル
NU: ニウエ
NZ: ニュージーランド
OM: オマーン
PA: パナマ
PE: ペルー
PF: フランス領ポリネシア
PG: パプアニューギニア
PH: フィリピン
PK: パキスタン
PL: ポーランド
PM: サンピエール島・ミクロン島
PN: ピトケアン
PR: プエルトリコ
PS: パレスチナ
PT: ポルトガル
PW: パラオ
PY: パラグアイ
QA: カタール
RE: レユニオン
RO: ルーマニア
RS: セルビア
RU: ロシア連邦
RW: ルワンダ
SA: サウジアラビア
SB: ソロモン諸島
SC: セーシェル
SD: スーダン
SE: スウェーデン
SG: シンガポール
SH: セントヘレナ・アセンションおよびトリスタンダクーニャ
SI: スロベニア
SJ: スヴァールバル諸島およびヤンマイエン島
SK: スロバキア
SL: シエラレオネ
SM: サンマリノ
SN: セネガル
SO: ソマリア
SR: スリナム
SS: 南スーダン
ST: サントメ・プリンシペ
SV: エルサルバドル
SX: シント・マールテン(オランダ領)
SY: シリア・アラブ共和国
SZ: エスワティニ
TC: タークス・カイコス諸島
TD: チャド
TF: フランス領南方・南極地域
TG: トーゴ
TH: タイ
TJ: タジキスタン
TK: トケラウ
TL: 東ティモール
TM: トルクメニスタン
TN: チュニジア
TO: トンガ
TR: トルコ
TT: トリニダード・トバゴ
TV: ツバル
TW: 台湾(中華民国)
TZ: タンザニア
UA: ウクライナ
UG: ウガンダ
UM: 合衆国領有小離島
US: アメリカ合衆国
UY: ウルグアイ
UZ: ウズベキスタン
VA: バチカン市国
VC: セントビンセントおよびグレナディーン諸島
VE: ベネズエラ・ボリバル共和国
VG: イギリス領ヴァージン諸島
VI: アメリカ領ヴァージン諸島
VN: ベトナム
VU: バヌアツ
WF: ウォリス・フツナ
WS: サモア
YE: イエメン
YT: マヨット
ZA: 南アフリカ
ZM: ザンビア
ZW: ジンバブエ
genaclconf.py
を実行すれば、ipfwへの国別テーブル作成用スクリプトが/usr/local/etc/ipfw/country
の下に生成される。
# sudo ./genaclconf.py
作成したスクリプトに実行権を付与しておく。
# sudo chmod 755 /usr/local/etc/ipfw/country/*.sh
自動更新設定
定期的に更新するよう、cronにgenaclconf.py
を登録する。
# sudo crontab -e
次の行を追加して、エディタを終了する。
27 0 * * 1 /usr/local/bin/genaclconf.py
ipfwスクリプト編集
テーブル登録
同時に使用できるテーブル数は、最大127まで。それを越えると、テーブル作成時に次のようなエラーを出す。
ipfw: Table creation failed: Device busy
すべてをテーブル登録するのは無理なので、参照する分だけ必要に応じてテーブル作成する。
テーブル作成とアクセス制限のコマンドは、/etc/rc.firewall
を編集して追加する。ただし、直接ファイル編集はしない。編集用に/etc/rc.firewall
をコピーしたものを使用する。
# sudo cp /etc/rc.firewall /usr/local/etc/ipfw.rules
/usr/local/etc/ipfw.rules
を編集して、次の行を追加する。
(略)
############
# Flush out the list before we begin.
#
${fwcmd} -f flush
setup_loopback
setup_ipv6_mandatory
# 【追加部分開始】
# 国別アクセス制限用テーブル作成
${fwcmd} table all destroy
deny_per_country_table() {
country="$1"
port="$2"
for i in $country; do
country_table=/usr/local/etc/ipfw/country/$i.sh
if ! ${fwcmd} table $i info > /dev/null 2>&1; then
if [ -f $country_table ]; then
$country_table
fi
fi
${fwcmd} add deny ip from "table($i)" to any $port
done
}
# メール送信用ポートのアクセス制限
if [ -n "${firewall_smtp_deny}" ]; then
deny_per_country_table "${firewall_smtp_deny}" "25,587"
fi
# Web用ポートのアクセス制限
if [ -n "${firewall_http_deny}" ]; then
deny_per_country_table "${firewall_http_deny}" "80,443"
fi
# 【追加部分終了】
############
# Network Address Translation. All packets are passed to natd(8)
# before they encounter your remaining rules. The firewall rules
# will then be run again on each packet after translation by natd
# starting at the rule number following the divert rule.
#
# For ``simple'' firewall type the divert rule should be put to a
# different place to not interfere with address-checking rules.
#
case ${firewall_type} in
(略)
アクセス制限登録
/etc/rc.conf
に、次のようにアクセス制限を行うための行を追加。
firewall_script="/usr/local/etc/ipfw.rules"
firewall_smtp_deny="BG RO RU UA"
firewall_http_deny="BG IL RO FR HK IN SG DE VN GB RU NL JO PL ES IT ID"