It is very common that the ISP-provided modem only allows you to block all incoming IPv6 connections entirely or let all incoming requests through.
To publish your service on the internet, the only option is to tell the ISP-provided modem to let all incoming requests go through. However, this poses a security risk, which may accidentally expose private service on the internet (e.g. SMB sharing or remote desktop).
To enhance the security on such a network, each device needs to configure its firewall properly. A device needs to either block all IPv6 incoming requests or only allow connections from hosts with the same IPv6 prefix. For the sake of convenience, we obviously want the latter. But things can be complicated when you have a dynamic IPv6 prefix from your ISP.
Generally speaking, you must run a script to update the firewall rules each time your prefix changes.
So the automated firewall rule modification can be breakdown into two parts:
- Setup firewall to allow dynamic update of rule set
- Detect IPv6 prefix changes and update the rule set
In the recent Linux, nftables is preferred over iptables.
Change your /etc/nftables.conf
to be:
#!/sbin/nft -f
flush ruleset
table inet filter {
set allowed_ipv6 {
type ipv6_addr
flags dynamic
flags interval
}
chain input {
type filter hook input priority 0; policy drop;
# Accept all connections related to connections made by us
ct state {established, related} counter accept
# Enable loopback but drop connections to loopback not from lo
iif lo accept comment "accept loopback"
iif != lo ip daddr 127.0.0.1/8 counter drop
iif != lo ip6 daddr ::1/128 counter drop
# Accept all ICMP including ping
ip protocol icmp counter accept
meta l4proto ipv6-icmp counter accept
# Accept all IPv4 incoming as we are behind NAT
ip saddr 0.0.0.0/0 accept
# Allow incoming IPv6 only if the source is in the set
ip6 saddr @allowed_ipv6 accept
tcp dport 22 counter accept comment "accept SSH"
#counter comment "count dropped packets"
}
chain forward {
type filter hook forward priority 0; policy drop;
counter comment "count dropped packets"
}
chain output {
type filter hook output priority 0; policy accept;
counter comment "count accepted packets"
}
}
This script drops all incoming connections except:
- ICMP, you need this for IPv6 access. Also covers ping.
- Port 22, for ssh. Assume you have enhanced the security of your ssh server. (e.g. pubkey-only login for public access)
- Allow all IPv4 incoming connections for convenience. This is safe as long as we are behind NAT.
To load the rule:
sudo nft -f /etc/nftables.conf
On Debian, to load nftables rules on startup, see https://www.naturalborncoder.com/2024/10/installing-and-configuring-nftables-on-debian/#google_vignette
On Ubuntu, make sure you have disabled UFW first.
To list allowed IPv6 ranges:
sudo nft list set inet filter allowed_ipv6
To add an IPv6 range to the allowed set:
sudo nft add element inet filter allowed_ipv6 {2401:d000:114:514::/64}
To remove an IPv6 range from the allowed set:
sudo nft delete element inet filter allowed_ipv6 {2401:d000:114:514::/64}
To clear the allowed set:
sudo nft flush set inet filter allowed_ipv6
#!/bin/bash
function process_route6_change() {
local action="$1"
local ipv6="$2"
local flags="$3"
local ipv6_netmask_size;
local netmask;
if [[ "$action" != 'add' && "$action" != 'delete' ]]; then
return
fi
if [[ "$ipv6" == 'default' ]]; then
return
fi
# Check if it's a ULA (fc00::/7) or Link-Local (fe80::/10), if so, return
if [[ "$ipv6" =~ ^fc[0-9a-fA-F]{2}:|^fd[0-9a-fA-F]{2}:|^fe8[0-9a-bA-B]: ]]; then
return
fi
# Extract the netmask size from CIDR notation
if [[ "$ipv6" == */* ]]; then
ipv6_netmask_size="${ipv6#*/}"
else
return
fi
[[ $ipv6_netmask_size -ne 64 ]] && return
netmask=$(netmask "$ipv6")
if [[ "$action" == 'add' ]]; then
echo Add to allowed_ipv6: "$netmask"
sudo nft add element inet filter allowed_ipv6 { "$netmask" }
else
echo Delete from allowed_ipv6: "$netmask"
sudo nft delete element inet filter allowed_ipv6 { "$netmask" }
fi
}
function foreach_route6_changes() {
local line="$1";
local action;
local content;
if [[ "$line" == local* || "$line" == "Deleted local"* ]]; then
return
fi
if [[ "$line" == Deleted\ * ]]; then
action='delete'
content="${line#Deleted }"
else
action='add'
content="$line"
fi
local ipv6="${content%% *}"
if [[ "$ipv6" == 'multicast' ]]; then
if [[ "$action" == 'delete' ]]; then
action='delete-multicast'
else
action='add-multicast'
fi
content="${content#multicast }"
ipv6="${content%% *}"
fi
local flags="${content#* }"
process_route6_change "$action" "$ipv6" "$flags"
}
ip -6 route | while IFS= read -r line; do
foreach_route6_changes "$line"
done
echo 'Start monitoring route changes'
ip -6 monitor route | while IFS= read -r line; do
foreach_route6_changes "$line"
done
This script checks for IPv6 prefix change and print out a log message whenever it finds a change.
- Save this file to
/opt/monitor-route6.sh
- Set up a systemd task (
/etc/systemd/system/monitor-route6.service
) to run this script at the background on boot:
[Unit]
Description=Monitor IPv6 route changes
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/opt/monitor-route6.sh
Restart=always
User=root
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
The script needs to be run as root to modify nftables set.