diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index 69afc1e41d5..dbe24c5072c 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -375,7 +375,7 @@ operation check_nymtun_iptables completed successfully. ###### 5. Remove old and apply new rules for wireguad routing ```sh -/network_tunnel_manager.sh remove_duplicate_rules nymwg +./network_tunnel_manager.sh remove_duplicate_rules nymwg ./network_tunnel_manager.sh apply_iptables_rules_wg ``` diff --git a/scripts/network_tunnel_manager.sh b/scripts/network_tunnel_manager.sh deleted file mode 100644 index 58eff34565d..00000000000 --- a/scripts/network_tunnel_manager.sh +++ /dev/null @@ -1,290 +0,0 @@ -#!/bin/bash - -network_device=$(ip route show default | awk '/default/ {print $5}') -tunnel_interface="nymtun0" -wg_tunnel_interface="nymwg" - -if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y iptables-persistent -else - echo "iptables-persistent is already installed." -fi - -fetch_ipv6_address() { - local interface=$1 - ipv6_global_address=$(ip -6 addr show "$interface" scope global | grep inet6 | awk '{print $2}' | head -n 1) - - if [[ -z "$ipv6_global_address" ]]; then - echo "no globally routable IPv6 address found on $interface. Please configure IPv6 or check your network settings." - exit 1 - else - echo "using IPv6 address: $ipv6_global_address" - fi -} - -fetch_and_display_ipv6() { - ipv6_address=$(ip -6 addr show "$network_device" scope global | grep inet6 | awk '{print $2}') - if [[ -z "$ipv6_address" ]]; then - echo "no global IPv6 address found on $network_device." - else - echo "IPv6 address on $network_device: $ipv6_address" - fi -} - -remove_duplicate_rules() { - local interface=$1 - local script_name=$(basename "$0") - - if [[ -z "$interface" ]]; then - echo "error: no interface specified. please enter the interface (nymwg or nymtun0):" - read -r interface - fi - - if [[ "$interface" != "nymwg" && "$interface" != "nymtun0" ]]; then - echo "error: invalid interface '$interface'. allowed values are 'nymwg' or 'nymtun0'." >&2 - exit 1 - fi - - echo "removing duplicate rules for $interface..." - - iptables-save | grep "$interface" | while read -r line; do - sudo iptables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - ip6tables-save | grep "$interface" | while read -r line; do - sudo ip6tables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - echo "duplicates removed for $interface." - echo "!!-important-!! you need to now reapply the iptables rules for $interface." - if [ "$interface" == "nymwg" ]; then - echo "run: ./$script_name apply_iptables_rules_wg" - else - echo "run: ./$script_name apply_iptables_rules" - fi -} - -adjust_ip_forwarding() { - ipv6_forwarding_setting="net.ipv6.conf.all.forwarding=1" - ipv4_forwarding_setting="net.ipv4.ip_forward=1" - - # remove duplicate entries for these settings from the file - sudo sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sudo sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - echo "$ipv6_forwarding_setting" | sudo tee -a /etc/sysctl.conf - echo "$ipv4_forwarding_setting" | sudo tee -a /etc/sysctl.conf - - sudo sysctl -p /etc/sysctl.conf - -} - -apply_iptables_rules() { - local interface=$1 - echo "applying IPtables rules for $interface..." - sleep 2 - - sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo iptables-save | sudo tee /etc/iptables/rules.v4 - sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 -} - -check_tunnel_iptables() { - local interface=$1 - echo "inspecting IPtables rules for $interface..." - echo "---------------------------------------" - echo "IPv4 rules:" - iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' - echo "---------------------------------------" - echo "IPv6 rules:" - ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' -} - -check_ipv6_ipv4_forwarding() { - result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) - result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - echo "IPv4 forwarding is $([ "$result_ipv4" == "1" ] && echo "enabled" || echo "not enabled")." - echo "IPv6 forwarding is $([ "$result_ipv6" == "1" ] && echo "enabled" || echo "not enabled")." -} - -check_ip_routing() { - echo "IPv4 routing table:" - ip route - echo "---------------------------------------" - echo "IPv6 routing table:" - ip -6 route -} - -perform_pings() { - echo "performing IPv4 ping to google.com..." - ping -c 4 google.com - echo "---------------------------------------" - echo "performing IPv6 ping to google.com..." - ping6 -c 4 google.com -} - -joke_through_tunnel() { - local interface=$1 - local green="\033[0;32m" - local reset="\033[0m" - local red="\033[0;31m" - local yellow="\033[0;33m" - - sleep 1 - echo - echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface...${reset}" - echo -e "${yellow}if these test succeeds, it confirms your machine can reach the outside world via IPv4 and IPv6.${reset}" - echo -e "${yellow}however, probes and external clients may experience different connectivity to your nym-node.${reset}" - - 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 - echo -e "${red}no IP address found on $interface. unable to fetch a joke.${reset}" - echo -e "${red}please verify your tunnel configuration and ensure the interface is up.${reset}" - return 1 - fi - - if [[ -n "$ipv4_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv4 address: $ipv4_address" - echo -e "testing IPv4 connectivity..." - echo - - if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv4 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv4 joke: $joke${reset}" || echo -e "failed to fetch a joke via IPv4." - else - echo -e "${red}IPv4 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv4 address found on $interface. unable to fetch a joke via IPv4.${reset}" - fi - - if [[ -n "$ipv6_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv6 address: $ipv6_address" - echo -e "testing IPv6 connectivity..." - echo - - if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv6 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via IPv6.${reset}" - else - echo -e "${red}IPv6 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv6 address found on $interface. unable to fetch a joke via IPv6.${reset}" - fi - - echo -e "${green}joke fetching processes completed for $interface.${reset}" - echo -e "------------------------------------" - - sleep 3 - echo - echo - echo -e "${yellow}### connectivity testing recommendations ###${reset}" - echo -e "${yellow}- use the following command to test WebSocket connectivity from an external client:${reset}" - echo -e "${yellow} wscat -c wss://:9001 ${reset}" - echo -e "${yellow}- test UDP connectivity on port 51822 (commonly used for nym wireguard) ${reset}" - echo -e "${yellow} from another machine, use tools like nc or socat to send UDP packets ${reset}" - echo -e "${yellow} echo 'test message' | nc -u 51822 ${reset}" - echo -e "${yellow}if connectivity issues persist, ensure port forwarding and firewall rules are correctly configured ${reset}" - echo -} - - -configure_dns_and_icmp_wg() { - echo "allowing icmp (ping)..." - sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - - echo "allowing dns over udp (port 53)..." - sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT - - echo "allowing dns over tcp (port 53)..." - sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT - - echo "saving iptables rules..." - sudo iptables-save >/etc/iptables/rules.v4 - - echo "dns and icmp configuration completed." -} - -case "$1" in -fetch_ipv6_address_nym_tun) - fetch_ipv6_address "$tunnel_interface" - ;; -fetch_and_display_ipv6) - fetch_and_display_ipv6 - ;; -apply_iptables_rules) - apply_iptables_rules "$tunnel_interface" - ;; -apply_iptables_rules_wg) - apply_iptables_rules "$wg_tunnel_interface" - ;; -check_nymtun_iptables) - check_tunnel_iptables "$tunnel_interface" - ;; -check_nym_wg_tun) - check_tunnel_iptables "$wg_tunnel_interface" - ;; -check_ipv6_ipv4_forwarding) - check_ipv6_ipv4_forwarding - ;; -check_ip_routing) - check_ip_routing - ;; -perform_pings) - perform_pings - ;; -joke_through_the_mixnet) - joke_through_tunnel "$tunnel_interface" - ;; -joke_through_wg_tunnel) - joke_through_tunnel "$wg_tunnel_interface" - ;; -configure_dns_and_icmp_wg) - configure_dns_and_icmp_wg - ;; -adjust_ip_forwarding) - adjust_ip_forwarding - ;; -remove_duplicate_rules) - remove_duplicate_rules "$2" - ;; -*) - echo "Usage: $0 [command]" - echo "Commands:" - echo " fetch_ipv6_address_nym_tun - Fetch IPv6 for nymtun0." - echo " fetch_and_display_ipv6 - Show IPv6 on default device." - echo " apply_iptables_rules - Apply IPtables rules for nymtun0." - echo " apply_iptables_rules_wg - Apply IPtables rules for nymwg." - echo " check_nymtun_iptables - Check IPtables for nymtun0." - echo " check_nym_wg_tun - Check IPtables for nymwg." - echo " check_ipv6_ipv4_forwarding - Check IPv4 and IPv6 forwarding." - echo " check_ip_routing - Display IP routing tables." - echo " perform_pings - Test IPv4 and IPv6 connectivity." - echo " joke_through_the_mixnet - Fetch a joke via nymtun0." - echo " joke_through_wg_tunnel - Fetch a joke via nymwg." - echo " configure_dns_and_icmp_wg - Allows icmp ping tests for probes alongside configuring dns" - echo " adjust_ip_forwarding - Enable IPV6 and IPV4 forwarding" - echo " remove_duplicate_rules - Remove duplicate iptables rules. Valid interfaces: nymwg, nymtun0" - exit 1 - ;; -esac - -echo "operation $1 completed successfully." diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh new file mode 100644 index 00000000000..7017adbd4db --- /dev/null +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -0,0 +1,1153 @@ +#!/bin/bash +# nym tunnel and wireguard exit policy manager +# run this script as root + +set -euo pipefail + +############################################################################### +# safety: must run as root, jq +############################################################################### +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +############################################################################### +# basic config +############################################################################### + +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 +NETWORK_DEVICE="${NETWORK_DEVICE:-}" +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default")" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default table all")" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + echo "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" + exit 1 +fi + +NYM_CHAIN="NYM-EXIT" +POLICY_FILE="/etc/nym/exit-policy.txt" +EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + +# colors (no emojis) +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + +############################################################################### +# shared helpers +############################################################################### + +ensure_jq() { + echo "checking for jq..." + + if command -v jq >/dev/null 2>&1; then + echo "jq is already installed" + else + echo "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 + echo "jq installed successfully" + else + echo "failed to install jq" + exit 1 + fi + fi +} + +install_iptables_persistent() { + if ! dpkg -s iptables-persistent >/dev/null 2>&1; then + echo -e "${YELLOW}installing iptables-persistent${NC}" + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent + else + echo -e "${GREEN}iptables-persistent is already installed${NC}" + fi +} + +adjust_ip_forwarding() { + echo -e "${YELLOW}configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf${NC}" + install -m 0644 /dev/null /etc/sysctl.d/99-nym-forwarding.conf + cat > /etc/sysctl.d/99-nym-forwarding.conf </dev/null || echo 0) + v6=$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0) + + if [[ "$v4" == "1" && "$v6" == "1" ]]; then + echo -e "${GREEN}ipv4 and ipv6 forwarding enabled${NC}" + else + echo -e "${RED}warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)${NC}" + fi +} + +save_iptables_rules() { + echo -e "${YELLOW}saving iptables rules to /etc/iptables${NC}" + mkdir -p /etc/iptables + iptables-save > /etc/iptables/rules.v4 + ip6tables-save > /etc/iptables/rules.v6 + echo -e "${GREEN}iptables rules saved${NC}" +} + +############################################################################### +# 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 + echo "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings" + exit 1 + else + echo "using ipv6 address: $ipv6_global_address" + fi +} + +fetch_and_display_ipv6() { + local ipv6_address + ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE" scope global | awk '/inet6/ {print $2}') + if [[ -z "$ipv6_address" ]]; then + echo "no global ipv6 address found on $NETWORK_DEVICE" + else + echo "ipv6 address on $NETWORK_DEVICE: $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 + echo "Error: No interface specified. Usage: $0 remove_duplicate_rules " + exit 1 + fi + + echo "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 + echo "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 + echo "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" + + for ((i=1; i/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 || \ + echo "warning: failed deleting ipv4 duplicate via index ($chain $index)" + else + echo "warning: unable to locate ipv4 duplicate index for: $rule" + fi + else + echo "warning: could not reliably match ipv4 duplicate rule: $rule" + fi + done + fi + + done < "$tmp4" + + rm -f "$tmp4" + + else + echo "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 + echo "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 + echo "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" + + for ((i=1; i/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 || \ + echo "warning: failed deleting ipv6 duplicate via index ($chain $index)" + else + echo "warning: unable to locate ipv6 duplicate index for: $rule" + fi + else + echo "warning: could not match ipv6 duplicate rule reliably: $rule" + fi + done + fi + + done < "$tmp6" + + rm -f "$tmp6" + + else + echo "no ipv6 rules found for $interface to deduplicate" + fi + + echo "duplicate rule scan completed for $interface" +} + +apply_iptables_rules() { + local interface=$1 + echo "applying iptables rules for $interface using uplink $NETWORK_DEVICE" + sleep 1 + + # ipv4 nat and forwarding + iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + # ipv6 nat and forwarding + ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + ip6tables -A FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + save_iptables_rules +} + +check_tunnel_iptables() { + local interface=$1 + echo "inspecting iptables rules for $interface" + echo "ipv4 forward chain:" + iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' + echo + echo "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) + echo "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && echo enabled || echo not enabled)" + echo "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && echo enabled || echo not enabled)" +} + +check_ip_routing() { + echo "ipv4 routing table:" + ip route + echo "---------------------------" + echo "ipv6 routing table:" + ip -6 route +} + +perform_pings() { + echo "performing ipv4 ping to google.com" + ping -c 4 google.com || echo "ipv4 ping failed" + echo "---------------------------" + echo "performing ipv6 ping to google.com" + ping6 -c 4 google.com || echo "ipv6 ping failed" +} + +joke_through_tunnel() { + ensure_jq + local interface=$1 + local green="\033[0;32m" + local reset="\033[0m" + local red="\033[0;31m" + local yellow="\033[0;33m" + + sleep 1 + echo + echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface${reset}" + echo -e "${yellow}if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6${reset}" + echo -e "${yellow}probes and external clients may still see different connectivity to your nym node${reset}" + + 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 + echo -e "${red}no ip address found on $interface. unable to fetch a joke${reset}" + echo -e "${red}please verify your tunnel configuration and ensure the interface is up${reset}" + return 1 + fi + + if [[ -n "$ipv4_address" ]]; then + echo + echo "------------------------------------" + echo "detected ipv4 address: $ipv4_address" + echo "testing ipv4 connectivity" + echo + + if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then + echo -e "${green}ipv4 connectivity is working. fetching a joke${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv4 joke: $joke${reset}" || echo "failed to fetch a joke via ipv4" + else + echo -e "${red}ipv4 connectivity is not working for $interface. verify your routing and nat settings${reset}" + fi + else + echo -e "${red}no ipv4 address found on $interface. unable to fetch a joke via ipv4${reset}" + fi + + if [[ -n "$ipv6_address" ]]; then + echo + echo "------------------------------------" + echo "detected ipv6 address: $ipv6_address" + echo "testing ipv6 connectivity" + echo + + if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then + echo -e "${green}ipv6 connectivity is working. fetching a joke${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}ipv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via ipv6${reset}" + else + echo -e "${red}ipv6 connectivity is not working for $interface. verify your routing and nat settings${reset}" + fi + else + echo -e "${red}no ipv6 address found on $interface. unable to fetch a joke via ipv6${reset}" + fi + + echo -e "${green}joke fetching processes completed for $interface${reset}" + echo "------------------------------------" + + sleep 3 + echo + echo + echo -e "${yellow}connectivity testing recommendations${reset}" + echo -e "${yellow}- from another machine use wscat to test websocket connectivity on 9001${reset}" + echo -e "${yellow}- test udp connectivity on port 51822 (wireguard)${reset}" + echo -e "${yellow}- example: echo 'test' | nc -u 51822${reset}" +} + +configure_dns_and_icmp_wg() { + echo "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 + echo "dns and icmp configuration completed" +} + +############################################################################### +# part 2: 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 + echo "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 + echo "added $cmd $NYM_CHAIN $protocol port $port" + fi + fi +} + +exit_policy_install_deps() { + install_iptables_persistent + + for cmd in iptables ip6tables ip grep sed awk wget curl; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "installing dependency: $cmd" + apt-get install -y "$cmd" + fi + done +} + +create_nym_chain() { + echo "creating nym exit policy chain $NYM_CHAIN" + + if iptables -L "$NYM_CHAIN" >/dev/null 2>&1; then + iptables -F "$NYM_CHAIN" + else + iptables -N "$NYM_CHAIN" + fi + + if ip6tables -L "$NYM_CHAIN" >/dev/null 2>&1; then + ip6tables -F "$NYM_CHAIN" + else + ip6tables -N "$NYM_CHAIN" + fi + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi +} + +setup_nat_rules() { + echo "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE" + + if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + fi + if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + fi + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + fi + if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + fi + if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi +} + +configure_exit_dns_and_icmp() { + echo "ensuring dns and icmp are allowed inside nym exit chain" + + if ! iptables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT + fi + if ! iptables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT + fi + + if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT + fi + if ! iptables -C "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT + fi + if ! ip6tables -C "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT + fi +} + +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" + ["NTP"]="123" + ["IMAP"]="143" + ["IMAP3"]="220" + ["LDAP"]="389" + ["HTTPS"]="443" + ["SMBWindowsFileShare"]="445" + ["Kpasswd"]="464" + ["RTSP"]="554" + ["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" + ["OpenVPN"]="1194" + ["WireGuardPeer"]="51820-51822" + ["QTServerAdmin"]="1220" + ["PKTKRB"]="1293" + ["MSSQL"]="1433" + ["VLSILicenseManager"]="1500" + ["OracleDB"]="1521" + ["Sametime"]="1533" + ["GroupWise"]="1677" + ["PPTP"]="1723" + ["RTSPAlt"]="1755" + ["MSNP"]="1863" + ["NFS"]="2049" + ["CPanel"]="2082-2083" + ["GNUnet"]="2086-2087" + ["NBX"]="2095-2096" + ["Zephyr"]="2102-2104" + ["XboxLive"]="3074" + ["MySQL"]="3306" + ["SVN"]="3690" + ["RWHOIS"]="4321" + ["Virtuozzo"]="4643" + ["RTPVOIP"]="5000-5005" + ["MMCC"]="5050" + ["ICQ"]="5190" + ["XMPP"]="5222-5223" + ["AndroidMarket"]="5228" + ["PostgreSQL"]="5432" + ["MongoDBDefault"]="27017" + ["Electrum"]="8082" + ["SimplifyMedia"]="8087-8088" + ["Zcash"]="8232-8233" + ["Bitcoin"]="8332-8333" + ["HTTPSALT"]="8443" + ["TeamSpeak"]="8767" + ["MQTTS"]="8883" + ["HTTPProxy"]="8888" + ["TorORPort"]="9001" + ["TorDirPort"]="9030" + ["Tari"]="9053" + ["Gaming"]="9339" + ["Git"]="9418" + ["HTTPSALT2"]="9443" + ["Lightning"]="9735" + ["NDMP"]="10000" + ["OpenPGP"]="11371" + ["Monero"]="18080-18081" + ["MoneroRPC"]="18089" + ["GoogleVoice"]="19294" + ["EnsimControlPanel"]="19638" + ["DarkFiTor"]="25551" + ["Minecraft"]="25565" + ["DarkFi"]="26661" + ["Steam"]="27000-27050" + ["ElectrumSSL"]="50002" + ["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() { + echo "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 + echo "failed to download exit policy, using minimal blocklist" + cat >"$POLICY_FILE" < "$tmpfile" + + local total_rules + total_rules=$(wc -l < "$tmpfile") + echo "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 + + # ipv4 reject + if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable \ + || echo "warning: failed adding ipv4 reject for $ip_range" + fi + + # ipv6 reject + if [[ "$ip_range" == *":"* ]]; then + if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ + || echo "warning: failed adding ipv6 reject for $ip_range" + fi + fi + + fi + done < "$tmpfile" + + rm -f "$tmpfile" +} + + + +add_default_reject_rule() { + echo "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() { + echo "clearing nym exit policy rules" + + iptables -F "$NYM_CHAIN" 2>/dev/null || true + ip6tables -F "$NYM_CHAIN" 2>/dev/null || true + + iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true + ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true + + iptables -X "$NYM_CHAIN" 2>/dev/null || true + ip6tables -X "$NYM_CHAIN" 2>/dev/null || true +} + +show_exit_policy_status() { + echo "nym exit policy status" + echo "network device: $NETWORK_DEVICE" + echo "wireguard interface: $WG_INTERFACE" + echo + + if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then + echo "warning: wireguard interface $WG_INTERFACE not found" + else + echo "interface details:" + ip link show "$WG_INTERFACE" + echo + echo "ipv4 addresses:" + ip -4 addr show dev "$WG_INTERFACE" + echo + echo "ipv6 addresses:" + ip -6 addr show dev "$WG_INTERFACE" + fi + + echo + echo "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 + echo "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() { + echo "testing connectivity through $WG_INTERFACE" + + local iface_info + iface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null || true) + if [[ -z "$iface_info" ]]; then + echo "interface $WG_INTERFACE not found" + return 1 + fi + + echo "interface:" + echo "$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) + + echo "ipv4 address: ${ipv4_address:-none}" + echo "ipv6 address: ${ipv6_address:-none}" + + if [[ -n "$ipv4_address" ]]; then + echo "testing ipv4 ping to 8.8.8.8" + timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \ + echo "ipv4 ping ok" || echo "ipv4 ping failed" + + echo "testing ipv4 dns resolution" + timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \ + echo "ipv4 dns ok" || echo "ipv4 dns failed" + fi + + if [[ -n "$ipv6_address" ]]; then + echo "testing ipv6 ping to google dns" + timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \ + echo "ipv6 ping ok" || echo "ipv6 ping failed" + + echo "testing ipv6 dns resolution" + timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \ + echo "ipv6 dns ok" || echo "ipv6 dns failed" + fi + + echo "connectivity tests finished" +} + +############################################################################### +# part 3: exit policy verification tests +############################################################################### + +test_port_range_rules() { + echo "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" + ) + + 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 + echo "rule ok: $name $proto $range" + else + echo "missing rule: $name $proto $range" + ((failures++)) + fi + done + + return "$failures" +} + +test_critical_services() { + echo "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 + echo "tcp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -q "$port"; then + echo "tcp port $port allowed by range" + else + echo "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 + echo "udp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -q "$port"; then + echo "udp port $port allowed by range" + else + echo "udp port $port missing" + ((failures++)) + fi + fi + done + + return "$failures" +} + +test_forward_chain_hook() { + echo "testing forward chain hook direction for ${NYM_CHAIN}" + + local failures=0 + + if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo "ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + echo "ipv4 forward hook missing or wrong" + ((failures++)) + fi + + if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + echo "ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + echo "ipv6 forward hook missing or wrong" + ((failures++)) + fi + + return "$failures" +} + +test_default_reject_rule() { + echo "testing default reject rule at end of ${NYM_CHAIN}" + + # not sure this will really check that it is on end + if iptables -L "$NYM_CHAIN" | grep -q "REJECT"; then + echo "default reject present in ipv4 chain" + else + echo "default reject missing in ipv4 chain" + return 1 + fi +} + +exit_policy_run_tests() { + local skip_default=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-default-reject) skip_default=1; shift ;; + *) echo "unknown test option: $1"; return 1 ;; + esac + done + + local total=0 + local failed=0 + + test_forward_chain_hook || ((failed++)) + ((total++)) + + test_port_range_rules || ((failed++)) + ((total++)) + + test_critical_services || ((failed++)) + ((total++)) + + if [[ $skip_default -eq 0 ]]; then + test_default_reject_rule || ((failed++)) + ((total++)) + fi + + echo "tests run: $total, failures: $failed" + if [[ $failed -eq 0 ]]; then + echo "all exit policy tests passed" + else + echo "some exit policy tests failed" + fi + + return "$failed" +} + +############################################################################### +# part 4: high level workflows +############################################################################### + +full_tunnel_setup() { + # this mirrors your previous chain of calls but inside one script + echo "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" + + echo "full tunnel setup completed" +} + +exit_policy_install() { + echo "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}" + exit_policy_install_deps + adjust_ip_forwarding + create_nym_chain + setup_nat_rules + apply_port_allowlist + apply_spamhaus_blocklist + add_default_reject_rule + save_iptables_rules + echo "nym exit policy installed" +} + +complete_networking_configuration() { + echo "starting complete networking configuration: tunnels + exit policy" + + full_tunnel_setup + exit_policy_install + exit_policy_run_tests || echo "exit policy tests reported problems, please review output" + + echo "complete networking configuration finished" +} + +############################################################################### +# cli +############################################################################### + +cmd="${1:-help}" + +case "$cmd" in + full_tunnel_setup) + full_tunnel_setup + status=$? + ;; + exit_policy_install) + exit_policy_install + status=$? + ;; + complete_networking_configuration) + complete_networking_configuration + 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=$? + ;; + 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 < [args] + +high level workflows: + full_tunnel_setup run tunnel iptables and checks for nymtun0 and nymwg + exit_policy_install install and configure wireguard exit policy + complete_networking_configuration run tunnel setup, exit policy install and tests + +tunnel and nat helpers: + fetch_ipv6_address_nym_tun show global ipv6 address on ${TUNNEL_INTERFACE} + fetch_and_display_ipv6 show ipv6 on uplink ${NETWORK_DEVICE} + apply_iptables_rules apply nat/forward rules for ${TUNNEL_INTERFACE} + apply_iptables_rules_wg apply nat/forward rules for ${WG_INTERFACE} + check_nymtun_iptables inspect forward chain for ${TUNNEL_INTERFACE} + check_nym_wg_tun inspect forward chain for ${WG_INTERFACE} + check_ipv6_ipv4_forwarding show ipv4/ipv6 forwarding flags + check_ip_routing show ipv4 and ipv6 routes + perform_pings test ipv4 and ipv6 pings + joke_through_the_mixnet test via ${TUNNEL_INTERFACE} with joke + joke_through_wg_tunnel test via ${WG_INTERFACE} with joke + configure_dns_and_icmp_wg allow ping and dns on this host + adjust_ip_forwarding enable ipv4/ipv6 forwarding via sysctl.d + remove_duplicate_rules deduplicate rules for interface in FORWARD and ${NYM_CHAIN} + +exit policy manager: + 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_clear remove ${NYM_CHAIN} chains and hooks + exit_policy_tests [--skip-default-reject] + run verification tests on exit policy + +environment overrides: + TUNNEL_INTERFACE default nymtun0 + WG_INTERFACE default nymwg + NETWORK_DEVICE uplink device, auto-detected if not set + +EOF + status=0 + ;; + + *) + echo "unknown command: $cmd" + echo "run with 'help' for usage" + exit 1 + ;; +esac + +if [ "${status:-1}" -eq 0 ]; then + echo "operation ${cmd} completed" +fi +exit $status diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index ebae150523b..def82b1d178 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -__version__ = "1.1.0" +__version__ = "1.2.0" __default_branch__ = "develop" import os @@ -22,17 +22,25 @@ class NodeSetupCLI: def __init__(self, args): self.branch = args.dev self.welcome_message = self.print_welcome_message() - self.mode = self.prompt_mode() + self.mode = self._get_or_prompt_mode(args) self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") - self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") self.node_install_sh = self.fetch_script("nym-node-install.sh") self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") - self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html") - self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh") - self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") - self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") - self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") + self.is_gwx = self.mode == "exit-gateway" + if self.is_gwx: + self.landing_page_html = self.fetch_script("landing-page.html") + self.nginx_proxy_wss_sh = self.fetch_script("nginx_proxy_wss_sh") + self.tunnel_manager_sh = self.fetch_script("network_tunnel_manager.sh") + self.quic_bridge_deployment_sh = self.fetch_script("quic_bridge_deployment.sh") + else: + self.landing_page_html = None + self.nginx_proxy_wss_sh = None + self.tunnel_manager_sh = None + self.wg_ip_tables_manager_sh = None + self.wg_ip_tables_test_sh = None + self.quic_bridge_deployment_sh = None + def print_welcome_message(self): """Welcome user, warns for needed pre-reqs and asks for confimation""" @@ -45,7 +53,7 @@ def print_welcome_message(self): self.print_character("=", 41) msg = \ "Before you begin, make sure that:\n"\ - "1. You run this setup on Debian based Linux (ie Ubuntu)\n"\ + "1. You run this setup on Debian based Linux (ie Ubuntu 22.04 LTS)\n"\ "2. You run this installation program from a root shell\n"\ "3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\ "4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\ @@ -59,43 +67,103 @@ def print_welcome_message(self): else: print("Without confirming the points above, we cannot continue.") exit(1) + + def ensure_env_values(self, args): + """Collect env vars from args or prompt interactively, then save to env.sh.""" + env_file = Path("env.sh") + fields = [ + ("hostname", "HOSTNAME", "Enter hostname (if you don't use a DNS, press enter): "), + ("location", "LOCATION", "Enter node location (country code or name): "), + ("email", "EMAIL", "Enter your email: "), + ("moniker", "MONIKER", "Enter node public moniker (visible in explorer & NymVPN app): "), + ("description", "DESCRIPTION", "Enter short node public description: "), + ] - def prompt_mode(self): - """Ask user to insert node functionality and save it in python and bash envs""" - mode = input( - "\nEnter the mode you want to run nym-node in: " - "\n1. mixnode " - "\n2. entry-gateway " - "\n3. exit-gateway (works as entry-gateway as well) " - "\nPress 1, 2 or 3 and enter:\n" - ).strip() - - if mode in ("1", "mixnode"): - mode = "mixnode" - elif mode in ("2", "entry-gateway"): - mode = "entry-gateway" - elif mode in ("3", "exit-gateway"): - mode = "exit-gateway" - else: - print("Only numbers 1, 2 or 3 are accepted.") - raise SystemExit(1) + existing = self._read_env_file(env_file) + updated = {} - # save mode for this Python instance - self.mode = mode - os.environ["MODE"] = mode + for arg_name, key, prompt in fields: + cli_val = getattr(args, arg_name, None) + value = cli_val.strip() if cli_val else existing.get(key) or input(prompt).strip() + updated[key] = value + os.environ[key] = value + + # autodetect PUBLIC_IP if not already set + if not os.environ.get("PUBLIC_IP"): + try: + ip = subprocess.run(["curl", "-fsS4", "https://ifconfig.me"], + capture_output=True, text=True, timeout=5) + if ip.returncode == 0 and ip.stdout.strip(): + updated["PUBLIC_IP"] = ip.stdout.strip() + os.environ["PUBLIC_IP"] = ip.stdout.strip() + except subprocess.TimeoutExpired: + print("[WARN] Timeout expired while trying to fetch public IP with curl.") + except FileNotFoundError: + print("[WARN] 'curl' command not found. Please install curl or set PUBLIC_IP manually.") + except subprocess.CalledProcessError as e: + print(f"[WARN] Error while running curl to fetch public IP: {e}") + + # write all collected variables to env.sh in one go + self._upsert_env_vars(updated, env_file) + + print(f"[OK] Updated env.sh with {len(updated)} entries.") + + + + + def _upsert_env_vars(self, updates: dict, env_file: Path = Path("env.sh")): + existing = self._read_env_file(env_file) + existing.update(updates) + with env_file.open("w") as f: + for k, v in existing.items(): + f.write(f'export {k}="{v}"\n') + os.environ.update(updates) + + def _read_env_file(self, env_file: Path) -> dict: + env = {} + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("export ") and "=" in line: + k, v = line.replace("export ", "", 1).split("=", 1) + env[k.strip()] = v.strip().strip('"') + return env + + def _get_or_prompt_mode(self, args): + """Resolve MODE from --mode, env.sh, os.environ, or prompt; persist to env.sh.""" - # persist to env.sh so other scripts can source it env_file = Path("env.sh") - with env_file.open("a") as f: - f.write(f'export MODE="{mode}"\n') - # source env.sh so future bash subprocesses see it immediately - subprocess.run("source ./env.sh", shell=True, executable="/bin/bash") + # CLI arg + mode = getattr(args, "mode", None) + if mode: + mode = mode.strip().lower() + self._upsert_env_vars({"MODE": mode}) + print(f"Mode set to '{mode}' from CLI argument.") + return mode + + # env.sh (replaces manual read) + existing = self._read_env_file(env_file) + mode = existing.get("MODE") + if mode: + os.environ["MODE"] = mode + return mode + + # process env + if os.environ.get("MODE"): + return os.environ["MODE"] + + # prompt + mode = input( + "\nEnter node mode (mixnode / entry-gateway / exit-gateway): " + ).strip().lower() + if mode not in ("mixnode", "entry-gateway", "exit-gateway"): + print("Invalid mode. Must be one of: mixnode, entry-gateway, exit-gateway.") + raise SystemExit(1) + self._upsert_env_vars({"MODE": mode}) print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") return mode - def fetch_script(self, script_name): """Fetches needed scripts according to a defined mode""" # print header only the first time @@ -119,16 +187,15 @@ def _return_script_url(self, script_init_name): github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" scripts_urls = { "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", - "setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", "nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh", "landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html", - "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", - "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", - "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + "network_tunnel_manager.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/network-tunnel-manager.sh", + "quic_bridge_deployment.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/quic_bridge_deployment.sh" } + return scripts_urls[script_init_name] def run_script( @@ -200,62 +267,61 @@ def _write_temp_script(self, script_text: str) -> Path: def _check_gwx_mode(self): """Helper: Several fns run only for GWx - this fn checks this condition""" - if self.mode == "exit-gateway": - return True - else: - return False + return self.mode == "exit-gateway" - def check_wg_enabled(self): - """Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh""" + def check_wg_enabled(self, args=None): + """Determine if WireGuard is enabled; precedence: CLI > env > env.sh > prompt. Persist normalized value.""" + env_file = os.path.join(os.getcwd(), "env.sh") - env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + def norm(v): + return "true" if str(v).strip().lower() == "true" else "false" - def norm(v): # -> "true" or "false" - return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false" + val = None - # precedence: process env → env.sh → prompt - val = os.environ.get("WIREGUARD") + # CLI argument + if args and getattr(args, "wireguard", None) is not None: + val = norm(getattr(args, "wireguard")) + print(f"[INFO] WireGuard mode provided via CLI: {val}") - if val is None and os.path.isfile(env_file): - try: - with open(env_file, "r", encoding="utf-8") as f: - m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M) - if m: - val = m.group(1) - except Exception: - pass + # Environment variable + val = val or os.environ.get("WIREGUARD") + # env.sh file + if val is None: + envs = self._read_env_file(Path(env_file)) + val = envs.get("WIREGUARD") + + # Prompt if val is None: ans = input( "\nWireGuard is not configured.\n" "Nodes routing WireGuard can be listed as both entry and exit in the app.\n" - "Enable WireGuard support? (y/n): " + "Enable WireGuard support? (Y/n): " ).strip().lower() - val = "true" if ans in ("y", "yes") else "false" + val = "true" if ans in ("", "y", "yes") else "false" val = norm(val) os.environ["WIREGUARD"] = val - # persist to env.sh (replace or append) + # Persist to env.sh try: text = "" if os.path.isfile(env_file): - with open(env_file, "r", encoding="utf-8") as f: + with open(env_file, encoding="utf-8") as f: text = f.read() if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M): text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M) else: - if text and not text.endswith("\n"): - text += "\n" - text += f'export WIREGUARD="{val}"\n' + text = (text.rstrip("\n") + "\n" if text else "") + f'export WIREGUARD="{val}"\n' with open(env_file, "w", encoding="utf-8") as f: f.write(text) print(f'WIREGUARD={val} saved to {env_file}') - except Exception as e: + except OSError as e: print(f"Warning: could not write {env_file}: {e}") - return (val == "true") + return val == "true" + def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True): """ @@ -316,10 +382,17 @@ def setup_test_wg_ip_tables(self): "Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt" "\nThis may take a while, follow the steps below and don't kill the process..." ) - self.run_script(self.wg_ip_tables_manager_sh, args=["install"]) - self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) - self.run_script(self.wg_ip_tables_test_sh) + self.run_script(self.tunnel_manager_sh, args=["exit_policy_install"]) + def quic_bridge_deploy(self): + """Setup QUIC bridge and configuration using external script""" + print("\n* * * Installing and configuring QUIC bridges * * *") + answer = input("\nDo you want to install, setup and run QUIC bridge? (Y/n) ").strip().lower() + + if answer in ("", "y", "yes"): + self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) + else: + print("Skipping QUIC bridge setup.") def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" @@ -347,8 +420,8 @@ def run_nym_node_as_service(self): if is_active: while True: - ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is already running. Restart it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env) return elif ans == "n": @@ -358,8 +431,8 @@ def run_nym_node_as_service(self): print("Invalid input. Please press 'y' or 'n' and press enter.") else: while True: - ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is not running. Start it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env) return elif ans == "n": @@ -510,8 +583,12 @@ def _env_with_envfile(self) -> dict: def run_node_installation(self,args): """Main function called by argparser command install running full node install flow""" + self.ensure_env_values(args) + # Pass uplink override to all helper scripts if provided + if getattr(args, "uplink_dev", None): + os.environ["UPLINK_DEV"] = args.uplink_dev + os.environ["NETWORK_DEVICE"] = args.uplink_dev self.run_script(self.prereqs_install_sh) - self.run_script(self.env_vars_install_sh) self.run_script(self.node_install_sh) self.run_script(self.service_config_sh) self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh) @@ -521,7 +598,7 @@ def run_node_installation(self,args): self.run_tunnel_manager_setup() if self.check_wg_enabled(): self.setup_test_wg_ip_tables() - self.setup_test_wg_ip_tables() + self.quic_bridge_deploy() @@ -537,7 +614,7 @@ def parser_main(self): version=f"nym-node-cli {__version__}" ) parent.add_argument("-d", "--dev", metavar="BRANCH", - help="Define github branch", + help="Define github branch (default: develop)", type=str, default=argparse.SUPPRESS) parent.add_argument("-v", "--verbose", action="store_true", @@ -553,11 +630,38 @@ def parser_main(self): subparsers = parser.add_subparsers(dest="command", help="subcommands") subparsers.required = True - p_install = subparsers.add_parser( + install_parser = subparsers.add_parser( "install", parents=[parent], help="Starts nym-node installation setup CLI", aliases=["i", "I"], add_help=True ) + install_parser.add_argument( + "--mode", + choices=["mixnode", "entry-gateway", "exit-gateway"], + help="Node mode: 'mixnode', 'entry-gateway', or 'exit-gateway'", + ) + install_parser.add_argument( + "--wireguard-enabled", + choices=["true", "false"], + help="WireGuard functionality switch: true / false" + ) + install_parser.add_argument("--hostname", help="Node domain / hostname") + install_parser.add_argument("--location", help="Node location (country code or name)") + install_parser.add_argument("--email", help="Contact email for the node operator") + install_parser.add_argument("--moniker", help="Public moniker displayed in explorer & NymVPN app") + install_parser.add_argument("--description", help="Short public description of the node") + install_parser.add_argument("--public-ip", help="External IPv4 address (autodetected if omitted)") + install_parser.add_argument("--nym-node-binary", help="URL for nym-node binary (autodetected if omitted)") + install_parser.add_argument("--uplink-dev", help="Override uplink interface used for NAT/FORWARD (e.g., 'eth0'; autodetected if omitted)") + + # generic fallback + install_parser.add_argument( + "--env", + action="append", + metavar="KEY=VALUE", + help="(Optional) Extra ENV VARS, e.g. --env CUSTOM_KEY=value", + ) + args = parser.parse_args() diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh index 3ea25687f59..af36c92017b 100644 --- a/scripts/nym-node-setup/nym-node-install.sh +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -34,8 +34,9 @@ check_existing_config() { if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then echo - read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans - if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then + echo "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one." + read -r -p "back up the old one first? (Y/n) " backup_ans + if [[ -z "${backup_ans}" || "${backup_ans}" =~ ^[Yy]$ ]]; then ts="$(date +%Y%m%d-%H%M%S)" backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts" echo "Backing up to: $backup_dir" @@ -181,24 +182,27 @@ fi NYM_NODE="$HOME/nym-binaries/nym-node" -# if binary already exists, ask to overwrite; if yes, remove first +# if binary already exists, ask to overwrite; if yes, remove first; if no, skip download if [[ -e "${NYM_NODE}" ]]; then echo echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" - read -r -p "Overwrite with the latest release? (y/n): " ow_ans - if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then - echo "Removing existing binary to avoid 'text file busy'..." + read -r -p "Overwrite with the latest release? (Y/n): " ow_ans + + if [[ -z "${ow_ans}" || "${ow_ans}" =~ ^[Yy]$ ]]; then + echo "Removing existing binary..." rm -f "${NYM_NODE}" + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" else - echo "Keeping existing binary." + echo "Keeping existing binary. Skipping download." fi -fi -download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" +else + # binary does not exist → must download + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" +fi echo -e "\n * * * Making binary executable * * *" chmod +x "${NYM_NODE}" - echo "---------------------------------------------------" echo "Nym node binary downloaded:" "${NYM_NODE}" --version || true @@ -225,17 +229,18 @@ WIREGUARD="${WIREGUARD:-}" if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then echo echo "Gateways can also route WireGuard in NymVPN." - echo "Enabling it means your node may be listed as both entry and exit in the app." - # show current default in the prompt if present def_hint="" [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" - read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true - case "${answer:-}" in - [Yy]* ) WIREGUARD="true" ;; - [Nn]* ) WIREGUARD="false" ;; - * ) : ;; # keep existing value if user just pressed enter - esac + + read -r -p "Enable WireGuard support? (Y/n)${def_hint}: " answer || true + + if [[ -z "${answer}" || "${answer}" =~ ^[Yy]$ ]]; then + WIREGUARD="true" + elif [[ "${answer}" =~ ^[Nn]$ ]]; then + WIREGUARD="false" + fi fi + # final default only if still empty WIREGUARD="${WIREGUARD:-false}" diff --git a/scripts/nym-node-setup/nym-node-prereqs-install.sh b/scripts/nym-node-setup/nym-node-prereqs-install.sh index 63f3264ff01..e462ecb5c92 100644 --- a/scripts/nym-node-setup/nym-node-prereqs-install.sh +++ b/scripts/nym-node-setup/nym-node-prereqs-install.sh @@ -1,6 +1,11 @@ #!/bin/bash -# update, upgrade & install dependencies +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + +# update, upgrade and install dependencies echo -e "\n* * * Installing needed prerequisities * * *" apt update -y && apt --fix-broken install @@ -8,11 +13,9 @@ apt upgrade apt install apt ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate neovim tree tmux tig nginx -y apt install ufw --fix-missing - - # enable & setup firewall echo -e "\n* * * Setting up firewall using ufw * * * " -echo "Please enable the firewall in the next prompt for node proper routing!" +echo "Please enable the firewall in the next prompt for node proper routing." echo ufw enable ufw allow 22/tcp # SSH - you're in control of these ports @@ -24,6 +27,6 @@ ufw allow 8080/tcp # Nym specific - nym-node-api ufw allow 9000/tcp # Nym Specific - clients port ufw allow 9001/tcp # Nym specific - wss port ufw allow 51822/udp # WireGuard -ufw allow 'Nginx Full' && \ +ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel ufw reload && \ ufw status diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index f7dc60e0f9f..db14127016b 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -47,7 +47,17 @@ NYM_ETC_BRIDGES="$NYM_ETC_DIR/bridges.toml" NYM_ETC_CLIENT_PARAMS_DEFAULT="$NYM_ETC_DIR/client_bridge_params.json" SERVICE_FILE="/etc/systemd/system/nym-bridge.service" -NET_DEV="$(ip route show default 2>/dev/null | awk '/default/ {print $5}' || true)" +NET_DEV="${UPLINK_DEV:-}" +if [[ -z "$NET_DEV" ]]; then + NET_DEV="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1)" + [[ -z "$NET_DEV" ]] && NET_DEV="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1)" +fi +if [[ -z "$NET_DEV" ]]; then + echo -e "${RED}Cannot determine uplink interface. Set UPLINK_DEV.${RESET}" | tee -a "$LOG_FILE" + exit 1 +fi +echo "Using uplink device: $NET_DEV" + WG_IFACE="nymwg" # Root check @@ -65,6 +75,17 @@ err() { echo -e "${RED}$1${RESET}"; } info() { echo -e "${CYAN}$1${RESET}"; } press_enter() { read -r -p "$1"; } +# Disable pauses and interactive prompts for noninteractive mode +if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then + # all pauses become no-ops + press_enter() { :; } + + # silence any "enter path:" prompts + echo_prompt() { :; } +else + echo_prompt() { echo -n "$1"; } +fi + # Helper: detect dpkg dependency failure for libc6>=2.34 deb_depends_libc_too_old() { local v @@ -176,13 +197,31 @@ verify_bridge_prerequisites() { } adjust_ip_forwarding() { - title "Adjusting IP Forwarding" - sed -i '/^net\.ipv4\.ip_forward=/d' /etc/sysctl.conf - sed -i '/^net\.ipv6\.conf\.all\.forwarding=/d' /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf - echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf - sysctl -p /etc/sysctl.conf - ok "IPv4/IPv6 forwarding enabled." + title "Checking IP forwarding" + 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" ]]; then + ok "IPv4 forwarding is enabled." + else + warn "IPv4 forwarding is not enabled." + fi + + if [[ "$v6" == "1" ]]; then + ok "IPv6 forwarding is enabled." + else + warn "IPv6 forwarding is not enabled." + fi + + if [[ "$v4" != "1" || "$v6" != "1" ]]; then + echo + echo "To enable forwarding and routing consistently, run the network tunnel manager script as root." + echo "For example:" + echo " ./network-tunnel-manager.sh complete_networking_configuration" + echo "or:" + echo " ./network-tunnel-manager.sh adjust_ip_forwarding" + fi } # Install nym-bridge @@ -377,6 +416,11 @@ User=root ExecStart=$BRIDGE_BIN --config $NYM_ETC_BRIDGES Restart=on-failure RestartSec=5 +LimitNOFILE=65535 +ProtectSystem=full +ProtectHome=yes +PrivateTmp=yes + [Install] WantedBy=multi-user.target @@ -390,21 +434,40 @@ EOF # IPTABLES & helpers apply_bridge_iptables_rules() { - title "Applying iptables rules" - iptables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - ip6tables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - iptables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true - ip6tables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true - iptables-save > /etc/iptables/rules.v4 - ip6tables-save > /etc/iptables/rules.v6 - ok "iptables rules applied." + title "Checking iptables rules for bridge routing" + + echo "Inspecting current iptables state for interface ${WG_IFACE} and uplink ${NET_DEV}." + echo + + echo "IPv4 FORWARD:" + iptables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || echo "iptables not available." + echo + echo "IPv4 NAT POSTROUTING:" + iptables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 FORWARD:" + ip6tables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 NAT POSTROUTING:" + ip6tables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true + + echo + echo "This script no longer changes iptables. To configure routing and NAT for nym, use the network tunnel manager script." + echo "For example (run as root):" + echo " ./network-tunnel-manager.sh complete_networking_configuration" } configure_dns_and_icmp() { - title "Allow ICMP and DNS" - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT || true - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT || true - ok "ICMP and DNS rules applied." + title "Checking ICMP and DNS firewall rules" + + echo "IPv4 INPUT rules related to ICMP and DNS:" + iptables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv4 rules shown." + echo + echo "IPv6 INPUT rules related to ICMP and DNS:" + ip6tables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv6 rules shown." + + echo + echo "If ping or DNS are blocked for bridge traffic, adjust your firewall using the network tunnel manager script or your chosen firewall tool." } # Full interactive setup @@ -429,6 +492,8 @@ full_bridge_setup() { echo "" echo "Step 2/6: Installing bridge binary..." install_bridge_binary + echo "[Bridge Install] $(date '+%F %T') $( $BRIDGE_BIN --version 2>/dev/null || echo 'nym-bridge (unknown)')" \ + >> /var/log/nym-bridge-version.log press_enter "Press Enter to continue..." echo "" @@ -447,7 +512,7 @@ full_bridge_setup() { press_enter "Press Enter to continue..." echo "" - echo "Step 6/6: Configuring network rules (optional but recommended)..." + echo "Step 6/6: Checking network rules and forwarding status..." adjust_ip_forwarding apply_bridge_iptables_rules configure_dns_and_icmp diff --git a/scripts/nym-node-setup/setup-env-vars.sh b/scripts/nym-node-setup/setup-env-vars.sh index 3eafd93ba5f..e119a61fd68 100644 --- a/scripts/nym-node-setup/setup-env-vars.sh +++ b/scripts/nym-node-setup/setup-env-vars.sh @@ -39,16 +39,6 @@ while true; do esac done -# try to get the latest binary URL (non-fatal if missing) -LATEST_BINARY=$( - curl -fsSL https://github.com/nymtech/nym/releases/latest \ - | grep -Eo 'href="/nymtech/nym/releases/download/[^"]+/nym-node"' \ - | head -n1 \ - | cut -d'"' -f2 -) -if [[ -z "${LATEST_BINARY:-}" ]]; then - echo "WARNING: Could not determine latest nym-node binary URL right now. The installer will resolve it later." -fi PUBLIC_IP=$(curl -fsS -4 https://ifconfig.me || true) PUBLIC_IP=${PUBLIC_IP:-""} diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index eae4d7165e0..cf1378f5b6c 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -1,315 +1,136 @@ #!/usr/bin/env bash set -euo pipefail -# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh) +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + +# load env if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then set -a; . "${ENV_FILE}"; set +a elif [[ -f "./env.sh" ]]; then set -a; . ./env.sh; set +a fi -: "${HOSTNAME:?HOSTNAME not set in env.sh}" -: "${EMAIL:?EMAIL not set in env.sh}" +: "${HOSTNAME:?HOSTNAME not set}" +: "${EMAIL:?EMAIL not set}" -export SYSTEMD_PAGER="" -export SYSTEMD_COLORS="0" -DEBIAN_FRONTEND=noninteractive +export DEBIAN_FRONTEND=noninteractive -# sanity check -if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" || "${HOSTNAME}" == "ubuntu" ]]; then - echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2 - exit 1 -fi - -echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" - -# set paths & ports vars WEBROOT="/var/www/${HOSTNAME}" -LE_ACME_DIR="/var/www/letsencrypt" SITES_AVAIL="/etc/nginx/sites-available" SITES_EN="/etc/nginx/sites-enabled" -BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost -BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (we’ll write it ourselves) -WSS_AVAIL="${SITES_AVAIL}/wss-config-nym" -BACKUP_DIR="/etc/nginx/sites-backups" - -NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}" -NYM_PORT_WSS="${NYM_PORT_WSS:-9000}" -WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}" - -mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}" - -# helpers -neat_backup() { - local file="$1"; [[ -f "$file" ]] || return 0 - local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0 - local tag; tag="$(basename "$file")" - local latest="${BACKUP_DIR}/${tag}.latest" - if [[ -f "$latest" ]]; then - local sha_prev; sha_prev="$(awk '{print $1}' "$latest")" - [[ "$sha_now" == "$sha_prev" ]] && return 0 - fi - cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)" - echo "$sha_now ${tag}" > "$latest" - ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f -} -ensure_enabled() { - local src="$1"; local name; name="$(basename "$src")" - ln -sf "$src" "${SITES_EN}/${name}" -} +HTTP_CONF="${SITES_AVAIL}/${HOSTNAME}" +WSS_CONF="${SITES_AVAIL}/wss-config-nym" -cert_ok() { - [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]] -} +echo +echo "* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" + +############################################################################### +# step 1: ensure landing page exists (local fetch -> github -> template) +############################################################################### -fetch_landing_html() { - local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html" - mkdir -p "${WEBROOT}" +mkdir -p "${WEBROOT}" - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "${WEBROOT}/index.html" || true - else - wget -qO "${WEBROOT}/index.html" "$url" || true - fi +SCRIPT_DIR="$(dirname "${ENV_FILE:-./env.sh}")" +LOCAL_FETCHED_PAGE="${SCRIPT_DIR}/landing-page.html" - if [[ ! -s "${WEBROOT}/index.html" ]]; then - cat > "${WEBROOT}/index.html" <<'HTML' +if [[ -s "${LOCAL_FETCHED_PAGE}" ]]; then + cp "${LOCAL_FETCHED_PAGE}" "${WEBROOT}/index.html" +elif curl -fsSL \ + https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ + -o "${WEBROOT}/index.html"; then + : +else + cat > "${WEBROOT}/index.html" < - - - - - Nym Exit Gateway - - - -

