Files
nym/scripts/nym-node-setup/network-tunnel-manager.sh
2026-05-20 08:30:49 +00:00

1750 lines
57 KiB
Bash

#!/bin/bash
# nym host network firewall, tunnel and wireguard exit policy manager
# run this script as root
set -euo pipefail
set +o errtrace
###############################################################################
# colors (no emojis)
###############################################################################
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m'
CYAN='\033[0;36m'
RESET='\033[0m'
info() {
printf "%b\n" "${YELLOW}[INFO] $*${NC}"
}
warn() {
printf "%b\n" "${YELLOW}[WARN] $*${NC}"
}
ok() {
printf "%b\n" "${GREEN}[OK] $*${NC}"
}
error() {
printf "%b\n" "${RED}[ERROR] $*${NC}"
}
###############################################################################
# safety: must run as root, jq
###############################################################################
if [ "$(id -u)" -ne 0 ]; then
error "This script must be run as root"
exit 1
fi
###############################################################################
# Logging
###############################################################################
LOG_FILE="/var/log/nym/network_tunnel_manager.log"
mkdir -p "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"
chmod 640 "$LOG_FILE"
# rotate log if >10MB
if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then
mv "$LOG_FILE" "${LOG_FILE}.1"
touch "$LOG_FILE"
chmod 640 "$LOG_FILE"
fi
echo "----- $(date '+%Y-%m-%d %H:%M:%S') START network-tunnel-manager -----" | tee -a "$LOG_FILE"
echo -e "${CYAN}Logs are being saved locally to:${RESET} $LOG_FILE"
echo -e "${CYAN}These logs never leave your machine.${RESET}"
echo "" | tee -a "$LOG_FILE"
# safe logger
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# global redirection, strip ANSI before writing to log
add_log_redirection() {
exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE"))
exec 2> >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE") >&2)
}
add_log_redirection
trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR
START_TIME=$(date +%s)
###############################################################################
# basic config
###############################################################################
NYM_CHAIN="NYM-EXIT"
POLICY_FILE="/etc/nym/exit-policy.txt"
EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt"
TUNNEL_INTERFACE="${TUNNEL_INTERFACE:-nymtun0}"
WG_INTERFACE="${WG_INTERFACE:-nymwg}"
# Function to detect and validate uplink interface
detect_uplink_interface() {
local cmd="$1"
local dev
dev="$(eval "$cmd" 2>/dev/null | awk '{print $5}' | head -n1 || true)"
if [[ -n "$dev" && "$dev" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "$dev"
else
echo ""
fi
}
# uplink device detection, can be overridden
# Backward compatibility:
# - NETWORK_DEVICE sets both IPv4 and IPv6 uplinks.
# Preferred overrides:
# - NETWORK_DEVICE_V4
# - NETWORK_DEVICE_V6
NETWORK_DEVICE="${NETWORK_DEVICE:-}"
NETWORK_DEVICE_V4="${NETWORK_DEVICE_V4:-${NETWORK_DEVICE:-}}"
NETWORK_DEVICE_V6="${NETWORK_DEVICE_V6:-${NETWORK_DEVICE:-}}"
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
NETWORK_DEVICE_V4="$(detect_uplink_interface "ip -o route show default")"
fi
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
NETWORK_DEVICE_V4="$(detect_uplink_interface "ip -o route show default table all")"
fi
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
error "cannot determine ipv4 uplink interface. set NETWORK_DEVICE_V4 or NETWORK_DEVICE"
exit 1
fi
if [[ -z "$NETWORK_DEVICE_V6" ]]; then
NETWORK_DEVICE_V6="$(detect_uplink_interface "ip -6 -o route show default")"
fi
if [[ -z "$NETWORK_DEVICE_V6" ]]; then
NETWORK_DEVICE_V6="$(detect_uplink_interface "ip -6 -o route show default table all")"
fi
has_ipv6_uplink() {
[[ -n "${NETWORK_DEVICE_V6:-}" ]]
}
info "detected ipv4 uplink: $NETWORK_DEVICE_V4"
if has_ipv6_uplink; then
info "detected ipv6 uplink: $NETWORK_DEVICE_V6"
else
warn "could not determine ipv6 uplink interface. continuing with ipv4-only setup; ipv6-specific setup will be skipped and ipv6 tests may fail"
fi
###############################################################################
# shared helpers
###############################################################################
ensure_jq() {
info "checking for jq..."
if command -v jq >/dev/null 2>&1; then
ok "jq is already installed"
else
info "jq not found, installing..."
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y jq
if command -v jq >/dev/null 2>&1; then
ok "jq installed successfully"
else
error "failed to install jq"
exit 1
fi
fi
}
install_iptables_persistent() {
if ! dpkg -s iptables-persistent >/dev/null 2>&1; then
info "installing iptables-persistent"
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent
else
ok "iptables-persistent is already installed"
fi
}
adjust_ip_forwarding() {
info "configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf"
install -m 0644 /dev/null /etc/sysctl.d/99-nym-forwarding.conf
cat > /etc/sysctl.d/99-nym-forwarding.conf <<EOF
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
sysctl --system
local v4 v6
v4=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)
v6=$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)
if [[ "$v4" == "1" && "$v6" == "1" ]]; then
ok "ipv4 and ipv6 forwarding enabled"
else
error "warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)"
fi
}
save_iptables_rules() {
info "saving iptables rules to /etc/iptables"
mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
ok "iptables rules saved"
}
###############################################################################
# part 1: network tunnel manager (nymtun0 + nymwg base nat/forwarding)
###############################################################################
fetch_ipv6_address() {
local interface=$1
local ipv6_global_address
ipv6_global_address=$(ip -6 addr show "$interface" scope global | awk '/inet6/ {print $2}' | head -n 1)
if [[ -z "$ipv6_global_address" ]]; then
error "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings"
exit 1
else
info "using ipv6 address: $ipv6_global_address"
fi
}
fetch_and_display_ipv6() {
local ipv6_address
if ! has_ipv6_uplink; then
warn "no ipv6 uplink detected; skipping ipv6 uplink address display"
return 0
fi
ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE_V6" scope global | awk '/inet6/ {print $2}')
if [[ -z "$ipv6_address" ]]; then
error "no global ipv6 address found on $NETWORK_DEVICE_V6"
else
ok "ipv6 address on $NETWORK_DEVICE_V6: $ipv6_address"
fi
}
# dedupe / clean-up rules for an interface in FORWARD and NYM-EXIT
# keeps a single copy of each rule
remove_duplicate_rules() {
local interface="$1"
if [[ -z "$interface" ]]; then
error "Error: No interface specified. Usage: $0 remove_duplicate_rules <interface>"
exit 1
fi
info "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}"
#
# ipv4
#
local rules_v4
rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true)
if [[ -n "$rules_v4" ]]; then
info "processing ipv4 rules"
local tmp4
tmp4=$(mktemp)
printf "%s\n" "$rules_v4" | sort | uniq > "$tmp4"
local rule count cleaned chain rest match index
while IFS= read -r rule; do
[[ -z "$rule" ]] && continue
# FIX: protect grep from rule content becoming flags
count=$(printf "%s\n" "$rules_v4" | grep -F -- "$rule" | wc -l)
if [[ "$count" -gt 1 ]]; then
info "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule"
for ((i=1; i<count; i++)); do
cleaned="${rule#-A }"
chain=$(echo "$cleaned" | awk '{print $1}')
rest=$(echo "$cleaned" | cut -d' ' -f2-)
read -ra RULE_ARR <<<"$rest"
if iptables -t filter -C "$chain" "${RULE_ARR[@]}" 2>/dev/null; then
iptables -t filter -D "$chain" "${RULE_ARR[@]}" && continue
fi
match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true)
if [[ -n "$match" ]]; then
chain=$(echo "$match" | awk '{print $2}')
index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}')
if [[ -n "$index" ]]; then
iptables -D "$chain" "$index" 2>/dev/null || \
error "warning: failed deleting ipv4 duplicate via index ($chain $index)"
else
error "warning: unable to locate ipv4 duplicate index for: $rule"
fi
else
error "warning: could not reliably match ipv4 duplicate rule: $rule"
fi
done
fi
done < "$tmp4"
rm -f "$tmp4"
else
ok "no ipv4 rules found for $interface to deduplicate"
fi
#
# ipv6
#
local rules_v6
rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true)
if [[ -n "$rules_v6" ]]; then
info "processing ipv6 rules"
local tmp6
tmp6=$(mktemp)
printf "%s\n" "$rules_v6" | sort | uniq > "$tmp6"
local rule count cleaned chain rule_spec match index
while IFS= read -r rule; do
[[ -z "$rule" ]] && continue
# FIX: protect grep from interpreting rule as flags
count=$(printf "%s\n" "$rules_v6" | grep -F -- "$rule" | wc -l)
if [[ "$count" -gt 1 ]]; then
info "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule"
for ((i=1; i<count; i++)); do
cleaned="${rule#-A }"
chain="${cleaned%% *}"
rule_spec="${cleaned#"$chain" }"
read -ra RULE6_ARR <<<"$rule_spec"
if ip6tables -t filter -C "$chain" "${RULE6_ARR[@]}" 2>/dev/null; then
ip6tables -t filter -D "$chain" "${RULE6_ARR[@]}" && continue
fi
match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true)
if [[ -n "$match" ]]; then
chain=$(echo "$match" | awk '{print $2}')
index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}')
if [[ -n "$index" ]]; then
ip6tables -D "$chain" "$index" 2>/dev/null || \
error "warning: failed deleting ipv6 duplicate via index ($chain $index)"
else
error "warning: unable to locate ipv6 duplicate index for: $rule"
fi
else
error "warning: could not match ipv6 duplicate rule reliably: $rule"
fi
done
fi
done < "$tmp6"
rm -f "$tmp6"
else
ok "no ipv6 rules found for $interface to deduplicate"
fi
ok "duplicate rule scan completed for $interface"
}
apply_iptables_rules() {
local interface=$1
info "applying iptables rules for $interface using ipv4 uplink $NETWORK_DEVICE_V4${NETWORK_DEVICE_V6:+ and ipv6 uplink $NETWORK_DEVICE_V6}"
sleep 1
# ipv4 nat and forwarding
iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE
# governed by NYM-EXIT, do not add a broad FORWARD ACCEPT
if ! iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || \
iptables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE_V4" -j ACCEPT
fi
iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -I FORWARD 2 -i "$NETWORK_DEVICE_V4" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
# ipv6 nat and forwarding
if has_ipv6_uplink; then
ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE 2>/dev/null || \
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE
# governed by NYM-EXIT, do not add a broad FORWARD ACCEPT
if ! ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || \
ip6tables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE_V6" -j ACCEPT
fi
ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE_V6" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
else
warn "no ipv6 uplink detected; skipping ipv6 nat/forwarding rules for $interface"
fi
save_iptables_rules
}
check_tunnel_iptables() {
local interface=$1
info "inspecting iptables rules for $interface"
info "ipv4 forward chain:"
iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"'
echo
info "ipv6 forward chain:"
ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"'
}
check_ipv6_ipv4_forwarding() {
local result_ipv4 result_ipv6
result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward)
result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding)
ok "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && ok enabled || error not enabled)"
ok "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && ok enabled || error not enabled)"
}
check_ip_routing() {
info "ipv4 routing table:"
ip route
info "---------------------------"
info "ipv6 routing table:"
ip -6 route
}
perform_pings() {
info "performing ipv4 ping to google.com"
ping -4 -c 4 google.com || error "ipv4 ping failed"
echo "---------------------------"
info "performing ipv6 ping to google.com"
ping6 -6 -c 4 google.com || error "ipv6 ping failed"
}
joke_through_tunnel() {
ensure_jq
local interface=$1
sleep 1
echo
info "checking tunnel connectivity and fetching a joke for $interface"
info "if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6"
info "probes and external clients may still see different connectivity to your nym node"
local ipv4_address ipv6_address joke
ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1)
ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1)
if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then
error "no ip address found on $interface. unable to fetch a joke"
error "please verify your tunnel configuration and ensure the interface is up"
return 1
fi
if [[ -n "$ipv4_address" ]]; then
echo
echo "------------------------------------"
info "detected ipv4 address: $ipv4_address"
info "testing ipv4 connectivity"
echo
if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then
ok "ipv4 connectivity is working. fetching a joke"
joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke)
[[ -n "$joke" && "$joke" != "null" ]] && ok "ipv4 joke: $joke" || echo "failed to fetch a joke via ipv4"
else
error "ipv4 connectivity is not working for $interface. verify your routing and nat settings"
fi
else
error "no ipv4 address found on $interface. unable to fetch a joke via ipv4"
fi
if [[ -n "$ipv6_address" ]]; then
echo
echo "------------------------------------"
info "detected ipv6 address: $ipv6_address"
info "testing ipv6 connectivity"
echo
if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then
ok "ipv6 connectivity is working. fetching a joke"
joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke)
[[ -n "$joke" && "$joke" != "null" ]] && ok "ipv6 joke: $joke" || error "failed to fetch a joke via ipv6"
else
error "ipv6 connectivity is not working for $interface. verify your routing and nat settings"
fi
else
error "no ipv6 address found on $interface. unable to fetch a joke via ipv6"
fi
ok "joke fetching processes completed for $interface"
echo "------------------------------------"
sleep 3
echo
echo
info "connectivity testing recommendations"
info "- from another machine use wscat to test websocket connectivity on 9001"
info "- test udp connectivity on port 51822 (wireguard)"
info "- example: echo 'test' | nc -u <your-ip> 51822"
}
configure_dns_and_icmp_wg() {
info "allowing ping (icmp) and dns on this host"
iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || \
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || \
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || \
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || \
iptables -A INPUT -p tcp --dport 53 -j ACCEPT
save_iptables_rules
ok "dns and icmp configuration completed"
}
apply_smtps_465_rate_limit() {
info "adding SMTPS tcp/465 rules with rate limiting to ${NYM_CHAIN}"
# IPv4
iptables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate NEW -m hashlimit \
--hashlimit-upto 30/min --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-name smtps465v4 -j ACCEPT
iptables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
# IPv6
ip6tables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
ip6tables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate NEW -m hashlimit \
--hashlimit-upto 30/min --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-name smtps465v6 -j ACCEPT
ip6tables -A "$NYM_CHAIN" -p tcp --dport 465 -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
ok "SMTPS tcp/465 installed: NEW <= 30/min burst 60 per srcip; overflow rejected; ESTABLISHED allowed"
}
###############################################################################
# part 2: host network firewall for nym services
###############################################################################
NETWORK_FIREWALL_COMMENT="NYM-NETWORK-FW"
HOST_SSH_PORT="${HOST_SSH_PORT:-22}"
NETWORK_FIREWALL_TCP_PORTS=("$HOST_SSH_PORT" 80 443 1789 1790 8080 9000 9001 41264)
NETWORK_FIREWALL_UDP_PORTS=(4443 51822 51264)
NETWORK_FIREWALL_WG_TCP_PORT=51830
validate_port_value() {
local name="$1"
local value="$2"
if ! [[ "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 || 10#$value > 65535 )); then
error "invalid ${name}='${value}'. expected integer 1-65535"
exit 1
fi
}
validate_port_value "HOST_SSH_PORT" "$HOST_SSH_PORT"
build_network_firewall_status_pattern() {
local patterns=("$NETWORK_FIREWALL_COMMENT")
local port
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}" "${NETWORK_FIREWALL_UDP_PORTS[@]}" "$NETWORK_FIREWALL_WG_TCP_PORT"; do
patterns+=("dpt:${port}")
done
local IFS='|'
printf '%s' "${patterns[*]}"
}
delete_managed_input_rules() {
local cmd="$1"
while read -r rule; do
[[ -z "$rule" ]] && continue
local spec
spec="${rule#-A INPUT }"
$cmd -D INPUT $spec 2>/dev/null || true
done < <($cmd -S INPUT | grep -F -- "$NETWORK_FIREWALL_COMMENT" || true)
}
add_input_port_rule() {
local cmd="$1"
local port="$2"
local protocol="$3"
local iface="${4:-}"
if [[ -n "$iface" ]]; then
if ! $cmd -C INPUT -i "$iface" -p "$protocol" --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
$cmd -A INPUT -i "$iface" -p "$protocol" --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT
ok "added $cmd INPUT $protocol port $port on $iface"
fi
else
if ! $cmd -C INPUT -p "$protocol" --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
$cmd -A INPUT -p "$protocol" --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT
ok "added $cmd INPUT $protocol port $port"
fi
fi
}
configure_network_firewall() {
info "configuring host network firewall for nym node services"
delete_managed_input_rules iptables
delete_managed_input_rules ip6tables
local port
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}"; do
add_input_port_rule iptables "$port" tcp
add_input_port_rule ip6tables "$port" tcp
done
for port in "${NETWORK_FIREWALL_UDP_PORTS[@]}"; do
add_input_port_rule iptables "$port" udp
add_input_port_rule ip6tables "$port" udp
done
add_input_port_rule iptables "$NETWORK_FIREWALL_WG_TCP_PORT" tcp "$WG_INTERFACE"
add_input_port_rule ip6tables "$NETWORK_FIREWALL_WG_TCP_PORT" tcp "$WG_INTERFACE"
save_iptables_rules
ok "host network firewall configuration completed"
}
show_network_firewall_status() {
info "managed host network firewall rules"
local status_pattern
status_pattern="$(build_network_firewall_status_pattern)"
echo
info "ipv4 input rules:"
iptables -L INPUT -n -v --line-numbers | grep -E "(${status_pattern})" || true
echo
info "ipv6 input rules:"
ip6tables -L INPUT -n -v --line-numbers | grep -E "(${status_pattern})" || true
}
test_network_firewall_rules() {
info "testing host network firewall rules"
local failures=0
local port
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}"; do
if iptables -C INPUT -p tcp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv4 tcp port $port allowed"
else
error "ipv4 tcp port $port missing"
((failures++))
fi
if ip6tables -C INPUT -p tcp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv6 tcp port $port allowed"
else
error "ipv6 tcp port $port missing"
((failures++))
fi
done
for port in "${NETWORK_FIREWALL_UDP_PORTS[@]}"; do
if iptables -C INPUT -p udp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv4 udp port $port allowed"
else
error "ipv4 udp port $port missing"
((failures++))
fi
if ip6tables -C INPUT -p udp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv6 udp port $port allowed"
else
error "ipv6 udp port $port missing"
((failures++))
fi
done
if iptables -C INPUT -i "$WG_INTERFACE" -p tcp --dport "$NETWORK_FIREWALL_WG_TCP_PORT" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv4 tcp port $NETWORK_FIREWALL_WG_TCP_PORT allowed on $WG_INTERFACE"
else
error "ipv4 tcp port $NETWORK_FIREWALL_WG_TCP_PORT missing on $WG_INTERFACE"
((failures++))
fi
if ip6tables -C INPUT -i "$WG_INTERFACE" -p tcp --dport "$NETWORK_FIREWALL_WG_TCP_PORT" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
ok "ipv6 tcp port $NETWORK_FIREWALL_WG_TCP_PORT allowed on $WG_INTERFACE"
else
error "ipv6 tcp port $NETWORK_FIREWALL_WG_TCP_PORT missing on $WG_INTERFACE"
((failures++))
fi
return "$failures"
}
###############################################################################
# part 3: wireguard exit policy manager
###############################################################################
add_port_rules() {
local cmd="$1" # iptables or ip6tables
local port="$2"
local protocol="${3:-tcp}"
if [[ "$port" == *"-"* ]]; then
local start_port end_port
start_port=$(echo "$port" | cut -d'-' -f1)
end_port=$(echo "$port" | cut -d'-' -f2)
if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then
$cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT
ok "added $cmd $NYM_CHAIN $protocol port range $start_port:$end_port"
fi
else
if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then
$cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT
ok "added $cmd $NYM_CHAIN $protocol port $port"
fi
fi
}
exit_policy_install_deps() {
install_iptables_persistent
for item in iptables ip6tables ip grep sed awk wget curl; do
if ! command -v "$item" >/dev/null 2>&1; then
info "installing dependency: $item"
apt-get install -y "$item"
fi
done
}
create_nym_chain() {
info "creating nym exit policy chain $NYM_CHAIN"
# create/flush chain
if iptables -S "$NYM_CHAIN" >/dev/null 2>&1; then
iptables -F "$NYM_CHAIN"
else
iptables -N "$NYM_CHAIN"
fi
if ip6tables -S "$NYM_CHAIN" >/dev/null 2>&1; then
ip6tables -F "$NYM_CHAIN"
else
ip6tables -N "$NYM_CHAIN"
fi
# remove *all* FORWARD -> NYM-EXIT jumps
while read -r rule; do
spec="${rule#-A FORWARD }"
iptables -D FORWARD $spec 2>/dev/null || true
done < <(iptables -S FORWARD | grep -F " -j $NYM_CHAIN" || true)
while read -r rule; do
spec="${rule#-A FORWARD }"
ip6tables -D FORWARD $spec 2>/dev/null || true
done < <(ip6tables -S FORWARD | grep -F " -j $NYM_CHAIN" || true)
# remove broad ACCEPT rules for wg + tun outbound so NYM-EXIT is authoritative
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || true
if has_ipv6_uplink; then
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || true
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || true
fi
# install the correct hook for both wg + tun
iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN"
iptables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN"
if has_ipv6_uplink; then
ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN"
ip6tables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN"
ok "NYM-EXIT chain ready + IPv4/IPv6 FORWARD hooks installed for $WG_INTERFACE and $TUNNEL_INTERFACE"
else
warn "no ipv6 uplink detected; installing only IPv4 FORWARD hooks for $WG_INTERFACE and $TUNNEL_INTERFACE"
ok "NYM-EXIT chain ready + IPv4 FORWARD hooks installed for $WG_INTERFACE and $TUNNEL_INTERFACE"
fi
}
setup_nat_rules() {
info "setting up nat and forwarding rules for $WG_INTERFACE via ipv4 uplink $NETWORK_DEVICE_V4${NETWORK_DEVICE_V6:+ and ipv6 uplink $NETWORK_DEVICE_V6}"
if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE 2>/dev/null; then
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE
fi
if has_ipv6_uplink; then
if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE 2>/dev/null; then
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE
fi
else
warn "no ipv6 uplink detected; skipping ipv6 NAT setup for $WG_INTERFACE"
fi
# keep reverse RELATED,ESTABLISHED in FORWARD for return traffic.
if ! iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
iptables -I FORWARD 2 -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
fi
if has_ipv6_uplink; then
if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
fi
fi
}
configure_exit_dns_and_icmp() {
info "ensuring dns and icmp are allowed inside nym exit chain"
# remove any existing DNS/ICMP rules first to avoid duplicates
iptables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true
iptables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true
iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || true
iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || true
ip6tables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true
ip6tables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true
ip6tables -D "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null || true
# insert rules at the beginning in correct order: DNS first, then ICMP
iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT
iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT
iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT
iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT
ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT
ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT
ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT
}
apply_port_allowlist() {
echo "applying allowed port list into ${NYM_CHAIN}"
configure_exit_dns_and_icmp
declare -A PORT_MAPPINGS=(
["FTP"]="20-21"
["SSH"]="22"
["WHOIS"]="43"
["DNS"]="53"
["Finger"]="79"
["HTTP"]="80-81"
["Kerberos"]="88"
["POP3"]="110"
["UseNet1"]="119"
["NTP"]="123"
["IMAP"]="143"
["IMAP3"]="220"
["SSHAlternative1"]="223"
["LDAP"]="389"
["HTTPS"]="443"
["SMBWindowsFileShare"]="445"
["Kpasswd"]="464"
# this port is opened and rate limited in apply_smtps_465_rate_limit
# ["SMTP"]="465"
["RTSP"]="554"
["UseNet2"]="563"
["SMTPSubmission"]="587"
["TelegramVoiceVideo"]="596-599"
["LDAPS"]="636"
["SILC"]="706"
["KerberosAdmin"]="749"
["DNSOverTLS"]="853"
["Rsync"]="873"
["VMware"]="902-904"
["RemoteHTTPS"]="981"
["FTPOverTLS"]="989-990"
["NetnewsAdmin"]="991"
["TelnetOverTLS"]="992"
["IMAPOverTLS"]="993"
["POP3OverTLS"]="995"
["WorldOfWorldcraft1"]="1119-1120"
["OpenVPN"]="1194"
["WireGuardPeer"]="51820-51822"
["QTServerAdmin"]="1220"
["PKTKRB"]="1293"
["TelegramMTProto"]="1400"
["MSSQL"]="1433"
["VLSILicenseManager"]="1500"
["OracleDB"]="1521"
["Sametime"]="1533"
["GroupWise"]="1677"
["PPTP"]="1723"
["RTSPAlt"]="1755"
["MSNP"]="1863"
["Gemini"]="1965"
["NFS"]="2049"
["DiscordVoiceChat2"]="2053"
["CPanel"]="2082-2083"
["GNUnet"]="2086-2087"
["NBX"]="2095-2096"
["Zephyr"]="2102-2104"
["SSHAlternative2"]="2222"
["DeathStrandingGaming"]="2703"
["Zwift"]="3022-3025"
["XboxLive"]="3074"
["MySQL"]="3306"
["MoneroMiningPools1"]="3333"
["SteamGaming1"]="3478-3480"
["SVN"]="3690"
["WorldOfWorldcraft2"]="3724"
["WorldOfWorldcraft3"]="4000"
["RWHOIS"]="4321"
["SteamGaming2"]="4379-4380"
["MoneroMiningPools2"]="4444"
["Virtuozzo"]="4643"
["RTPVOIP"]="5000-5005"
["MMCC"]="5050"
["WorldOfWorldcraft8"]="5060-5062"
["ICQ"]="5190"
["XMPP"]="5222-5223"
["AndroidMarket"]="5228"
["PostgreSQL"]="5432"
["WorldOfWorldcraft6"]="6012"
["WorldOfWorldcraft7"]="6112-6120"
["WorldOfWorldcraft9"]="6250"
["WebSocket"]="6300"
["WorldOfWorldcraft4"]="1119-1120"
["WorldOfWorldcraft5"]="8080"
["Electrum"]="8082"
["WorldOfWorldcraft6"]="8085"
["SimplifyMedia"]="8087-8088"
["Zcash"]="8232-8233"
["Bitcoin"]="8332-8333"
["HTTPSALT"]="8443"
["TeamSpeak"]="8767"
["MQTTS"]="8883"
["HTTPProxy"]="8888"
["TorORPort"]="9001"
["TorDirPort"]="9030"
["Tari"]="9053"
["LiteCoinP2P"]="9333"
["Gaming"]="9339"
["Git"]="9418"
["HTTPSALT2"]="9443"
["Lightning"]="9735"
["TeamSpeakVoice"]="9987"
["DashNetwork"]="9999"
["NDMP"]="10000"
["TeamSpeakQuery"]="10011-10080"
["TeamSpeakHTTPS"]="10443"
["OpenPGP"]="11371"
["MoneroMiningPools1"]="14444"
["Monero"]="18080-18081"
["MoneroRPC"]="18089"
["GoogleVoice"]="19294-19344"
["EnsimControlPanel"]="19638"
["Session"]="22021"
["DarkFiTor"]="25551"
["Minecraft"]="25565"
["DarkFi"]="26661"
# ["MongoDBDefault"]="27017" # Within Steam range
["Steam"]="27000-27050"
["WhatsAppRange"]="3478-3484"
["TeamSpeakTSDNS"]="41144"
["DiscordVoiceChat1"]="50000-65535"
# ["ElectrumSSL"]="50002" # Within DiscordVoiceChat1 range
["MOSH"]="60000-61000"
["Mumble"]="64738"
["Metadata"]="51830"
)
local port
for service in "${!PORT_MAPPINGS[@]}"; do
port="${PORT_MAPPINGS[$service]}"
echo "adding rules for $service (ports $port)"
add_port_rules iptables "$port" "tcp"
add_port_rules ip6tables "$port" "tcp"
add_port_rules iptables "$port" "udp"
add_port_rules ip6tables "$port" "udp"
done
}
apply_spamhaus_blocklist() {
info "applying spamhaus-like blocklist from $EXIT_POLICY_LOCATION"
mkdir -p "$(dirname "$POLICY_FILE")"
if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then
error "failed to download exit policy, using minimal blocklist"
cat >"$POLICY_FILE" <<EOF
ExitPolicy reject 5.188.10.0/23:*
ExitPolicy reject 31.132.36.0/22:*
ExitPolicy reject 37.9.42.0/24:*
ExitPolicy reject 45.43.128.0/18:*
ExitPolicy reject *:*
EOF
fi
local tmpfile
tmpfile=$(mktemp)
grep "^ExitPolicy reject" "$POLICY_FILE" | grep -v "\*:\*" > "$tmpfile"
local total_rules
total_rules=$(wc -l < "$tmpfile")
info "processing $total_rules blocklist rules"
local line ip_range
while IFS= read -r line; do
[[ -z "$line" ]] && continue
ip_range=$(echo "$line" | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/')
if [[ -n "$ip_range" ]]; then
# insert blocklist BEFORE the allowlist (after DNS/ICMP bootstrap rules)
# ipv4 reject (DNS/ICMP occupy positions 1-4)
if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null; then
iptables -I "$NYM_CHAIN" 5 -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable \
|| error "warning: failed adding ipv4 reject for $ip_range"
fi
# ipv6 reject (DNS/ICMP occupy positions 1-3)
if [[ "$ip_range" == *":"* ]]; then
if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null; then
ip6tables -I "$NYM_CHAIN" 4 -d "$ip_range" -j REJECT --reject-with icmp6-port-unreachable \
|| error "warning: failed adding ipv6 reject for $ip_range"
fi
fi
fi
done < "$tmpfile"
rm -f "$tmpfile"
}
add_default_reject_rule() {
info "ensuring default reject rule at end of ${NYM_CHAIN}"
iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true
iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true
ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true
ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true
iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable
ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable
}
clear_exit_policy_rules() {
info "clearing nym exit policy rules ..."
iptables -F "$NYM_CHAIN" 2>/dev/null || true
ip6tables -F "$NYM_CHAIN" 2>/dev/null || true
# remove hooks for BOTH wg + tun
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null || true
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null || true
if has_ipv6_uplink; then
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null || true
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null || true
fi
iptables -X "$NYM_CHAIN" 2>/dev/null || true
ip6tables -X "$NYM_CHAIN" 2>/dev/null || true
}
show_exit_policy_status() {
info "nym exit policy status"
info "ipv4 network device: $NETWORK_DEVICE_V4"
if has_ipv6_uplink; then
info "ipv6 network device: $NETWORK_DEVICE_V6"
else
warn "ipv6 network device: not detected"
fi
info "wireguard interface: $WG_INTERFACE"
info "tunnel interface: $TUNNEL_INTERFACE"
echo
if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then
error "warning: wireguard interface $WG_INTERFACE not found"
else
info "wireguard interface details:"
ip link show "$WG_INTERFACE"
echo
info "wireguard ipv4 addresses:"
ip -4 addr show dev "$WG_INTERFACE"
echo
info "wireguard ipv6 addresses:"
ip -6 addr show dev "$WG_INTERFACE"
fi
echo
if ! ip link show "$TUNNEL_INTERFACE" >/dev/null 2>&1; then
error "warning: tunnel interface $TUNNEL_INTERFACE not found"
else
info "tunnel interface details:"
ip link show "$TUNNEL_INTERFACE"
echo
info "tunnel ipv4 addresses:"
ip -4 addr show dev "$TUNNEL_INTERFACE"
echo
info "tunnel ipv6 addresses:"
ip -6 addr show dev "$TUNNEL_INTERFACE"
fi
echo
info "iptables chains for ${NYM_CHAIN}:"
iptables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv4 chain not found"
echo
ip6tables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv6 chain not found"
echo
show_network_firewall_status
echo
info "ip forwarding:"
echo "ipv4: $(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)"
echo "ipv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)"
}
test_exit_policy_connectivity() {
info "testing connectivity through $WG_INTERFACE"
local iface_info
iface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null || true)
if [[ -z "$iface_info" ]]; then
error "interface $WG_INTERFACE not found"
return 1
fi
ok "interface:"
ok "$iface_info"
local ipv4_address ipv6_address
ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | awk '/inet / {print $2}' | cut -d'/' -f1 | head -n1)
ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | awk '/inet6/ {print $2}' | cut -d'/' -f1 | head -n1)
ok "ipv4 address: ${ipv4_address:-none}"
ok "ipv6 address: ${ipv6_address:-none}"
if [[ -n "$ipv4_address" ]]; then
echo -e "${NC}testing ipv4 ping to 8.8.8.8 ..."
timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \
ok "ipv4 ping ok" || error "ipv4 ping failed"
echo -e "${NC}testing ipv4 dns resolution ..."
timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \
ok "ipv4 dns ok" || error "ipv4 dns failed"
fi
if [[ -n "$ipv6_address" ]]; then
echo -e "${NC}testing ipv6 ping to google dns ..."
timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \
ok "ipv6 ping ok" || error "ipv6 ping failed"
echo -e "${NC}testing ipv6 dns resolution ..."
timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \
ok "ipv6 dns ok" || error "ipv6 dns failed"
fi
ok "connectivity tests finished"
}
###############################################################################
# part 4: check the firewall setup
###############################################################################
firewall_rule_line() {
local chain=$1
local rule_idx=$2
# this is because thefirst rule appears on line 3
iptables -L "$chain" -n --line-numbers | sed -n "$((rule_idx + 2))p"
}
check_forward_chain() {
local errors=0
info "checking FORWARD hooks and reverse RELATED,ESTABLISHED rules"
if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv4 FORWARD hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
else
error "ipv4 FORWARD hook missing (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
errors=1
fi
if iptables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv4 FORWARD hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
else
error "ipv4 FORWARD hook missing (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
errors=1
fi
if iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
ok "ipv4 reverse RELATED,ESTABLISHED ok (wg): -i $NETWORK_DEVICE_V4 -o $WG_INTERFACE"
else
error "ipv4 reverse RELATED,ESTABLISHED missing (wg): -i $NETWORK_DEVICE_V4 -o $WG_INTERFACE"
errors=1
fi
if iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$TUNNEL_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
ok "ipv4 reverse RELATED,ESTABLISHED ok (tun): -i $NETWORK_DEVICE_V4 -o $TUNNEL_INTERFACE"
else
error "ipv4 reverse RELATED,ESTABLISHED missing (tun): -i $NETWORK_DEVICE_V4 -o $TUNNEL_INTERFACE"
errors=1
fi
if has_ipv6_uplink; then
if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv6 FORWARD hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
else
error "ipv6 FORWARD hook missing (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
errors=1
fi
if ip6tables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv6 FORWARD hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
else
error "ipv6 FORWARD hook missing (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
errors=1
fi
if ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
ok "ipv6 reverse RELATED,ESTABLISHED ok (wg): -i $NETWORK_DEVICE_V6 -o $WG_INTERFACE"
else
error "ipv6 reverse RELATED,ESTABLISHED missing (wg): -i $NETWORK_DEVICE_V6 -o $WG_INTERFACE"
errors=1
fi
if ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$TUNNEL_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
ok "ipv6 reverse RELATED,ESTABLISHED ok (tun): -i $NETWORK_DEVICE_V6 -o $TUNNEL_INTERFACE"
else
error "ipv6 reverse RELATED,ESTABLISHED missing (tun): -i $NETWORK_DEVICE_V6 -o $TUNNEL_INTERFACE"
errors=1
fi
else
warn "no ipv6 uplink detected; skipping ipv6 FORWARD validation"
fi
return $errors
}
check_nym_exit_chain() {
local errors=0
local patterns=("udp.*dpt:53" "tcp.*dpt:53" "icmp.*type 8" "icmp.*type 0")
for idx in "${!patterns[@]}"; do
local line
line=$(firewall_rule_line "$NYM_CHAIN" $((idx + 1)))
if [[ "$line" =~ ${patterns[$idx]} ]]; then
ok "${NYM_CHAIN} rule $((idx + 1)) ok (${patterns[$idx]})"
else
error "${NYM_CHAIN} rule $((idx + 1)) is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install"
errors=1
fi
done
local last_rule
last_rule=$(iptables -L "$NYM_CHAIN" -n --line-numbers | awk 'NR>2 {line=$0} END {print line}')
if [[ -z "${last_rule:-}" ]]; then
error "${NYM_CHAIN} chain is empty; re-run network-tunnel-manager.sh exit_policy_install"
errors=1
elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then
ok "${NYM_CHAIN} ends with the catch-all REJECT"
else
error "${NYM_CHAIN} final rule is not the catch-all REJECT (got: $last_rule)"
errors=1
fi
return $errors
}
check_iptables_default_policies() {
info "checking base iptables default policies (INPUT/FORWARD)"
local issues=0
local input_policy forward_policy output_policy
input_policy=$(iptables -S INPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}')
forward_policy=$(iptables -S FORWARD 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}')
output_policy=$(iptables -S OUTPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}')
if [[ -z "${input_policy:-}" ]]; then
error "unable to read INPUT default policy (iptables -S INPUT failed?)"
issues=1
else
info "INPUT default policy is ${input_policy^^}"
fi
if [[ -z "${forward_policy:-}" ]]; then
error "unable to read FORWARD default policy (iptables -S FORWARD failed?)"
issues=1
else
info "FORWARD default policy is ${forward_policy^^}"
fi
if [[ -z "${output_policy:-}" ]]; then
error "unable to read OUTPUT default policy (iptables -S OUTPUT failed?)"
issues=1
elif [[ "${output_policy^^}" != "ACCEPT" ]]; then
error "OUTPUT default policy is ${output_policy^^}; expected ACCEPT"
issues=1
else
ok "OUTPUT default policy is ACCEPT"
fi
return $issues
}
check_firewall_setup() {
info "checking ipv4 firewall ordering…"
local errors=0
check_iptables_default_policies || errors=1
check_forward_chain || errors=1
check_nym_exit_chain || errors=1
test_network_firewall_rules || errors=1
if command -v ip6tables >/dev/null 2>&1; then
info "checking ipv6 firewall ordering…"
if ip6tables -L "$NYM_CHAIN" -n --line-numbers >/dev/null 2>&1; then
if ! ip6tables -L "$NYM_CHAIN" -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then
error "ip6tables ${NYM_CHAIN} rule 1 is not UDP 53"
errors=1
fi
fi
fi
if [[ $errors -ne 0 ]]; then
error "There may be some ordering issues, it is recommended to re-run network-tunnel-manager.sh exit_policy_install and review the firewall output above."
return 1
fi
ok "It's looking good!"
return 0
}
###############################################################################
# part 5: full exit policy verification tests
###############################################################################
test_port_range_rules() {
info "testing port range rules in ${NYM_CHAIN}"
local port_ranges=(
"20-21:tcp:ftp"
"80-81:tcp:http"
"2082-2083:tcp:cpanel"
"5222-5223:tcp:xmpp"
"27000-27050:tcp:steam-sample"
"989-990:tcp:ftp-tls"
"5000-5005:tcp:rtp-voip"
"8087-8088:tcp:simplify-media"
"8232-8233:tcp:zcash"
"8332-8333:tcp:bitcoin"
"18080-18081:tcp:monero"
"3478-3484:tcp:whatsapp"
"50000-65535:tcp:discord"
"4379-4380:tcp:steam"
)
local failures=0
local start end
for entry in "${port_ranges[@]}"; do
IFS=':' read -r range proto name <<<"$entry"
start=$(echo "$range" | cut -d'-' -f1)
end=$(echo "$range" | cut -d'-' -f2)
if iptables -t filter -C "$NYM_CHAIN" -p "$proto" --dport "$start:$end" -j ACCEPT 2>/dev/null; then
ok "rule ok: $name $proto $range"
else
error "missing rule: $name $proto $range"
((failures++))
fi
done
return "$failures"
}
test_critical_services() {
info "testing critical service rules in ${NYM_CHAIN}"
local tcp_ports=(22 53 443 853 1194)
local udp_ports=(53 123 1194)
local failures=0
for port in "${tcp_ports[@]}"; do
if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then
ok "tcp port $port allowed"
else
if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -q "$port"; then
ok "tcp port $port allowed by range"
else
error "tcp port $port missing"
((failures++))
fi
fi
done
for port in "${udp_ports[@]}"; do
if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then
ok "udp port $port allowed"
else
if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -q "$port"; then
ok "udp port $port allowed by range"
else
error "udp port $port missing"
((failures++))
fi
fi
done
return "$failures"
}
test_forward_chain_hook() {
info "testing forward chain hook direction for ${NYM_CHAIN}"
local failures=0
# verify BOTH interfaces are hooked to NYM-EXIT for IPv4
if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv4 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
else
error "ipv4 forward hook missing or wrong (wg)"
((failures++))
fi
if iptables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv4 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
else
error "ipv4 forward hook missing or wrong (tun)"
((failures++))
fi
if has_ipv6_uplink; then
if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv6 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
else
error "ipv6 forward hook missing or wrong (wg)"
((failures++))
fi
if ip6tables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
ok "ipv6 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
else
error "ipv6 forward hook missing or wrong (tun)"
((failures++))
fi
else
warn "no ipv6 uplink detected; skipping ipv6 forward hook tests"
fi
return "$failures"
}
test_default_reject_rule() {
info "testing default reject rule position in ${NYM_CHAIN}"
local last_rule_v4
last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}')
if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then
error "default reject missing or not last in ipv4 chain"
return 1
fi
local last_rule_v6
last_rule_v6=$(ip6tables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}')
if [[ "$last_rule_v6" != "-A $NYM_CHAIN -j REJECT --reject-with icmp6-port-unreachable" ]]; then
error "default reject missing or not last in ipv6 chain"
return 1
fi
ok "default reject confirmed at end of ${NYM_CHAIN}"
}
exit_policy_run_tests() {
local skip_default=0
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-default-reject) skip_default=1; shift ;;
*) error "unknown test option: $1"; return 1 ;;
esac
done
local total=0
local failed=0
test_forward_chain_hook || ((failed += 1))
((total += 1))
test_port_range_rules || ((failed += 1))
((total += 1))
test_critical_services || ((failed += 1))
((total += 1))
test_network_firewall_rules || ((failed += 1))
((total += 1))
if [[ $skip_default -eq 0 ]]; then
test_default_reject_rule || ((failed += 1))
((total += 1))
fi
info "tests run: ${GREEN}$total${YELLOW}, test failed: ${RED}$failed${NC}"
if [[ $failed -eq 0 ]]; then
ok "all exit policy tests passed"
else
error "some exit policy tests failed"
fi
return "$failed"
}
###############################################################################
# part 6: high level workflows
###############################################################################
nym_tunnel_setup() {
info "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}"
check_tunnel_iptables "$TUNNEL_INTERFACE"
remove_duplicate_rules "$TUNNEL_INTERFACE"
remove_duplicate_rules "$WG_INTERFACE"
check_tunnel_iptables "$TUNNEL_INTERFACE"
adjust_ip_forwarding
apply_iptables_rules "$TUNNEL_INTERFACE"
check_tunnel_iptables "$TUNNEL_INTERFACE"
apply_iptables_rules "$WG_INTERFACE"
configure_dns_and_icmp_wg
adjust_ip_forwarding
check_ipv6_ipv4_forwarding
joke_through_tunnel "$TUNNEL_INTERFACE"
joke_through_tunnel "$WG_INTERFACE"
ok "full tunnel setup completed"
}
exit_policy_install() {
info "installing nym wireguard exit policy for ${WG_INTERFACE} via ipv4 uplink ${NETWORK_DEVICE_V4}${NETWORK_DEVICE_V6:+ and ipv6 uplink ${NETWORK_DEVICE_V6}}"
exit_policy_install_deps
adjust_ip_forwarding
create_nym_chain
setup_nat_rules
apply_port_allowlist
apply_smtps_465_rate_limit
apply_spamhaus_blocklist
add_default_reject_rule
save_iptables_rules
ok "nym exit policy installed"
}
complete_networking_configuration() {
info "starting complete networking configuration: tunnels + host firewall + exit policy"
nym_tunnel_setup
configure_network_firewall
exit_policy_install
check_firewall_setup || error "firewall order checks reported problems, please review output"
exit_policy_run_tests || error "exit policy tests reported problems, please review output"
ok "complete networking configuration finished"
}
###############################################################################
# cli
###############################################################################
cmd="${1:-help}"
log "COMMAND: $cmd ARGS: $*"
case "$cmd" in
nym_tunnel_setup)
nym_tunnel_setup
status=$?
;;
exit_policy_install)
exit_policy_install
status=$?
;;
complete_networking_configuration)
complete_networking_configuration
status=$?
;;
# nym firewall setup
configure_network_firewall)
configure_network_firewall
status=$?
;;
# tunnel manager cmds
fetch_ipv6_address_nym_tun)
fetch_ipv6_address "$TUNNEL_INTERFACE"
status=$?
;;
fetch_and_display_ipv6)
fetch_and_display_ipv6
status=$?
;;
apply_iptables_rules)
apply_iptables_rules "$TUNNEL_INTERFACE"
status=$?
;;
apply_iptables_rules_wg)
apply_iptables_rules "$WG_INTERFACE"
status=$?
;;
check_nymtun_iptables)
check_tunnel_iptables "$TUNNEL_INTERFACE"
status=$?
;;
check_nym_wg_tun)
check_tunnel_iptables "$WG_INTERFACE"
status=$?
;;
check_ipv6_ipv4_forwarding)
check_ipv6_ipv4_forwarding
status=$?
;;
check_ip_routing)
check_ip_routing
status=$?
;;
perform_pings)
perform_pings
status=$?
;;
joke_through_the_mixnet)
joke_through_tunnel "$TUNNEL_INTERFACE"
status=$?
;;
joke_through_wg_tunnel)
joke_through_tunnel "$WG_INTERFACE"
status=$?
;;
configure_dns_and_icmp_wg)
configure_dns_and_icmp_wg
status=$?
;;
adjust_ip_forwarding)
adjust_ip_forwarding
status=$?
;;
remove_duplicate_rules)
remove_duplicate_rules "${2:-}"
status=$?
;;
# exit policy manager cmds
exit_policy_status)
show_exit_policy_status
status=$?
;;
check_firewall_setup)
check_firewall_setup
status=$?
;;
exit_policy_test_connectivity)
test_exit_policy_connectivity
status=$?
;;
exit_policy_clear)
clear_exit_policy_rules
status=$?
;;
exit_policy_tests)
shift
exit_policy_run_tests "$@"
status=$?
;;
help|--help|-h)
cat <<EOF
usage: $0 <command> [args]
high level workflows:
complete_networking_configuration Install tunnel interfaces, setup networking, host firewall, iptables, wg exit policy & tests
nym_tunnel_setup Install tunnel interfaces & setup networking
configure_network_firewall Install host firewall rules for nym services
exit_policy_install Install and configure wireguard exit policy
tunnel and nat helpers:
adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d
apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE}
apply_iptables_rules_wg Apply nat/forward rules for ${WG_INTERFACE}
check_ip_routing Show ipv4 and ipv6 routes
check_ipv6_ipv4_forwarding Show ipv4/ipv6 forwarding flags
check_nym_wg_tun Inspect forward chain for ${WG_INTERFACE}
check_nymtun_iptables Inspect forward chain for ${TUNNEL_INTERFACE}
configure_dns_and_icmp_wg Allow ping and dns ports on this host
fetch_and_display_ipv6 Show ipv6 on uplink ${NETWORK_DEVICE_V6:-<none>}
fetch_ipv6_address_nym_tun Show global ipv6 address on ${TUNNEL_INTERFACE}
joke_through_the_mixnet Test via ${TUNNEL_INTERFACE} with joke
joke_through_wg_tunnel Test via ${WG_INTERFACE} with joke
perform_pings Test ipv4 and ipv6 pings
remove_duplicate_rules <iface> Deduplicate FORWARD and ${NYM_CHAIN} rules for <iface> (required).
exit policy manager:
check_firewall_setup Run ordering sanity check (dns/icmp + FORWARD jump)
exit_policy_clear Remove ${NYM_CHAIN} chains and hooks
exit_policy_install Install exit policy (iptables rules and blocklist)
exit_policy_status Show status of exit policy and forwarding
exit_policy_test_connectivity Test connectivity via ${WG_INTERFACE}
exit_policy_tests [--skip-default-reject]
Run verification tests on exit policy (options: --skip-default-reject).
environment overrides:
NETWORK_DEVICE Backward-compatible override that sets both uplinks.
NETWORK_DEVICE_V4 Auto-detected IPv4 uplink (e.g., eth0). Set manually if detection fails.
NETWORK_DEVICE_V6 Auto-detected IPv6 uplink (e.g., eth2). Optional; if unset, IPv6-specific setup is skipped.
TUNNEL_INTERFACE Default: nymtun0. Requires root privileges (sudo) to manage.
WG_INTERFACE Default: nymwg - Must match your WireGuard interface name.
HOST_SSH_PORT Default: 22. Set manually if you connect to your host's SSH daemon through another port.
EOF
status=0
;;
*)
error "unknown command: $cmd"
info "run with 'help' for usage"
exit 1
;;
esac
if [[ "$cmd" != help && "$cmd" != "--help" && "$cmd" != "-h" && ${status:-1} -eq 0 ]]; then
echo ""
echo "Logs saved locally at: $LOG_FILE"
ok "operation ${cmd} completed"
fi
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
echo "----- $(date '+%Y-%m-%d %H:%M:%S') END operation ${cmd} (status $status, duration ${ELAPSED}s) -----" >> "$LOG_FILE"
exit $status