This pf(4) configuration mostly comes from Tim’s blog, and a bit of change in order to work on FreeBSD 13.1 which also works on FreeBSD 14.3.

For the IP blocks, IPDeny.com provides regularly-updated lists of IPv4 & IPv6 ranges for each country, you can download it and put it under somewhere like /user/local/share/geoip.

user$ DEST="/usr/local/share/geoip"
user$ doas mkdir -p "$DEST/tmp"
user$ doas chmod 755 "$DEST"
user$ doas chmod 750 "$DEST/tmp"

Then download the IP blocks you want as below:

user$ DEST="/usr/local/share/geoip"
user$ V4URL="https://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone"
user$ V6URL="https://www.ipdeny.com/ipv6/ipaddresses/aggregated/cn-aggregated.zone"
user$ V4DEST="$DEST/ipv4_cn-aggregated.zone"
user$ V6DEST="$DEST/ipv6_cn-aggregated.zone"
user$ ftp -o "$V4DEST" "$V4URL"
user$ ftp -o "$V6DEST" "$V6URL"

One more word for SSH protection, it’s better to forbit Root login as well as Password login, this can be done in file /etc/ssh/sshd_config.

cat /etc/pf.conf
##########
# Macros #
##########
# see also `sysctl net.inet.ip.portrange.{first,last}`
minefield="10000:65535"
ssh_alternate_port=11222


##########
# Tables #
##########
table <bruteforce> persist
table <troublemakers> persist
table <domesticv4> persist \
  file "/usr/local/share/geoip/ipv4_cn-aggregated.zone"
table <domesticv6> persist \
  file "/usr/local/share/geoip/ipv6_cn-aggregated.zone"


###########
# Options #
###########
set skip on lo


#########
# Rules #
#########
block return    # block stateless traffic
pass      # establish keep-state

# block brute-forcers
block quick proto tcp from <bruteforce> \
  to any port $ssh_alternate_port
block quick proto tcp from <troublemakers> \
  to any port $ssh_alternate_port

# non-domestic v4/v6 connections simply not allowed to touch SSH
# IPv4 GeoIP blocks: https://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone
# IPv6 GeoIP blocks: https://www.ipdeny.com/ipv6/ipaddresses/aggregated/cn-aggregated.zone
#
block quick inet proto tcp \
  from ! <domesticv4> \
  to any port $ssh_alternate_port
block quick inet6 proto tcp \
  from ! <domesticv6> \
  to any port $ssh_alternate_port


# these made it through to the SSH port but abusing it
pass proto tcp from any to any \
  port {$ssh_alternate_port} \
  flags S/SA keep state \
  (max-src-conn 5, max-src-conn-rate 5/5, \
  overload <bruteforce> flush global \
  )

## these are just randomly probing

# port 22 is a dead-giveaway
# because we know we moved our SSH server elsewhere
# Also, we have no telnet or SMB
# so anyone poking there
# is up to no good
pass in proto tcp \
  from any \
  to any port { \
    telnet, \
    ssh, \
    pop3, \
    netbios-ns, \
    netbios-ssn, \
    microsoft-ds \
  } \
  synproxy state \
  tag trouble

# tag stuff in the range $minefield as trouble
pass in proto tcp from any to any port $minefield \
  synproxy state \
  tag trouble

# unless it's to our hole-in-one
pass in proto tcp from any to any port $ssh_alternate_port tag good

# if we're still trouble, add to the troublemakers
pass proto tcp from any to any port $ssh_alternate_port \
  tagged trouble \
  synproxy state \
  (max-src-conn 1, max-src-conn-rate 1/10, \
  overload <troublemakers> flush global \
  )

pass proto tcp from any to any port {http https} \
  keep state (max-src-conn 100, max-src-conn-rate 20/3)