Nym Exit Gateway

-

This is a Nym Exit Gateway. The operator of this router has no access to any of the data routing through that due to encryption design.

+ +nym node + +

nym exit gateway

+

this is a nym exit gateway.

+

Operator contact: ${EMAIL}

-HTML - fi -} - -inject_email() { - local file="${WEBROOT}/index.html" - [[ -n "${EMAIL:-}" && -s "$file" ]] || return 0 - - # Escape characters that would break sed replacement - local esc_email - esc_email="$(printf '%s' "$EMAIL" | sed -e 's/[&/\]/\\&/g')" - - # try to update existing meta (case-insensitive on the name attr) - if grep -qiE ']+name=["'"'"']contact:email["'"'"']' "$file"; then - sed -i -E \ - "s|(]+name=[\"']contact:email[\"'][^>]*content=\")[^\"]*(\"[^>]*>)|\1${esc_email}\2|I" \ - "$file" || true - return 0 - fi - - # insert before if present (case-insensitive) - if grep -qi '' "$file"; then - awk -v email="$EMAIL" ' - BEGIN{IGNORECASE=1} - /<\/head>/ && !done { - print " " - done=1 - } - { print } - ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" - return 0 - fi - - # fallback: append at end - printf '\n\n' "$EMAIL" >> "$file" || true -} +EOF +fi -fetch_logo() { - local logo_url="https://raw.githubusercontent.com/nymtech/websites/refs/heads/main/www/nym.com/public/images/Nym_meta_Image.png?token=GHSAT0AAAAAACEERII7URYRTFACZ4F2OWZ42GMCPBQ" - mkdir -p "${WEBROOT}/images" - if [[ ! -s "${WEBROOT}/images/nym_logo.png" ]]; then - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$logo_url" -o "${WEBROOT}/images/nym_logo.png" || true - else - wget -qO "${WEBROOT}/images/nym_logo.png" "$logo_url" || true - fi - fi -} +echo "Landing page at ${WEBROOT}/index.html" -reload_nginx() { nginx -t && systemctl reload nginx; } +############################################################################### +# step 2: remove default site and old configs, restart nginx +############################################################################### -# landing page (idempotent) -fetch_landing_html -inject_email -fetch_logo -echo "Landing page at ${WEBROOT}/index.html" +echo "Cleaning existing nginx configuration" -# disable default and stale SSL configs +# remove default nginx site [[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -q "/etc/letsencrypt/live/localhost" "$f"; then - echo "Disabling vhost referencing localhost cert: $f"; unlink "$f" - fi -done -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -qE 'listen\s+.*443' "$f"; then - cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then - echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f" - fi - fi -done - -# HTTP :80 vhost (ACME-safe, proxy to :8080) -neat_backup "${BASE_HTTP}" -cat > "${BASE_HTTP}" < 8080) +############################################################################### + +cat > "${HTTP_CONF}" </dev/null; then - echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2 -fi -THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)" -DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')" -echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}" -if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then - echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4." -fi -timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true - -# install certbot if missing -if ! command -v certbot >/dev/null 2>&1; then - if command -v snap >/dev/null 2>&1; then - snap install core || true; snap refresh core || true - snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot - else - apt-get update -y >/dev/null 2>&1 || true - apt-get install -y certbot >/dev/null 2>&1 || true - fi -fi -# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails -STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING." -if ! cert_ok; then - certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \ - --webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true -fi +ln -sf "${HTTP_CONF}" "${SITES_EN}/${HOSTNAME}" -# create own 443 vhost (only if certs exist) -if cert_ok; then - neat_backup "${BASE_HTTPS}" - cat > "${BASE_HTTPS}" </dev/null 2>&1 || true +apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +echo "Requesting Let's Encrypt certificate for ${HOSTNAME}" - location = /favicon.ico { return 204; access_log off; log_not_found off; } +certbot --nginx --non-interactive --agree-tos --redirect --reuse-key \ + -m "${EMAIL}" -d "${HOSTNAME}" || true - location / { - try_files \$uri \$uri/ @app; - } +############################################################################### +# step 5: create WSS 9001 config using certbot-generated certs +############################################################################### - location @app { - proxy_pass http://127.0.0.1:${NYM_PORT_HTTP}; - proxy_http_version 1.1; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header Host \$host; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - } -} -EOF - ensure_enabled "${BASE_HTTPS}" +if [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" ]]; then + echo "Certificate detected, creating WSS config" - # optional: redirect HTTP->HTTPS (keeps ACME path in HTTP too via separate small server) - neat_backup "${BASE_HTTP}" - cat > "${BASE_HTTP}" < "${WSS_CONF}" < "${WSS_AVAIL}" </dev/null; then - echo -e "${GREEN}✓ Rule exists: $chain $protocol port range $start_port:$end_port${NC}" - return 0 - else - echo -e "${RED}✗ Rule missing: $chain $protocol port range $start_port:$end_port${NC}" - - echo -e "${YELLOW}Dumping all rules in $chain:${NC}" - iptables -L "$chain" -n | grep "$protocol" - - return 1 - fi -} - -# Test port range rules -test_port_range_rules() { - echo -e "${YELLOW}Testing Port Range Rules...${NC}" - - # Select the essential port ranges for testing - local port_ranges=( - "20-21:tcp:FTP" - "80-81:tcp:HTTP" - "2082-2083:tcp:CPanel" - "5222-5223:tcp:XMPP" - "27000-27050:tcp:Steam (sampling)" - "989-990:tcp:FTP over TLS" - "5000-5005:tcp:RTP/VoIP" - "8087-8088:tcp:Simplify Media" - "8232-8233:tcp:Zcash" - "8332-8333:tcp:Bitcoin" - "18080-18081:tcp:Monero" - ) - - local total_failures=0 - - for range in "${port_ranges[@]}"; do - IFS=':' read -r port_range protocol service <<< "$range" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - echo -e "${YELLOW}Testing $service $protocol port range $port_range${NC}" - - if iptables -t filter -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - else - echo -e "${RED}✗ Rule missing: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - ((total_failures++)) - - echo -e "${YELLOW}Existing rules for protocol $protocol:${NC}" - iptables -L "$NYM_CHAIN" -n | grep "$protocol" - fi - done - - if [ $total_failures -eq 0 ]; then - return 0 - else - return 1 - fi -} - -test_critical_services() { - echo -e "${YELLOW}Testing Critical Service Rules...${NC}" - - local tcp_services=( - 22 # SSH - 53 # DNS - 443 # HTTPS - 853 # DNS over TLS - 1194 # OpenVPN - ) - - local udp_services=( - 53 # DNS - 123 # NTP - 1194 # OpenVPN - ) - - local failures=0 - - # Test TCP services - for port in "${tcp_services[@]}"; do - local rule_found=false - - # First check for exact match - if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - # This checks if the port is covered by any range rule - if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT tcp port $port${NC}" - ((failures++)) - fi - fi - done - - # Test UDP services - similar approach - for port in "${udp_services[@]}"; do - local rule_found=false - - if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT udp port $port${NC}" - ((failures++)) - fi - fi - done - - echo -e "${YELLOW}Relevant existing rules for HTTP (port 80):${NC}" - iptables-save | grep -E "$NYM_CHAIN.*tcp" | grep -E "(dpt|dpts):.*80" - - return $failures -} - -# Verify default reject rule exists -test_default_reject_rule() { - echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" - echo - echo -e "${YELLOW}Testing Default Reject Rule...${NC}" - - # Try different patterns to detect the reject rule - if iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*anywhere.*anywhere.*reject-with"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all -- .*everywhere.*everywhere"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -n -L "$NYM_CHAIN" | grep -qE "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | tail -1 | grep -q "REJECT"; then - echo -e "${GREEN}✓ Default REJECT rule exists at the end of chain${NC}" - return 0 - else - echo -e "${RED}✗ Default REJECT rule missing${NC}" - # Display the last 3 rules in the chain for debugging - echo -e "${YELLOW}Last 3 rules in the chain:${NC}" - iptables -L "$NYM_CHAIN" | tail -3 - return 1 - fi -} - -run_all_tests() { - local total_failures=0 - local total_tests=0 - local skip_default_reject=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --skip-default-reject) - skip_default_reject=true - shift - ;; - *) - echo -e "${RED}Unknown argument: $1${NC}" - exit 1 - ;; - esac - done - - local test_functions=( - "test_port_range_rules" - "test_critical_services" - ) - - if [ "$skip_default_reject" = false ]; then - test_functions+=("test_default_reject_rule") - fi - - echo -e "${YELLOW}Running Nym Exit Policy Verification Tests...${NC}" - - for test_func in "${test_functions[@]}"; do - ((total_tests++)) - $test_func - if [ $? -ne 0 ]; then - ((total_failures++)) - echo -e "${RED}Test $test_func FAILED${NC}" - else - echo -e "${GREEN}Test $test_func PASSED${NC}" - fi - done - - echo -e "\n${YELLOW}Test Summary:${NC}" - echo -e "Total Tests: $total_tests" - echo -e "Failures: $total_failures" - - if [ $total_failures -eq 0 ]; then - echo -e "${GREEN}All Tests Passed Successfully!${NC}" - exit 0 - else - echo -e "${RED}Some Tests Failed. Please review the iptables configuration.${NC}" - exit 1 - fi -} - -if [[ $EUID -ne 0 ]]; then - echo -e "${RED}This script must be run as root${NC}" - exit 1 -fi - -# Run the tests -run_all_tests "$@" diff --git a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh b/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh deleted file mode 100644 index e93d28a24fe..00000000000 --- a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -validate_exit_policy() { - echo "=== Nym Exit Policy Blocking Validation ===" - - # Check iptables rules - echo "Checking iptables NYM-EXIT chain:" - sudo iptables -L NYM-EXIT -v -n - - # Test IP ranges and individual IPs - test_ips=( - "5.188.10.0/24" # Blocked network range - "31.132.36.50" # Specific blocked IP - "37.9.42.100" # Another blocked IP - ) - - for target in "${test_ips[@]}"; do - echo -e "\n\e[33mTesting blocking for $target\e[0m" - - # Multiple connection test methods - methods=( - "ping -c 4 -W 2" - "curl -m 5 http://$target" - "nc -z -w 5 $target 80" - "telnet $target 80" - ) - - for method in "${methods[@]}"; do - echo -n "Testing with $method: " - if sudo timeout 5 $method >/dev/null 2>&1; then - echo -e "\e[31mFAILED: Connection succeeded (Blocking ineffective)\e[0m" - else - echo -e "\e[32mBLOCKED\e[0m" - fi - done - done -} - -# Run the test -validate_exit_policy \ No newline at end of file diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh deleted file mode 100644 index bce3d09711c..00000000000 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ /dev/null @@ -1,679 +0,0 @@ -#!/bin/bash -# -# Nym Wireguard Exit Policy Manager -# Version: 1.0.0 -# -# This script manages iptables rules for Nym Wireguard exit policies -# Features: -# - Implements the Nym exit policy from official documentation -# - Makes rules persistent across reboots -# - Provides commands to inspect and manage rules -# - Groups rules logically for easier management -# - Integrates with existing Nym node configuration -# -# Usage: ./nym-exit-policy.sh [command] - -set -e - -NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') -WG_INTERFACE="nymwg" -NYM_CHAIN="NYM-EXIT" -POLICY_FILE="/etc/nym/exit-policy.txt" -EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -add_port_rules() { - local chain="$1" - local port="$2" - local protocol="${3:-tcp}" - - # Check if the port contains a range - if [[ "$port" == *"-"* ]]; then - # Port range handling - add as a single rule with a range - local start_port=$(echo "$port" | cut -d'-' -f1) - local end_port=$(echo "$port" | cut -d'-' -f2) - - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - fi - else - # Single port handling - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port $port${NC}" - fi - fi -} - -install_dependencies() { - if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - echo -e "${YELLOW}Installing iptables-persistent...${NC}" - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent - echo -e "${GREEN}iptables-persistent installed.${NC}" - else - echo -e "${GREEN}iptables-persistent is already installed.${NC}" - fi - - # Check for other required dependencies - for cmd in iptables ip6tables ip grep sed awk wget curl; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${YELLOW}Installing $cmd...${NC}" - apt-get install -y "$cmd" - fi - done -} - -configure_ip_forwarding() { - echo -e "${YELLOW}Configuring IP forwarding...${NC}" - - # Remove any existing forwarding settings to avoid duplicates - sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - # Add forwarding settings - echo "net.ipv6.conf.all.forwarding=1" | tee -a /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p /etc/sysctl.conf - - # Verify settings - ipv4_forwarding=$(cat /proc/sys/net/ipv4/ip_forward) - ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - - if [ "$ipv4_forwarding" == "1" ] && [ "$ipv6_forwarding" == "1" ]; then - echo -e "${GREEN}IP forwarding configured successfully.${NC}" - else - echo -e "${RED}Failed to configure IP forwarding.${NC}" - exit 1 - fi -} - -create_nym_chain() { - echo -e "${YELLOW}Creating Nym exit policy chain...${NC}" - - # Check if the chain already exists - if iptables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists. Flushing it...${NC}" - iptables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN...${NC}" - iptables -N "$NYM_CHAIN" - fi - - # Do the same for IPv6 - if ip6tables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists in ip6tables. Flushing it...${NC}" - ip6tables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN in ip6tables...${NC}" - ip6tables -N "$NYM_CHAIN" - fi - - # Link it to the FORWARD chain if not already linked - if ! iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" - iptables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" - fi - - # Link IPv6 chain - if ! ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" - ip6tables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" - fi -} - -setup_nat_rules() { - echo -e "${YELLOW}Setting up NAT rules...${NC}" - - # Check if NAT rule for IPv4 exists - if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then - iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE - echo -e "${GREEN}Added IPv4 NAT rule.${NC}" - else - echo -e "${GREEN}IPv4 NAT rule already exists.${NC}" - fi - - # Check if NAT rule for IPv6 exists - if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then - ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE - echo -e "${GREEN}Added IPv6 NAT rule.${NC}" - else - echo -e "${GREEN}IPv6 NAT rule already exists.${NC}" - fi - - # Setup forwarding rules for Wireguard interface - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (WG → Internet).${NC}" - fi - - if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (Internet → WG for established connections).${NC}" - fi - - # IPv6 forwarding rules - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (WG → Internet).${NC}" - fi - - if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (Internet → WG for established connections).${NC}" - fi -} - -configure_dns_and_icmp() { - echo -e "${YELLOW}Configuring DNS and ICMP rules...${NC}" - - # ICMP rules for ping - if ! iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping requests).${NC}" - fi - - if ! iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then - iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping replies).${NC}" - fi - - # ICMPv6 rules for ping6 - if ! ip6tables -C INPUT -p ipv6-icmp -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT - echo -e "${GREEN}Added IPv6 ICMP rule (allow ping6).${NC}" - fi - - # DNS rules - if ! iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (UDP).${NC}" - fi - - if ! iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (TCP).${NC}" - fi - - # IPv6 DNS rules - if ! ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (UDP).${NC}" - fi - - if ! ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (TCP).${NC}" - fi -} - -# Apply Spamhaus blocklist from the Nym exit policy -apply_spamhaus_blocklist() { - echo -e "${YELLOW}Applying Spamhaus blocklist...${NC}" - - # Create directory if not exists - mkdir -p "$(dirname "$POLICY_FILE")" - - # Try to download the policy file - echo -e "${YELLOW}Downloading exit policy from $EXIT_POLICY_LOCATION${NC}" - if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - echo -e "${RED}Failed to download exit policy. Using minimal blocklist.${NC}" - - # Create a minimal policy file with a few entries - cat >"$POLICY_FILE" </dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - - # Apply IPv6 rules for IPv6 addresses - if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - fi - done - - echo -e "${GREEN}Blocklist applied successfully.${NC}" -} - -add_default_reject_rule() { - echo -e "${YELLOW}Adding default reject rule...${NC}" - - # First remove any existing plain reject rules (without specific destinations) - 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 - - # Add the default catch-all reject rule (must be the last rule in the chain) - iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable - ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable - - echo -e "${GREEN}Default reject rule added successfully.${NC}" -} - -apply_port_allowlist() { - echo -e "${YELLOW}Applying allowed ports...${NC}" - - # Dictionary of services and their ports - declare -A PORT_MAPPINGS=( - ["FTP"]="20-21" - ["SSH"]="22" - ["WHOIS"]="43" - ["DNS"]="53" - ["Finger"]="79" - ["HTTP"]="80-81" - ["Kerberos"]="88" - ["POP3"]="110" - ["NTP"]="123" - ["IMAP"]="143" - ["IMAP3"]="220" - ["LDAP"]="389" - ["HTTPS"]="443" - ["SMBWindowsFileShare"]="445" - ["Kpasswd"]="464" - ["RTSP"]="554" - ["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" - ["OpenVPN"]="1194" - ["QTServerAdmin"]="1220" - ["PKTKRB"]="1293" - ["MSSQL"]="1433" - ["VLSILicenseManager"]="1500" - ["OracleDB"]="1521" - ["Sametime"]="1533" - ["GroupWise"]="1677" - ["PPTP"]="1723" - ["RTSPAlt"]="1755" - ["MSNP"]="1863" - ["NFS"]="2049" - ["CPanel"]="2082-2083" - ["GNUnet"]="2086-2087" - ["NBX"]="2095-2096" - ["Zephyr"]="2102-2104" - ["XboxLive"]="3074" - ["MySQL"]="3306" - ["SVN"]="3690" - ["RWHOIS"]="4321" - ["Virtuozzo"]="4643" - ["RTPVOIP"]="5000-5005" - ["MMCC"]="5050" - ["ICQ"]="5190" - ["XMPP"]="5222-5223" - ["AndroidMarket"]="5228" - ["PostgreSQL"]="5432" - ["MongoDBDefault"]="27017" - ["Electrum"]="8082" - ["SimplifyMedia"]="8087-8088" - ["Zcash"]="8232-8233" - ["Bitcoin"]="8332-8333" - ["HTTPSALT"]="8443" - ["TeamSpeak"]="8767" - ["MQTTS"]="8883" - ["HTTPProxy"]="8888" - ["TorORPort"]="9001" - ["TorDirPort"]="9030" - ["Tari"]="9053" - ["Gaming"]="9339" - ["Git"]="9418" - ["HTTPSALT2"]="9443" - ["Lightning"]="9735" - ["NDMP"]="10000" - ["OpenPGP"]="11371" - ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" - ["GoogleVoice"]="19294" - ["EnsimControlPanel"]="19638" - ["DarkFiTor"]="25551" - ["Minecraft"]="25565" - ["DarkFi"]="26661" - ["Steam"]="27000-27050" - ["ElectrumSSL"]="50002" - ["MOSH"]="60000-61000" - ["Mumble"]="64738" - ) - - # Add TCP and UDP rules for each allowed port - for service in "${!PORT_MAPPINGS[@]}"; do - port="${PORT_MAPPINGS[$service]}" - echo -e "${YELLOW}Adding rules for $service (Port: $port)${NC}" - - # Add both TCP and UDP rules for all services - 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 - - add_default_reject_rule - - echo -e "${GREEN}Port allowlist applied successfully.${NC}" -} - -safe_iptables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if iptables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - iptables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -safe_ip6tables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if ip6tables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - ip6tables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -clear_rules() { - echo -e "${YELLOW}Clearing Nym exit policy rules...${NC}" - - # Flush all rules in the NYM-EXIT chain - iptables -F "$NYM_CHAIN" 2>/dev/null || true - ip6tables -F "$NYM_CHAIN" 2>/dev/null || true - - # Remove the chain from FORWARD if it exists - iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - - # Delete the chains - iptables -X "$NYM_CHAIN" 2>/dev/null || true - ip6tables -X "$NYM_CHAIN" 2>/dev/null || true - - echo -e "${GREEN}Nym exit policy rules cleared successfully.${NC}" -} - -remove_duplicate_rules() { - local interface="$1" - - if [[ -z "$interface" ]]; then - echo -e "${RED}Error: No interface specified. Usage: $0 remove-duplicates ${NC}" >&2 - exit 1 - fi - - echo -e "${YELLOW}Detecting and removing duplicate rules for $interface...${NC}" - - # Verbose duplicate rule detection for IPv4 - echo -e "${YELLOW}Checking IPv4 duplicate rules:${NC}" - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv4 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/iptables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv4 rules found.${NC}" - - # Verbose duplicate rule detection for IPv6 - echo -e "${YELLOW}Checking IPv6 duplicate rules:${NC}" - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv6 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/ip6tables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv6 rules found.${NC}" - - # Additional verification - echo -e "\n${YELLOW}Rule verification:${NC}" - echo "IPv4 Rules:" - iptables -L FORWARD -v -n | grep "$interface" - echo "IPv6 Rules:" - ip6tables -L FORWARD -v -n | grep "$interface" - - echo -e "${GREEN}Duplicate rule removal process completed.${NC}" -} - -save_rules() { - echo -e "${YELLOW}Saving iptables rules to make them persistent...${NC}" - - if [ -d "/etc/iptables" ]; then - # For Debian/Ubuntu with iptables-persistent - iptables-save >/etc/iptables/rules.v4 - ip6tables-save >/etc/iptables/rules.v6 - echo -e "${GREEN}Rules saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6${NC}" - else - # Fallback method - iptables-save >/etc/iptables.rules - ip6tables-save >/etc/ip6tables.rules - echo -e "${GREEN}Rules saved to /etc/iptables.rules and /etc/ip6tables.rules${NC}" - - # Add loading script to rc.local if it doesn't exist - if [ ! -f "/etc/network/if-pre-up.d/iptables" ]; then - cat >/etc/network/if-pre-up.d/iptables </dev/null; then - echo -e "${RED}WARNING: Wireguard interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Interface details - echo -e "\n${YELLOW}Interface Details:${NC}" - ip link show "$WG_INTERFACE" - - # IP Addresses - echo -e "\n${YELLOW}IP Addresses:${NC}" - ip -4 addr show dev "$WG_INTERFACE" - ip -6 addr show dev "$WG_INTERFACE" - - # Iptables Chain Status - echo -e "\n${YELLOW}Iptables Chains:${NC}" - { - echo "IPv4 Chain:" - iptables -L "$NYM_CHAIN" -n -v - echo -e "\nIPv6 Chain:" - ip6tables -L "$NYM_CHAIN" -n -v - } || echo "One or both chains not found" - - # Forwarding Status - echo -e "\n${YELLOW}IP Forwarding:${NC}" - echo "IPv4: $(cat /proc/sys/net/ipv4/ip_forward)" - echo "IPv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding)" -} - -test_connectivity() { - echo -e "${YELLOW}Testing connectivity through $WG_INTERFACE...${NC}" - - # More comprehensive interface check - interface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null) - - if [ -z "$interface_info" ]; then - echo -e "${RED}Interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Check for multiple possible interface states - if ! echo "$interface_info" | grep -qE "state (UP|UNKNOWN|DORMANT)"; then - echo -e "${RED}Interface $WG_INTERFACE is not in an active state!${NC}" - echo "$interface_info" - return 1 - fi - - # Detailed interface information - echo -e "${GREEN}Interface Details:${NC}" - echo "$interface_info" - - # Get IP addresses with more robust method - ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | cut -d'/' -f1 | head -n1) - ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | grep -oP '(?<=inet6\s)[0-9a-f:]+/\d+' | cut -d'/' -f1 | head -n1) - - echo -e "${GREEN}IPv4 Address:${NC} ${ipv4_address:-Not found}" - echo -e "${GREEN}IPv6 Address:${NC} ${ipv6_address:-Not found}" - - # Connectivity tests - if [[ -n "$ipv4_address" ]]; then - echo -e "${YELLOW}Testing IPv4 connectivity from $ipv4_address...${NC}" - - # Ping test - if timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 connectivity to 8.8.8.8: Success${NC}" - else - echo -e "${RED}IPv4 connectivity to 8.8.8.8: Failed${NC}" - fi - - # DNS resolution test - if timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv4 DNS resolution: Failed${NC}" - fi - - # HTTP(S) connectivity test - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv4_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv4 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv4 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${RED}No IPv4 address configured on $WG_INTERFACE${NC}" - fi - - # Similar tests for IPv6 if available - if [[ -n "$ipv6_address" ]]; then - echo -e "${YELLOW}Testing IPv6 connectivity from $ipv6_address...${NC}" - - if timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 connectivity to Google DNS: Success${NC}" - else - echo -e "${RED}IPv6 connectivity to Google DNS: Failed${NC}" - fi - - if timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv6 DNS resolution: Failed${NC}" - fi - - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv6_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv6 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv6 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${YELLOW}No IPv6 address configured on $WG_INTERFACE${NC}" - fi - - echo -e "${GREEN}Connectivity tests completed.${NC}" -} - -main() { - # Check for root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}This script must be run as root${NC}" >&2 - exit 1 - fi - - # Parse command-line arguments - case "$1" in - install) - install_dependencies - configure_ip_forwarding - create_nym_chain - setup_nat_rules - configure_dns_and_icmp - apply_spamhaus_blocklist - apply_port_allowlist - save_rules - echo -e "${GREEN}Nym exit policy installed successfully.${NC}" - ;; - status) - show_status - ;; - test) - test_connectivity - ;; - clear) - clear_rules - echo -e "${GREEN}Nym exit policy rules cleared.${NC}" - ;; - remove-duplicates) - remove_duplicate_rules "$2" - ;; - help | --help | -h) - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " install Install and configure Nym exit policy" - echo " status Show current Nym exit policy status" - echo " test Test connectivity through Wireguard interface" - echo " clear Remove all Nym exit policy rules" - echo " remove-duplicates Remove duplicate iptables rules for an interface" - echo " help Show this help message" - ;; - *) - echo -e "${RED}Invalid command. Use '$0 help' for usage information.${NC}" >&2 - exit 1 - ;; - esac -} - -main "$@"