Blocking IP addr except for my area with FreeBSD+PF
This article is a translation of the following my article:
Original: FreeBSD+PFで日本以外からのSSH接続をブロックする
* Translated automatically by Google.
* Please note that some links or referenced content in this article may be in Japanese.
* Comments in the code are basically in Japanese.
by bokumin
Blocking IP addr except for my area with FreeBSD+PF
Introduction
Currently, I am building and running a firewall server at my house using the Wyse3040 that I previously purchased. Previously, it was used for purposes such as collecting logs, but after many twists and turns, it settled on its current form.
*Click here for the previous article on Wyse3040
Although it was called a firewall, it did not have such a complicated configuration; it simply opened ports, assigned DHCP to the L2 switch, and controlled access to the web server.
I had Fail2ban running, and it blocked people who tried to connect to SSH. Even with blocking, there are people who attempt SSH access an average of 200 to 500 times every day.
Since this server is accessed only by me, I decided that there would be no problem with implementing geographical access restrictions, so I actually tried building it.
This time I will briefly explain how to do this.
Geographically restricting areas where SSH connections can be made
I live in Japan & basically don’t go abroad. In fact, I don’t think I’ve ever been there even once in my life. Therefore, if you only allow access to Japanese IPs, there is basically no problem.
For the Japanese IP list, we will obtain and format the data from APNIC (Asia Pacific Network Information Center). APNIC is the Asia-Pacific Regional Internet Number Resource Management Authority, which is a highly reliable organization that mainly manages the allocation of IP addresses and AS numbers in some Asian regions.
We will actually acquire data from APNIC.
$ wget "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest"
$ cat delegated-apnic-latest
apnic|CN|ipv6|240e:800::|21|20111214|allocated
apnic|CN|ipv6|240e:1000::|20|20181227|allocated
apnic|CN|ipv6|240e:2000::|19|20181227|allocated
apnic|JP|ipv6|240f::|24|20101224|allocated
apnic|JP|ipv6|240f:100::|24|20171027|allocated
apnic|CN|ipv6|240f:4000::|24|20190709|allocated
apnic|CN|ipv6|240f:8000::|24|20150427|allocated
apnic|CN|ipv6|240f:c000::|24|20190917|allocated
apnic|SG|ipv6|2410::|17|20241108|allocated
We will extract only the Japanese IPv4 block from the obtained delegated-apnic-latest and convert it to CIDR format.
Since the number of hosts is converted to the prefix length of CIDR, it is necessary to convert it as follows.
256 hosts → /24
512 hosts → /23
1024 hosts → /22
4096 hosts → /20
8192 hosts → /19
Here is a simple script to do the conversion.
#!/bin/sh
grep '^apnic|JP|ipv4|' delegated-apnic-latest | while IFS='|' read -r registry cc type start value date status; do
case $value in
256) echo "$start/24" ;;
512) echo "$start/23" ;;
1024) echo "$start/22" ;;
2048) echo "$start/21" ;;
4096) echo "$start/20" ;;
8192) echo "$start/19" ;;
16384) echo "$start/18" ;;
65536) echo "$start/16" ;;
*) echo "$start/$(echo "32 - l($value)/l(2)" | bc -l | cut -d. -f1)" ;;
esac
done > japan-ips.txt
# IPv6の場合
grep '^apnic|JP|ipv6|' delegated-apnic-latest | while IFS='|' read -r registry cc type start value date status; do
echo "$start/$value"
done > japan-ips-v6.txt
Check the data and if it looks like the following, it is successful.
*IPv6 implementation is not implemented at this time
$ cat japan-ips.txt
223.216.0.0/13
223.223.0.0/16
223.223.160.0/22
223.223.164.0/22
223.223.208.0/21
223.223.224.0/19
223.252.64.0/19
223.252.112.0/20
Now, we will actually use the formatted japan-ips.txt to restrict access using PF.
table <japan_ips> persist file "/etc/japan-ips.txt"
# persist → システム再起動後もテーブル内容を保持
block in quick log proto tcp from !<japan_ips> to any port 22
# !<japan_ips> → 日本IPテーブルに含まれないIPアドレスすべて
# quick → 即座にブロックで以降のルールを反映しない
# log → ブロックされたパケットをログに
block in quick log proto tcp from !
↑This command blocks IPs outside Japan. Because quick is attached, packets blocked outside of Japan will not proceed to the next evaluation.
Since all IPs from outside Japan are blocked, access from the internal network will not be authenticated unless explicitly configured. In my case, it is 192.168.3.0, but please note that the internal IPs allowed will change depending on the environment.
If you block Japanese IPs first, you will not be able to use local IPs, so be careful. We recommend blocking in the following order:
table <japan_ips> persist file "/etc/japan-ips.txt"
# persist → システム再起動後もテーブル内容を保持
pass in quick on $int_if proto tcp from 192.168.3.0/24 to any port 22
# 内向きIPの許可
# 日本のIPでブロックする前に行う
block in quick log proto tcp from !<japan_ips> to any port 22
# !<japan_ips> → 日本IPテーブルに含まれないIPアドレスすべて
# quick → 即座にブロックで以降のルールを反映しない
# log → ブロックされたパケットをログに
With this setting, attack packets from overseas IPs will be dropped before establishing a TCP connection, which can be expected to reduce the load on the CPU, etc. Basic countermeasures against impersonation of internal IPs from the outside can be implemented using antispoof, but this is only a partial measure, and appropriate settings are required for each environment.
APNIC data periodic acquisition
Regular updates of APNIC data are required in order for geographical IP restrictions to function correctly due to assignments to new carriers, changes to existing assignments, consolidation, etc.
So I created a data acquisition/conversion script (IPv4 only).
In addition to acquisition and conversion functions, it also has simple backup and PF table update functions.
#!/bin/sh
# /usr/local/bin/update-japan-ips.sh
# APNIC公式データから日本のIPアドレス範囲を取得・変換
TARGETFILE="/etc/japan-ips.txt"
BACKUP_DIR="/var/backups"
DATE=$(date '+%Y%m%d_%H%M%S')
APNIC_URL="http://ftp.apnic.net/stats/apnic/delegated-apnic-latest"
TMPFILE="/tmp/delegated-apnic-latest"
JAPAN_BLOCKS="/tmp/japan-blocks.txt"
# バックアップ作成
if [ -f "$TARGETFILE" ]; then
cp "$TARGETFILE" "$BACKUP_DIR/japan-ips.txt.backup.$DATE"
fi
# APNICデータのダウンロード
fetch -o "$TMPFILE" "$APNIC_URL"
if [ $? -eq 0 ]; then
# ファイルサイズチェック
FILESIZE=$(stat -f%z "$TMPFILE" 2>/dev/null || echo 0)
if [ "$FILESIZE" -gt 100000 ]; then
grep '^apnic|JP|ipv4|' "$TMPFILE" | while IFS='|' read -r registry cc type start value date status; do
# Calc CIDR
case $value in
1) cidr=32 ;;
2) cidr=31 ;;
4) cidr=30 ;;
8) cidr=29 ;;
16) cidr=28 ;;
32) cidr=27 ;;
64) cidr=26 ;;
128) cidr=25 ;;
256) cidr=24 ;;
512) cidr=23 ;;
1024) cidr=22 ;;
2048) cidr=21 ;;
4096) cidr=20 ;;
8192) cidr=19 ;;
16384) cidr=18 ;;
32768) cidr=17 ;;
65536) cidr=16 ;;
131072) cidr=15 ;;
262144) cidr=14 ;;
524288) cidr=13 ;;
1048576) cidr=12 ;;
2097152) cidr=11 ;;
4194304) cidr=10 ;;
8388608) cidr=9 ;;
16777216) cidr=8 ;;
*)
# 上記以外は対数計算でCIDR算出
cidr=$(echo "32 - l($value)/l(2)" | bc -l | cut -d. -f1)
;;
esac
echo "$start/$cidr"
done > "$JAPAN_BLOCKS"
# 抽出結果の検証とPFテーブル更新
BLOCK_COUNT=$(wc -l < "$JAPAN_BLOCKS")
# データ数チェック
if [ "$BLOCK_COUNT" -gt 50 ]; then
mv "$JAPAN_BLOCKS" "$TARGETFILE"
# pfの更新
pfctl -t japan_ips -T replace -f "$TARGETFILE" 2>/dev/null
if [ $? -ne 0 ]; then
# 更新失敗時
if [ -f "$BACKUP_DIR/japan-ips.txt.backup.$DATE" ]; then
cp "$BACKUP_DIR/japan-ips.txt.backup.$DATE" "$TARGETFILE"
pfctl -t japan_ips -T replace -f "$TARGETFILE" 2>/dev/null
fi
fi
else
rm "$JAPAN_BLOCKS"
fi
rm "$TMPFILE"
else
rm "$TMPFILE"
fi
fi
# 30日以上の古いバックアップファイルの削除
find "$BACKUP_DIR" -name "japan-ips.txt.backup.*" -mtime +30 -delete 2>/dev/null
Give it execution privileges and run it periodically with cron and you’re done. It will then update automatically.
# chmod +x /usr/local/bin/update-japan-ips.sh
# crontab -e
0 2 * * 0 /usr/local/bin/update-japan-ips.sh
Operation check
Let’s check to see if it is actually geographically blocked correctly.
This time, I will install TOR on the PC I usually use (Arch Linux) and test the connection from an overseas IP via the TOR network
$ sudo pacman -S tor torsocks
$ sudo systemctl start tor
$ torsocks curl https://httpbin.org/ip
{
"origin": "37.114.50.142"
}
Since this IP address is from Germany, you can see that it has been successfully changed from the original Japanese IP address and now we have a testing environment with a foreign IP.
$ torsocks ssh -l hoge hogehoge.hoge-hoge.net -i id_ed25519_client
1756448417 ERROR torsocks[10366]: General SOCKS server failure (in socks5_recv_connect_reply() at socks5.c:527)
ssh: connect to host bokumin45.server-on.net port 22: Connection refused
Connections from overseas IPs are successfully blocked. On the other hand, with a normal SSH connection from a Japanese IP, you can access normally as shown below, so you can confirm that the geographical restriction is working properly.
# 日本からのアクセス
$ ssh -l hoge hogehoge.hoge-hoge.net -i id_ed25519_test
_ov
.,HH' #o
?&MM?.,oooo__ `MH\
|R6M&RMH&9MMMMHb. ,MMM|
|6MHMGHMHMM&M9MH6HMHHMM}
MHMHMHMH6MH6MRMHMMHMP'
iHSD6HMHMHH&MHMMMMMH'
oMHMMHMMHM$RM9MHM9M?'
-v_ |9&RHRMMHMHMHH96MMMM!
\_ "\:HHHDM9H&M&kM&6HMHMH
. `"qod' `?*MH&R6M6MRMMMH'
`+&oo$PHbd##|``H9HHHMHHM!
H9HMb\_d9MHH6M9M?
#&MHM9M&HH&MMHHMM,
"^*HHRM96M&M9MMML
`MRHMHHMMMRMM?
,RMHHMMMHMMMM,
H&RM&6MHMMHMk
HMHMH6MHMMMMb_
_H&H96RMR6M[*MM#o\_
,/:-:)&9&*#<MH9HHMHHHM, ""**HHHH#o\_.
>?:\?d?_:/v?ZMHHHHRMM9D `"""*HH#.
'''-\|?\RM&&##+*""` .db ._HMF
oo#HMHMMMMM?
`'"' '
====================================================
PRIVATE SYSTEM - AUTHORIZED ACCESS ONLY
====================================================
Enter passphrase for key 'id_ed25519_client':
Conclusion
This time, I tried implementing geographical access restrictions on FreeBSD + PF using APNIC data. This method can significantly reduce unauthorized SSH connection attempts from overseas, and is expected to improve server security and save resources.
However, you need to be careful as the parts that have to process a large number of IP lists may affect performance. In my case, I basically use IPv4, so I narrow down the list of IPs to IPv4 only. Also, since it will be completely dependent on APNIC data, I think it will be possible to make the server more robust by not relying completely on geographical blocks and by using public keys and running systems such as Fail2ban to provide multi-layered defense.
If you are unable to configure the settings as expected, it is a good idea to use a command such as tcpdump to find out where the problem is and what is causing it, so please use this as a reference.
# 22番ポートチェック
tcpdump -n -e -ttt -i pflog0 port 22
On a different topic, I think Wyse3040 is quite energy efficient and suitable for FW servers. It runs on TDP4W, and is small and cute. However, since it only has one USB 3.0 port, you can only use up to two ports for gigabit connectivity, but it is more than sufficient for general home use.
Some people may think that Raspberry Pi is fine, but the big advantage of this PC is that it uses x86-64 architecture. This allows full-fledged firewall OSes such as FreeBSD to work, and many software not available on the ARM architecture to work without problems.
This is a mini PC that I personally recommend, as it is extremely economical with an annual electricity bill of around 1,000 yen, and there is almost no burden on the household budget even if it is operated 24 hours a day. The price range is relatively easy to obtain even on the second-hand market, so if you ever come across one, we recommend you give it a try.

End