#!/usr/bin/env bash # # Copyright (C)2026 kimapr # # This file is part of fcnatfw. # # fcnatfw is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # fcnatfw is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # FCNATFW_INSTALLED="" ## PRELUDE_PLACEHOLDER ## PORT="$1" PROTOCOL="$2" STUNSERVER="$3" if test -z "$STUNSERVER"; then STUNSERVER="stun.kimapr.net" fi if test -z "$PROTOCOL"; then PROTOCOL=tcp fi dep() { for i; do if ! command -v "$i" >/dev/null; then printf 'fatal: required program '\''%s'\'' not installed\n' "$i" exit 1 fi done } if test -z "$FCNATFW_INSTALLED"; then wrapper="$(dirname "$(realpath "$(command -v "$0")")")/fcnatwrap" dep ncat ping stunclient "$wrapper" else dep ping fi if { ! { [ "$PROTOCOL" = tcp ] || [ "$PROTOCOL" = udp ]; }; } || { ! { printf '%s\n' "$PORT" | grep -q '^[0-9][0-9:]*$'; }; } || test -z "$STUNSERVER"; then printf '%s\n' "Usage: fcnatfw PORT[:EXTERNAL_PORT...] PROTOCOL STUNSERVER_IP[:STUNSERVER_PORT]" exit 1; fi port_of() { printf '%s\n' "$1" | sed -E 's/.*:([0-9]*)/\1/'; } ip_of() { printf '%s\n' "$1" | sed -E 's/(.*):[0-9]*/\1/'; } randstream() { local char; while true; do char=$((RANDOM%36)); if [ $char -ge 10 ]; then char=$((65+char-10)); else char=$((48+char)); fi; printf "\\$(printf '%.3o' "$char")"; done; } upnp_id="fcnatfw_$(randstream | head -c10)" if printf '%s\n' "$STUNSERVER" | grep -q ':[0-9]*$'; then STUNPORT="$(port_of "$STUNSERVER")" STUNIP="$(ip_of "$STUNSERVER")" else STUNIP="$(printf '%s\n' "$STUNSERVER")" STUNPORT="3478" fi upnp_poke() { local pid local loc ( bash -c 'echo $PPID' ( (cat <&2 break; fi UPNPS[count]="$url" count=$((count + 1)) printf 'Found UPnP server [%s]: %s\n' "$count" "$url" >&2 done } ext_ports() { printf '%s:' "$PORT" | { lp=0 while read -d ':' port; do if test -z "$port"; then break; fi lp="$port" echo "$port" done yes "$lp" } } printf 'Internal port: %s (%s)\nSTUN server IP: %s\nSTUN server port: %s\n' "$(ext_ports | head -n1)" "$PROTOCOL" "$STUNIP" "$STUNPORT" >&2 upnp_unforward_single() { upnpc -u "$1" -d "$2" "$PROTOCOL" >/dev/null printf 'DELETE UPnP mapping %s -> %s\n' "$3" "$4" >&2 } upnp_unforward() { while [ "${#UMAPS[@]}" -gt 0 ]; do upnp_unforward_single ${UMAPS[-1]} unset UMAPS[-1] done } upnp_forward() { if test -z "${UPNPS[0]}"; then return fi local addr local oldfaddr local port local faddr { read port addr="$(upnpc -u "${UPNPS[0]}" -s | grep 'Local LAN' -m1 | sed 's/.* *: *//')" UMAPS=() for i in "${UPNPS[@]}"; do read portex faddr="$(upnpc -u "$i" -e "$upnp_id" -a "$addr" "$port" "$portex" "$PROTOCOL" | grep -m1 'external .* is redirected' | sed -E 's/^external ([^ ]*).*$/\1/')" if test -z "$faddr"; then return 1; fi if test -z "$1"; then printf 'CREATE UPnP mapping %s -> %s:%s\n' "$faddr" "$addr" "$port" >&2 fi oldfaddr="$addr:$port" addr="$(ip_of "$faddr")" port="$(port_of "$faddr")" UMAPS+=("$i $port $faddr $oldfaddr") done; } < <(ext_ports) } if ! command -v upnpc > /dev/null; then find_upnp() { true } upnp_unforward() { true } upnp_forward() { printf 'Warning: upnpc is missing\n' >&2 } fi time_ms() { echo $(("$(date +%s%N | head -c-7)")) } ms_to_s() { printf '%s\n' "$1" | sed -E 's/(...)$/.\1/' } stunloop() { local faddr="" local nfaddr local dt local stunout local begin_t="$(time_ms)" local last_upnp="$(time_ms)" while true; do printf 'binding STUN... ' >&2 stunout="$("$wrapper" stunclient "$STUNIP" "$STUNPORT" --localport "$(ext_ports | head -n1)" --protocol "$PROTOCOL")" nfaddr="$(printf '%s\n' "$stunout" | grep 'Mapped address' | sed 's/^[^:]*: *//')" if test -z "$nfaddr"; then printf "Error! %s\n" "$stunout" >&2 else printf "Ok (%s)\n" "$nfaddr" >&2 fi if ! test "$faddr" = "$nfaddr"; then printf '%s\n' "$nfaddr"; faddr="$nfaddr"; fi dt="$((5000 - ($(time_ms) - begin_t) ))" if [ $dt -ge 0 ]; then sleep $(ms_to_s $dt) fi begin_t="$(time_ms)" if [ $(($(time_ms) - last_upnp)) -ge 30000 ]; then find_upnp upnp_forward last_upnp="$(time_ms)" fi done } trap upnp_unforward EXIT find_upnp upnp_forward stunloop