op-packages/openwrt-natmapt/files/natmap.init
github-actions[bot] 32600acc60 🌴 Sync 2026-03-05 23:51:42
2026-03-05 23:51:42 +08:00

487 lines
16 KiB
Bash

#!/bin/bash /etc/rc.common
. "${IPKG_INSTROOT}/lib/functions/network.sh"
START=97
STOP=90
USE_PROCD=1
# natmap
[ -x "$(command -v nft)" ] && FW='fw4' || FW='fw3'
NAME=natmap
GLOBALSECTION=global
NATMAPSECTION=natmap
PROG=/usr/bin/$NAME
STATUS_PATH=/var/run/natmap
config_load "$NAME"
# define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_*
define_nic() {
local dev sub addr
# get all active NICs
for dev in $(ls /sys/class/net/); do
#ipv4
sub=$(ip -o -4 addr|sed -En "s|.*${dev}\s+inet\s+([0-9\./]+).*|\1|gp")
eval "NIC_${dev//[[:punct:]]/_}=\"\$sub\""
#ipv6
sub=$(ip -o -6 addr|sed -En "s|.*${dev}\s+inet6\s+([A-Za-z0-9\./:]+).*|\1|gp")
# ref: https://github.com/openwrt/openwrt/blob/main/package/base-files/files/lib/functions/network.sh#L53 #network_get_subnet6()
for _ in $sub; do
for addr in $sub; do
case "$addr" in fe[8ab]?:*|f[cd]??:*)
continue
esac
sub=$addr; break
done
# Attempt to return first non-fe80::/10 range
for addr in $sub; do
case "$addr" in fe[8ab]?:*)
continue
esac
sub=$addr; break
done
# Return first item
for addr in $sub; do
sub=$addr; break
done
done
eval "NIC6_${dev//[[:punct:]]/_}=\"\$sub\""
done
# get default gateway 0.0.0.0/::
network_find_wan DEF_WAN true
network_find_wan6 DEF_WAN6 true
return 0
}
define_nic
load_interfaces() {
local bind_ifname enable
config_get bind_ifname "$1" bind_ifname
config_get_bool enable "$1" enable 0
[ "$enable" = 1 ] && interfaces=" $(uci -q show network|grep "device='$bind_ifname'"|cut -f2 -d'.') $interfaces"
}
# define global var: GLOBAL_*
define_global() {
[ "$2" == "0" ] || { >&2 echo "$(basename $0): section $1 validation failed"; return 1; }
local error=0
local v ucivv="enable def_tcp_stun def_udp_stun def_http_server def_fwmark_value def_tcp_interval def_udp_interval def_udp_stun_cycle"
for v in $ucivv; do
[ -z "$(config_get $1 $v)" ] && grep -qEv "^(def_fwmark_value)$" <<< "$v" && {
err_msg__empty $1 $v
let error++
}
eval "GLOBAL_$v=\$$v"
done
[ "$error" -gt 0 ] && return 1 || return 0
}
validate_section_global() {
uci_load_validate "$NAME" "$GLOBALSECTION" "$1" "$2" \
'enable:bool:0' \
'def_tcp_stun:string' \
'def_udp_stun:string' \
'def_http_server:string' \
'def_fwmark_value:string' \
'def_tcp_interval:and(uinteger, min(1)):30' \
'def_udp_interval:and(uinteger, min(1)):15' \
'def_udp_stun_cycle:uinteger:5' \
'test_port:and(port, min(1))'
}
validate_section_natmap() {
uci_load_validate "$NAME" "$NATMAPSECTION" "$1" "$2" \
'enable:bool:0' \
'interval:and(uinteger, min(1))' \
'stun_cycle:uinteger' \
'stun_server:string' \
'http_server:string' \
'fwmark_value:string' \
'comment:string' \
'udp_mode:bool:0' \
'family:or("ipv4", "ipv6"):ipv4' \
'bind_ifname:network' \
'port:or(port, portrange)' \
'port_pointer:bool:0' \
'forward:bool:0' \
'forward_mode:or("dnat", "via"):via' \
'natloopback:bool:1' \
'forward_congestion:string' \
'forward_timeout:and(uinteger, min(1))' \
'forward_target:or(hostname,ipaddr(1))' \
'forward_port:port' \
'refresh:bool:0' \
'clt_script:file' \
'clt_scheme:or("http", "https"):http' \
'clt_web_port:and(port, min(1))' \
'clt_username:string' \
'clt_password:string' \
'notify_enable:bool:0' \
'notify_script:file' \
'notify_tokens:list(string)' \
'notify_custom_domain:hostname' \
'notify_text:string' \
'ddns_enable:bool:0' \
'ddns_script:file' \
'ddns_tokens:list(string)' \
'ddns_a:hostname' \
'ddns_aaaa:hostname' \
'ddns_srv:hostname' \
'ddns_srv_serv:string' \
'ddns_srv_proto:string:tcp' \
'ddns_srv_target:hostname' \
'ddns_srv_priority:range(0, 65535):0' \
'ddns_srv_weight:range(0, 65535):65535' \
'ddns_https:hostname' \
'ddns_https_target:or(".", hostname):.' \
'ddns_https_svcparams:string:alpn="h2,http/1.1"' \
'ddns_https_priority:range(1, 65535):1' \
'custom_script:file' \
'log_stdout:bool:1' \
'log_stderr:bool:1'
}
# ip_match <hostname>
hostname_match() {
[ "$#" -ge 1 ] || return 1
local _hostname="$1"
echo "$_hostname" | grep -E "^[[:alnum:]][-[:alnum:]]{0,62}(\.[[:alnum:]][-[:alnum:]]{0,62})*\.?$"
}
# ip_match <family> <ipaddr>
ip_match() {
[ "$#" -ge 2 ] || return 1
local _family="$1"
local _ip="$2"
case "$_family" in
ipv4)
echo "$_ip" | grep -E "^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$"
;;
ipv6)
echo "$_ip" | grep -E "^(([[:xdigit:]]{1,4}:){7}[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}:){1,7}:|([[:xdigit:]]{1,4}:){1,6}:[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}:){1,5}(:[[:xdigit:]]{1,4}){1,2}|([[:xdigit:]]{1,4}:){1,4}(:[[:xdigit:]]{1,4}){1,3}|([[:xdigit:]]{1,4}:){1,3}(:[[:xdigit:]]{1,4}){1,4}|([[:xdigit:]]{1,4}:){1,2}(:[[:xdigit:]]{1,4}){1,5}|[[:xdigit:]]{1,4}:(:[[:xdigit:]]{1,4}){1,6}|:((:[[:xdigit:]]{1,4}){1,7}|:)|\
fe80:(:[[:xdigit:]]{0,4}){0,4}%\w+|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([[:xdigit:]]{1,4}:){1,4}:((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?))$"
;;
esac
}
# fwmark_match <fwmark>
fwmark_match() {
echo "$1" | grep -E '^(0|0x[[:xdigit:]]+|[1-9][0-9]*|0[0-7]+)$'
}
natmap_instance() {
procd_open_instance "$1"
procd_set_param command "$PROG" \
${interval:+-k "$interval"} \
${stun_cycle:+-c "$stun_cycle"} \
${stun_server:+-s "$stun_server"} \
${http_server:+-h "$http_server"} \
${fwmark_value:+-f "$fwmark_value"} \
${port:+-b "$([ "$port_pointer" = 1 ] && echo ${port/-/\~} || echo $port)"} \
[ "${family}" = ipv4 ] && procd_append_param command -4
[ "${family}" = ipv6 ] && procd_append_param command -6
[ "${udp_mode}" = 1 ] && procd_append_param command -u
procd_set_param netdev "br-lan"
[ -n "${bind_ifname}" ] && {
procd_append_param command -i "$bind_ifname"
procd_append_param netdev "$bind_ifname"
} || {
local ifname
network_get_device ifname "$DEF_WAN" && procd_append_param netdev "$ifname"
network_get_device ifname "$DEF_WAN6" && procd_append_param netdev "$ifname"
}
if [ "$forward" = 1 -a "$forward_mode" = "via" -a -n "${forward_target}" ]; then
procd_append_param command ${forward_congestion:+-C "$forward_congestion"} ${forward_timeout:+-T "$forward_timeout"} -t "$forward_target" -p "$forward_port"
fi
procd_set_param env "SECTIONID=$1"
procd_append_param env "COMMENT=$comment"
if [ "$forward" = 1 ]; then
# Rewrite the dest_port(forward port) using a public port
[ "$forward_port" = 0 -a "$forward_mode" = "dnat" ] && procd_append_param env "RWFW=1"
if [ "$refresh" = 1 ]; then
procd_append_param env "REFRESH=$clt_script"
json_init
json_add_string scheme "$clt_scheme"
json_add_int web_port "$clt_web_port"
json_add_string username "$clt_username"
json_add_string password "$clt_password"
json_add_string host "$forward_target"
procd_append_param env "REFRESH_PARAM=$(json_dump)"
fi
fi
if [ "$notify_enable" = 1 ]; then
procd_append_param env "NOTIFY=$notify_script"
json_init
json_add_string custom_domain "$notify_custom_domain"
json_add_string text "$notify_text"
json_add_string tokens "$notify_tokens"
procd_append_param env "NOTIFY_PARAM=$(json_dump)"
fi
if [ "$ddns_enable" = 1 ]; then
procd_append_param env "DDNS=$ddns_script"
json_init
json_add_string tokens "$ddns_tokens"
[ "$family" = ipv4 ] && json_add_string hostype "A" && json_add_string host "$ddns_a"
[ "$family" = ipv6 ] && json_add_string hostype "AAAA" && json_add_string host "$ddns_aaaa"
[ -n "$ddns_srv" ] && {
json_add_string srv "$ddns_srv"
json_add_string srv_serv "$ddns_srv_serv"
json_add_string srv_proto "$ddns_srv_proto"
json_add_string srv_target "$ddns_srv_target"
json_add_int srv_priority "$ddns_srv_priority"
json_add_int srv_weight "$ddns_srv_weight"
}
[ -n "$ddns_https" ] && {
json_add_string https "$ddns_https"
json_add_string https_target "$ddns_https_target"
json_add_string https_svcparams "$ddns_https_svcparams"
json_add_int https_priority "$ddns_https_priority"
}
procd_append_param env "DDNS_PARAM=$(json_dump)"
fi
[ -n "${custom_script}" ] && procd_append_param env "CUSTOM_SCRIPT=${custom_script}"
procd_append_param command -e /usr/lib/natmap/update.sh
procd_set_param respawn
procd_set_param stdout "$log_stdout"
procd_set_param stderr "$log_stderr"
procd_open_data
# configure firewall
json_add_array firewall
if [ "$forward" = 1 -a "$forward_mode" = "dnat" ]; then
json_add_object ''
json_add_string type redirect
json_add_string target DNAT
json_add_string name "$1"
json_add_string family "$family"
json_add_string proto "$proto"
json_add_string src "$($FW -q device $bind_ifname)"
#json_add_string src_dip "$bind_ip" # fix for #11 (The external address of the firewall will not be updated), fw4 cannot add `iifname` rule in `<action>_<fw zone>` chain
json_add_string src_dport "$port"
#json_add_string dest '' # zlan or zwan
json_add_string dest_ip "$forward_target"
[ -n "$PUBPORT" ] \
&& json_add_string dest_port "$PUBPORT" && unset PUBPORT \
|| json_add_string dest_port "$forward_port"
json_add_boolean reflection $natloopback
json_close_object
fi
if [ "$forward" = 1 -a "$forward_mode" = "via" -o "$forward" = 0 ]; then
json_add_object ''
json_add_string type rule
json_add_string target ACCEPT
json_add_string name "$1"
json_add_string family "$family"
json_add_string proto "$proto"
json_add_string direction in
json_add_string src "$($FW -q device $bind_ifname)"
#json_add_string dest '' # '' = input
#json_add_string dest_ip "$bind_ip" # fix for #11 (The external address of the firewall will not be updated), fw4 cannot add `iifname` rule in `<action>_<fw zone>` chain
json_add_string dest_port "$port"
json_close_object
fi
json_close_array
procd_close_data
procd_close_instance
}
clear_status_files() {
local f sid="$1"
mkdir -p "${STATUS_PATH}" 2>/dev/null
pushd "${STATUS_PATH}" >/dev/null
if [ -n "$sid" ]; then
for f in *.json; do
[ "$(jsonfilter -q -i $f -e '@.sid')" = "$sid" ] || continue
rm -f $f; break
done
else
find . -type f -print0 | xargs -0 rm -f --
fi
popd >/dev/null
}
# err_msg__empty <section> <option>
err_msg__empty() {
>&2 echo "$(basename $0): section $1 option $2 cannot be empty"
}
launcher() {
[ "$2" = 0 ] || { >&2 echo "$(basename $0): section $1 validation failed"; return 1; }
[ "$enable" = 0 ] && return 0
# global options
if [ "$udp_mode" = 1 ]; then
local interval=${interval:-$GLOBAL_def_udp_interval} \
stun_cycle=${stun_cycle:-$GLOBAL_def_udp_stun_cycle} \
stun_server=${stun_server:-$GLOBAL_def_udp_stun} \
http_server='' \
;
else
local interval=${interval:-$GLOBAL_def_tcp_interval} \
stun_cycle='' \
stun_server=${stun_server:-$GLOBAL_def_tcp_stun} \
http_server=${http_server:-$GLOBAL_def_http_server} \
;
fi
local fwmark_value=${fwmark_value:-$GLOBAL_def_fwmark_value}
# natmap options
local error=0
local proto ifname bind_ip lan_addr
[ "$udp_mode" = 1 ] && proto=udp || proto=tcp
case "$family" in
ipv4) network_get_device ifname "$DEF_WAN";;
ipv6) network_get_device ifname "$DEF_WAN6";;
esac
local bind_ifname=${bind_ifname:-$ifname}
[ -z "$bind_ifname" ] && >&2 echo "$(basename $0): section $1 option bind_ifname parsing failed, there may be no $family network connection" && let error++
case "$family" in
ipv4)
eval "bind_ip=\"\${NIC_${bind_ifname//[[:punct:]]/_}%/*}\""
lan_addr="${NIC_br_lan%/*}"
;;
ipv6)
eval "bind_ip=\"\${NIC6_${bind_ifname//[[:punct:]]/_}%/*}\""
lan_addr="${NIC6_br_lan%/*}"
;;
esac
[ -z "$bind_ip" ] && >&2 echo "$(basename $0): section $1 option bind_ip parsing failed, there may be no $family network connection" && let error++
[ -z "$port" ] && err_msg__empty $1 port && let error++
grep -qE '^([1-9]\d*)(-([1-9]\d*))?$' <<< "$port" || { >&2 echo "$(basename $0): section $1 option port '$port' is invalid"; let error++; }
[ -z "$fwmark_value" -o -n "$(fwmark_match "$fwmark_value")" ] || { >&2 echo "$(basename $0): section $1 option fwmark_value '$fwmark_value' is invalid"; let error++; }
## forward
if [ "$forward" = 1 ]; then
[ "$forward_mode" = "dnat" -a "$family" = "ipv6" ] && >&2 echo "$(basename $0): section $1 option forward_mode 'dnat' not support under IPv6" && let error++
[ "$forward_mode" != "dnat" ] && natloopback=0
if [ -n "$forward_target" ]; then
if [ -n "$(hostname_match "$forward_target")" -a -z "$(ip_match ipv4 "$forward_target")" ]; then
# hostname
[ "$forward_mode" = "dnat" ] && >&2 echo "$(basename $0): section $1 option forward_target '$forward_target', hostname not support under DNAT mode" && let error++
else
# ipaddr(1)
[ -n "$(grep -E "^127(\.\d+){3}" <<< $forward_target)" -o "$forward_target" = "::1" ] && forward_target="$lan_addr"
[ -n "$(grep -E "^0(\.\d+){3}" <<< $forward_target)" -o "$forward_target" = "::" ] && forward_target="$bind_ip"
if [ -n "$forward_target" ]; then
[ -z "$(ip_match "$family" "$forward_target")" ] && >&2 echo "$(basename $0): section $1 option forward_target '$forward_target' not a $family address" && let error++
else
>&2 echo "$(basename $0): section $1 option forward_target parsing failed, there may be no $family network connection"; let error++
fi
fi
else
err_msg__empty $1 forward_target; let error++
fi
[ -z "$forward_port" ] && err_msg__empty $1 forward_port && let error++
elif [ "$forward" = 0 ]; then
natloopback=0
unset forward_target forward_port
fi
## refresh
if [ "$refresh" = 1 ]; then
[ -x "$clt_script" ] || { >&2 echo "$(basename $0): section $1 option clt_script '$clt_script' is empty or non-executable"; let error++; }
[ -z "$clt_web_port" ] && err_msg__empty $1 clt_web_port && let error++
fi
## notify
if [ "$notify_enable" = 1 ]; then
[ -x "$notify_script" ] || { >&2 echo "$(basename $0): section $1 option notify_script '$notify_script' is empty or non-executable"; let error++; }
fi
## ddns
if [ "$ddns_enable" = 1 ]; then
[ -x "$ddns_script" ] || { >&2 echo "$(basename $0): section $1 option ddns_script '$ddns_script' is empty or non-executable"; let error++; }
if [ -n "$ddns_srv" ]; then
[ -z "$ddns_srv_serv" ] && err_msg__empty $1 ddns_srv_serv && let error++
local ddns_srv_target=${ddns_srv_target:-$ddns_srv}
fi
fi
# review
[ -n "$NATMAP_DEBUG" ] && {
local v ucivv="enable interval stun_cycle stun_server http_server fwmark_value comment udp_mode proto family bind_ifname ifname bind_ip lan_addr port port_pointer forward forward_mode natloopback forward_congestion forward_timeout forward_target forward_port refresh clt_script clt_scheme clt_web_port clt_username clt_password notify_enable notify_script notify_tokens notify_custom_domain notify_text ddns_enable ddns_script ddns_tokens ddns_a ddns_aaaa ddns_srv ddns_srv_serv ddns_srv_proto ddns_srv_target ddns_srv_priority ddns_srv_weight ddns_https ddns_https_target ddns_https_svcparams ddns_https_priority custom_script"
for v in $ucivv; do eval "echo $1 $v=\'\$$v\'"; done # ash not support ${!v}
}
[ "$error" -gt 0 ] && return 1
natmap_instance "$1"
}
service_triggers() {
procd_add_reload_trigger "$NAME" 'network'
local interfaces
config_foreach load_interfaces $NATMAPSECTION
[ -n "$interfaces" ] && {
for n in $interfaces; do
procd_add_reload_interface_trigger $n
done
} || {
for n in $DEF_WAN $DEF_WAN6; do
procd_add_reload_interface_trigger $n
done
}
interfaces=$(uci show network|grep "device='br-lan'"|cut -f2 -d'.')
[ -n "$interfaces" ] && {
for n in $interfaces; do
procd_add_reload_interface_trigger $n
done
}
procd_add_validation validate_section_natmap
}
start_service() {
local sid="$1"
config_foreach validate_section_global "$GLOBALSECTION" define_global || return $?
[ "${GLOBAL_enable:=0}" == "0" ] && return 1
if [ -n "$sid" ]; then
clear_status_files "$sid"
validate_section_natmap "$sid" launcher
else
clear_status_files
config_foreach validate_section_natmap "$NATMAPSECTION" launcher
fi
}
stop_service() {
local sid="$1"
clear_status_files "$sid"
}
service_stopped() {
sleep 1s # Wait for procd_kill complete
ps | grep natmap | awk '{print $1}' | xargs kill -9
}
reload_service() {
stop "$@"
start "$@"
}
service_started() { procd_set_config_changed firewall; }
service_stopped() { procd_set_config_changed firewall; }