ipfwで国別アクセス制限

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"

参考

タイトルとURLをコピーしました