259 lines
5.4 KiB
Bash
259 lines
5.4 KiB
Bash
#!/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 <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
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 <<EOF
|
|
M-SEARCH * HTTP/1.1
|
|
HOST: 239.255.255.250:1900
|
|
MAN: "ssdp:discover"
|
|
MX: 2
|
|
ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1
|
|
|
|
EOF
|
|
) | sed 's/$/\r/'; sleep 2) | ncat -u "$1" 1900 | grep '^LOCATION' -m1 | sed 's/^LOCATION: *//'
|
|
exec true ) | {
|
|
read pid
|
|
read loc || return 1;
|
|
printf '%s\n' "$loc";
|
|
kill -3 $pid;
|
|
}; }
|
|
|
|
upnpc_direct() {
|
|
local addr
|
|
|
|
addr="$1"
|
|
shift
|
|
upnpc -u "$(upnp_poke "$addr")" "$@"
|
|
}
|
|
|
|
declare -a UPNPS
|
|
declare -a UMAPS
|
|
|
|
NO_UPNP_IP=''
|
|
|
|
find_upnp() {
|
|
|
|
local count
|
|
local addr
|
|
local url
|
|
|
|
count=0
|
|
UPNPS=()
|
|
|
|
while true; do
|
|
addr="$(ping -n -c 1 -t $((count + 1)) -w 2 "$STUNIP" | grep -o '^From [^ ]*[^ :]' | sed 's/^From //')";
|
|
|
|
if test -z "$addr" || test "$addr" = "$NO_UPNP_IP"; then
|
|
break;
|
|
fi
|
|
|
|
url="$(upnp_poke "$addr")"
|
|
|
|
if test -z "$url"; then
|
|
NO_UPNP_IP="$addr"
|
|
printf 'Not an UPnP server: %s\n' "$addr" >&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
|