diff --git a/net/travelmate/Makefile b/net/travelmate/Makefile index 17e8cac360..4bc8f96251 100644 --- a/net/travelmate/Makefile +++ b/net/travelmate/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=travelmate -PKG_VERSION:=2.4.0 -PKG_RELEASE:=2 +PKG_VERSION:=2.4.5 +PKG_RELEASE:=1 PKG_LICENSE:=GPL-3.0-or-later PKG_MAINTAINER:=Dirk Brenken diff --git a/net/travelmate/files/25-travelmate.hotplug b/net/travelmate/files/25-travelmate.hotplug index a61ed3e42a..ff3517577d 100755 --- a/net/travelmate/files/25-travelmate.hotplug +++ b/net/travelmate/files/25-travelmate.hotplug @@ -8,8 +8,14 @@ trm_init="/etc/init.d/travelmate" trm_funlib="/usr/lib/travelmate-functions.sh" -trm_ntplock="/var/lock/travelmate.ntp.lock" +trm_ntplock="/var/run/travelmate/travelmate.ntp.lock" +# ensure runtime directory exists +# +[ ! -d "${trm_ntplock%/*}" ] && mkdir -p "${trm_ntplock%/*}" + +# check for ntp hotplug event and travelmate service autostart condition +# if mkdir "${trm_ntplock}" 2>/dev/null; then if [ "${ACTION}" = "stratum" ] && "${trm_init}" enabled; then . "${trm_funlib}" diff --git a/net/travelmate/files/README.md b/net/travelmate/files/README.md index a934f52189..b8f7351842 100644 --- a/net/travelmate/files/README.md +++ b/net/travelmate/files/README.md @@ -36,11 +36,10 @@ automatically (re)connnects to configured APs/hotspots as they become available. * VPN hook supports 'wireguard' or 'openvpn' client setups to handle VPN (re)connections automatically * Email hook via 'msmtp' sends notification e-mails after every successful uplink connect * Proactively scan and switch to a higher priority uplink, replacing an existing connection -* Connection tracking logs start and end date of an uplink connection * Check router subnet vs. uplink subnet, to show conflicts with router LAN network -* Automatically disable the uplink after n minutes, e.g. for timed connections -* Automatically (re)enable the uplink after n minutes, e.g. after failed login attempts * (Optional) Generate a random unicast MAC address for each uplink connection +* (Optional) Evil twin protection by skipping access points with locally-administered (LAA) BSSIDs +* Configurable retry limit per uplink, with optional unlimited retry mode * NTP time sync before sending emails * procd init and ntp-hotplug support * Runtime information available via LuCI & via 'status' init command @@ -100,8 +99,9 @@ automatically (re)connnects to configured APs/hotspots as they become available. | trm_autoadd | 0, disabled | automatically add open uplinks like hotel captive portals to your wireless config | | trm_ssidfilter | -, not set | list of SSID patterns for filtering/skipping specific open uplinks, e.g. 'Chromecast*' | | trm_randomize | 0, disabled | generate a random unicast MAC address for each uplink connection | +| trm_eviltwin | 0, disabled | detect and skip access points with locally administered (LAA) BSSIDs to mitigate evil twin attacks | | trm_triggerdelay | 2 | additional trigger delay in seconds before travelmate processing begins | -| trm_maxretry | 3 | retry limit to connect to an uplink | +| trm_maxretry | 3 | retry limit to connect to an uplink, set to '0' for unlimited retries | | trm_minquality | 35 | minimum signal quality threshold as percent for conditional uplink (dis-) connections | | trm_maxwait | 30 | how long should travelmate wait for a successful wlan uplink connection | | trm_timeout | 60 | overall retry timeout in seconds | @@ -123,17 +123,13 @@ automatically (re)connnects to configured APs/hotspots as they become available. | Option | Default | Description/Valid Values | | :----------------- | :--------------------------------- | :---------------------------------------------------------------------------------------------------- | -| enabled | 1, enabled | enable or disable the uplink, automatically set if the retry limit or the conn. expiry was reached | +| enabled | 1, enabled | enable or disable the uplink, automatically set if the retry limit was reached | | device | -, not set | match the 'device' in the wireless config section | | ssid | -, not set | match the 'ssid' in the wireless config section | | bssid | -, not set | match the 'bssid' in the wireless config section | -| con_start | -, not set | connection start (will be automatically set after a successful ntp sync) | -| con_end | -, not set | connection end (will be automatically set after a successful ntp sync) | -| con_start_expiry | 0, disabled | automatically disable the uplink after n minutes, e.g. for timed connections | -| con_end_expiry | 0, disabled | automatically (re-)enable the uplink after n minutes, e.g. after failed login attempts | | script | -, not set | reference to an external auto login script for captive portals | | script_args | -, not set | optional runtime args for the auto login script | -| macaddr | -, not set | use a specified MAC address for the uplink +| macaddr | -, not set | use a specified MAC address for the uplink | | vpn | 0, disabled | automatically handle VPN (re-) connections | | vpnservice | -, not set | reference the already configured 'wireguard' or 'openvpn' client instance as vpn provider | | vpniface | -, not set | the logical vpn interface, e.g. 'wg0' or 'tun0' | @@ -196,7 +192,8 @@ Hopefully more scripts for different captive portals will be provided by the com ## Runtime information -**Receive Travelmate runtime information:** +Travelmate stores all runtime files (pid, scan results, status JSON, etc.) under `/var/run/travelmate/`. The runtime status is exposed both via the LuCI status panel and the init command: +

 root@2go:~# /etc/init.d/travelmate status
 ::: travelmate runtime information
@@ -207,7 +204,7 @@ root@2go:~# /etc/init.d/travelmate status
   + station_mac        : 42:40:45:EC:B3:D1
   + station_interfaces : wwan, -
   + station_subnet     : 10.168.20.0 (lan: 10.200.1.0)
-  + run_flags          : scan: active, captive: ✔, proactive: ✔, netcheck: ✘, autoadd: ✘, randomize: ✔
+  + run_flags          : captive: ✔, proactive: ✔, netcheck: ✘, autoadd: ✘, randomize: ✔, eviltwin: ✘
   + ext_hooks          : ntp: ✔, vpn: ✘, mail: ✔
   + last_run           : 2025.12.11-09:08:24
   + system             : Cudy TR3000 v1, mediatek/filogic, OpenWrt SNAPSHOT (r32287-1c7ec8ab19)
diff --git a/net/travelmate/files/travelmate-functions.sh b/net/travelmate/files/travelmate-functions.sh
index 22fa440a87..e1336d5dba 100644
--- a/net/travelmate/files/travelmate-functions.sh
+++ b/net/travelmate/files/travelmate-functions.sh
@@ -18,6 +18,7 @@ trm_vpn="0"
 trm_netcheck="0"
 trm_autoadd="0"
 trm_randomize="0"
+trm_eviltwin="0"
 trm_mail="0"
 trm_mailtemplate="/etc/travelmate/mail.template"
 trm_vpnpgm="/etc/travelmate/travelmate.vpn"
@@ -35,16 +36,24 @@ trm_vpnifacelist=""
 trm_vpninfolist=""
 trm_stdvpnservice=""
 trm_stdvpniface=""
-trm_rtfile="/tmp/trm_runtime.json"
+trm_subnet=""
+trm_subnet_last=""
+trm_lannet=""
+trm_rundir="/var/run/travelmate"
+trm_ntplock="${trm_rundir}/travelmate.ntp.lock"
+trm_vpnfile="${trm_rundir}/travelmate.vpn"
+trm_mailfile="${trm_rundir}/travelmate.mail"
+trm_refreshfile="${trm_rundir}/travelmate.refresh"
+trm_pidfile="${trm_rundir}/travelmate.pid"
+trm_scanfile="${trm_rundir}/travelmate.scan"
+trm_tmpfile="${trm_rundir}/travelmate.tmp"
+trm_rtfile="${trm_rundir}/travelmate.runtime.json"
 trm_captiveurl="http://detectportal.firefox.com"
 trm_useragent="Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0"
-trm_ntplock="/var/lock/travelmate.ntp.lock"
-trm_vpnfile="/var/state/travelmate.vpn"
-trm_mailfile="/var/state/travelmate.mail"
-trm_refreshfile="/var/state/travelmate.refresh"
-trm_pidfile="/var/run/travelmate.pid"
-trm_scanfile="/var/run/travelmate.scan"
-trm_tmpfile="/var/run/travelmate.tmp"
+
+# ensure runtime directory exists
+#
+[ ! -d "${trm_rundir}" ] && mkdir -p "${trm_rundir}"
 
 # gather system information
 #
@@ -66,13 +75,15 @@ f_system() {
 f_cmd() {
 	local cmd pri_cmd="${1}" sec_cmd="${2}"
 
+	# check for primary command, if not found check for secondary command (if provided), if still not found log an error
+	#
 	cmd="$(command -v "${pri_cmd}" 2>/dev/null)"
-	if [ ! -x "${cmd}" ]; then
+	if [ -z "${cmd}" ]; then
 		if [ -n "${sec_cmd}" ]; then
 			[ "${sec_cmd}" = "optional" ] && return
 			cmd="$(command -v "${sec_cmd}" 2>/dev/null)"
 		fi
-		if [ -x "${cmd}" ]; then
+		if [ -n "${cmd}" ]; then
 			printf "%s" "${cmd}"
 		else
 			f_log "emerg" "command '${pri_cmd:-"-"}'/'${sec_cmd:-"-"}' not found"
@@ -87,42 +98,44 @@ f_cmd() {
 f_conf() {
 	local device
 
-	 unset trm_stalist trm_radiolist trm_uplinklist trm_vpnifacelist trm_uplinkcfg trm_activesta trm_ssidfilter
+	unset trm_stalist trm_radiolist trm_vpnifacelist trm_uplinkcfg trm_activesta trm_ssidfilter
 
-	 config_cb() {
+	config_cb() {
 		option_cb() {
 			local option="${1}" value="${2//\"/\\\"}"
 
 			case "${option}" in
-				*[!a-zA-Z0-9_]*)
-					;;
-				*)
-					eval "${option}=\"\${value}\""
-					;;
+			*[!a-zA-Z0-9_]*) ;;
+
+			*)
+				eval "${option}=\"\${value}\""
+				;;
 			esac
 		}
 		list_cb() {
 			local option="${1}" value="${2//\"/\\\"}"
 
-			 case "${option}" in
-				*[!a-zA-Z0-9_]*)
-					;;
-				*)
-					eval "append=\"\${${option}}\""
-					if [ -n "${append}" ]; then
-						eval "${option}=\"${append} ${value}\""
-					else
-						eval "${option}=\"${value}\""
-					fi
-					;;
+			case "${option}" in
+			*[!a-zA-Z0-9_]*) ;;
+
+			*)
+				eval "append=\"\${${option}}\""
+				if [ -n "${append}" ]; then
+					eval "${option}=\"\${${option}} \${value}\""
+				else
+					eval "${option}=\"\${value}\""
+				fi
+				;;
 			esac
 		}
 	}
 	config_load travelmate
 
-	[ "${trm_action}" = "stop" ] && return 0
-
-	if [ "${trm_enabled}" != "1" ]; then
+	# early exit on stop action, otherwise run runtime sanity checks
+	#
+	if [ "${trm_action}" = "stop" ]; then
+		return 0
+	elif [ "${trm_enabled}" != "1" ]; then
 		f_log "info" "travelmate is currently disabled, please set 'trm_enabled' to '1' to use this service"
 		/etc/init.d/travelmate stop
 	elif [ -z "${trm_iface}" ]; then
@@ -133,6 +146,8 @@ f_conf() {
 		/etc/init.d/travelmate stop
 	fi
 
+	# apply wifi-device config, commit and reload on changes
+	#
 	config_load wireless
 	config_foreach f_setdev "wifi-device"
 	if [ -n "$(uci -q changes "wireless")" ]; then
@@ -140,6 +155,8 @@ f_conf() {
 		f_wifi
 	fi
 
+	# init runtime json (create empty data object on missing/invalid file)
+	#
 	json_load_file "${trm_rtfile}" >/dev/null 2>&1
 	if ! json_select data >/dev/null 2>&1; then
 		: >"${trm_rtfile}"
@@ -147,11 +164,15 @@ f_conf() {
 		json_add_object "data"
 	fi
 
+	# enumerate logical vpn interfaces (only if vpn enabled and list still empty)
+	#
 	if [ "${trm_vpn}" = "1" ] && [ -z "${trm_vpninfolist}" ]; then
 		config_load network
 		config_foreach f_getvpn "interface"
 	fi
 
+	# build curl fetch parameters, bind to uplink device if known
+	#
 	trm_fetchparm="--silent --show-error --location --fail --referer http://www.example.com --retry $((trm_maxwait / 6)) --retry-delay $((trm_maxwait / 6)) --max-time $((trm_maxwait / 6))"
 	device="$("${trm_ifstatuscmd}" "${trm_iface}" | "${trm_jsoncmd}" -ql1 -e '@.device')"
 	[ -n "${device}" ] && trm_fetchparm="${trm_fetchparm} --interface ${device}"
@@ -159,16 +180,19 @@ f_conf() {
 	f_log "debug" "f_conf      ::: frontend: ${trm_fver}, backend: ${trm_bver}, sys_ver: ${trm_sysver}, fetch_parm: ${trm_fetchparm:-"-"}"
 }
 
+# travelmate pid file handling
+#
 f_rmpid() {
 	local ppid pid
 
 	if [ -s "${trm_pidfile}" ]; then
-		ppid="$(< "${trm_pidfile}")"
+		ppid="$("${trm_catcmd}" "${trm_pidfile}" 2>/dev/null)"
 		if [ -n "${ppid}" ]; then
 			pid="$("${trm_pgrepcmd}" -nf "sleep ${trm_timeout} 0" -P ${ppid} 2>/dev/null)"
 			[ -n "${pid}" ] && "${trm_killcmd}" -INT ${pid} 2>/dev/null
 		fi
 	fi
+
 	f_log "debug" "f_rmpid     ::: ppid: ${ppid:-"-"}, pid: ${pid:-"-"}, timeout: ${trm_timeout}"
 }
 
@@ -182,29 +206,34 @@ f_trim() {
 	printf "%s" "${trim}"
 }
 
-# status helper function
-#
-f_char() {
-	local result input="${1}"
-
-	[ "${input}" = "1" ] && result="✔" || result="✘"
-	printf "%s" "${result}"
-}
-
 # wifi helper function
 #
 f_wifi() {
-	local status radio radio_up timeout="0"
+	local parse status up pending radio radio_up timeout="0"
 
+	# trigger wifi reload, then poll each radio until ready (up=true, pending=false)
+	#
 	"${trm_wificmd}" reload
 	for radio in ${trm_radiolist}; do
 		while :; do
+
+			# global timeout abort across all radios
+			#
 			if [ "${timeout}" -ge "${trm_maxwait}" ]; then
 				break 2
 			fi
 			status="$("${trm_wificmd}" status 2>/dev/null)"
-			if [ "$(printf "%s" "${status}" | "${trm_jsoncmd}" -ql1 -e "@.${radio}.up")" != "true" ] ||
-				[ "$(printf "%s" "${status}" | "${trm_jsoncmd}" -ql1 -e "@.${radio}.pending")" != "false" ]; then
+			parse="$(printf "%s" "${status}" | "${trm_jsoncmd}" -e "@.${radio}.up" -e "@.${radio}.pending")"
+			{
+				IFS= read -r up
+				IFS= read -r pending
+			} <<-EOF
+				${parse}
+			EOF
+
+			# not ready: trigger 'wifi up' once per radio, then keep polling
+			#
+			if [ "${up}" != "true" ] || [ "${pending}" != "false" ]; then
 				if [ "${radio}" != "${radio_up}" ]; then
 					"${trm_wificmd}" up "${radio}"
 					radio_up="${radio}"
@@ -216,10 +245,14 @@ f_wifi() {
 			fi
 		done
 	done
+
+	# settle delay if all radios came up within budget
+	#
 	if [ "${timeout}" -lt "${trm_maxwait}" ]; then
 		sleep "$((trm_maxwait / 6))"
 		timeout="$((timeout + (trm_maxwait / 6)))"
 	fi
+
 	f_log "debug" "f_wifi      ::: radio_list: ${trm_radiolist}, ssid_filter: ${trm_ssidfilter:-"-"}, radio: ${radio}, timeout: ${timeout}"
 }
 
@@ -228,11 +261,15 @@ f_wifi() {
 f_vpn() {
 	local rc info iface vpn vpn_service vpn_iface vpn_instance vpn_status vpn_action="${1}"
 
-	if  [ "${trm_vpn}" = "1" ] && [ -n "${trm_vpninfolist}" ]; then
+	# only proceed when vpn handling is enabled and known interfaces exist
+	#
+	if [ "${trm_vpn}" = "1" ] && [ -n "${trm_vpninfolist}" ]; then
 		vpn="$(f_getval "vpn")"
 		vpn_service="$(f_getval "vpnservice")"
 		vpn_iface="$(f_getval "vpniface")"
 
+		# initial cleanup: tear down all known vpn ifaces and openvpn instances
+		#
 		if [ ! -f "${trm_vpnfile}" ] || { [ -f "${trm_vpnfile}" ] && [ "${vpn_action}" = "enable" ]; }; then
 			for info in ${trm_vpninfolist}; do
 				iface="${info%%&&*}"
@@ -250,6 +287,9 @@ f_vpn() {
 			done
 			rm -f "${trm_vpnfile}"
 			sleep 1
+
+		# switch path: tear down only foreign vpn ifaces, keep the configured one
+		#
 		elif [ "${vpn}" = "1" ] && [ -n "${vpn_iface}" ] && [ "${vpn_action}" = "enable_keep" ]; then
 			for info in ${trm_vpninfolist}; do
 				iface="${info%%&&*}"
@@ -271,182 +311,179 @@ f_vpn() {
 				fi
 			done
 		fi
+
+		# invoke external vpn program for valid enable/disable transitions
+		#
 		if [ -x "${trm_vpnpgm}" ] && [ -n "${vpn_service}" ] && [ -n "${vpn_iface}" ]; then
 			if { [ "${vpn_action}" = "disable" ] && [ -f "${trm_vpnfile}" ]; } ||
-				{ [ -d "${trm_ntplock}" ] && { [ "${vpn}" = "1" ] && [ "${vpn_action%%_*}" = "enable" ] && [ ! -f "${trm_vpnfile}" ]; } ||
-				{ [ "${vpn}" != "1" ] && [ "${vpn_action%%_*}" = "enable" ] && [ -f "${trm_vpnfile}" ]; }; }; then
-					if [ "${trm_connection%%/*}" = "net ok" ] || [ "${vpn_action}" = "disable" ]; then
-						for info in ${trm_vpninfolist}; do
-							iface="${info%%&&*}"
-							if [ "${iface}" = "${vpn_iface}" ]; then
-								[ "${iface}" = "${info}" ] && vpn_instance="" || vpn_instance="${info##*&&}"
-								break
-							fi
-						done
-						"${trm_vpnpgm}" "${vpn:-"0"}" "${vpn_action}" "${vpn_service}" "${vpn_iface}" "${vpn_instance}" >/dev/null 2>&1
-						rc="${?}"
-					fi
+				{ [ "${vpn}" != "1" ] && [ "${vpn_action%%_*}" = "enable" ] && [ -f "${trm_vpnfile}" ]; } ||
+				{ [ -d "${trm_ntplock}" ] && [ "${vpn}" = "1" ] && [ "${vpn_action%%_*}" = "enable" ] && [ ! -f "${trm_vpnfile}" ]; }; then
+				if [ "${trm_connection%%/*}" = "net ok" ] || [ "${vpn_action}" = "disable" ]; then
+					for info in ${trm_vpninfolist}; do
+						iface="${info%%&&*}"
+						if [ "${iface}" = "${vpn_iface}" ]; then
+							[ "${iface}" = "${info}" ] && vpn_instance="" || vpn_instance="${info##*&&}"
+							break
+						fi
+					done
+					"${trm_vpnpgm}" "${vpn:-"0"}" "${vpn_action}" "${vpn_service}" "${vpn_iface}" "${vpn_instance}" >/dev/null 2>&1
+					rc="${?}"
+				fi
 			fi
 			[ -n "${rc}" ] && f_genstatus
 		fi
 	fi
+
 	f_log "debug" "f_vpn       ::: vpn: ${trm_vpn:-"-"}, enabled: ${vpn:-"-"}, action: ${vpn_action}, vpn_service: ${vpn_service:-"-"}, vpn_iface: ${vpn_iface:-"-"}, vpn_instance: ${vpn_instance:-"-"}, vpn_infolist: ${trm_vpninfolist:-"-"}, connection: ${trm_connection%%/*}, rc: ${rc:-"-"}"
 }
 
 # mac helper function
 #
 f_mac() {
-	local raw result ifname macaddr action="${1}" section="${2}"
+	local raw result macaddr action="${1}" section="${2}"
 
+	# set mac address for wifi station interface, with optional randomization (LAA) or fallback to driver-assigned mac via ubus
+	#
 	if [ "${action}" = "set" ]; then
 		macaddr="$(f_getval "macaddr")"
+
+		# use macaddr from uplink config
+		#
 		if [ -n "${macaddr}" ]; then
 			result="${macaddr}"
 			uci_set "wireless" "${section}" "macaddr" "${result}"
+
+		# generate random LAA mac (second nibble forced to 2/6/A/E)
+		#
 		elif [ "${trm_randomize}" = "1" ]; then
-			result="$(hexdump -n6 -ve '/1 "%.02X "' /dev/random 2>/dev/null |
-				"${trm_awkcmd}" -v local="2,6,A,E" -v seed="$(date +%s)" 'BEGIN{srand(seed)}NR==1{split(local,b,",");
+			result="$(hexdump -n6 -ve '/1 "%.02X "' /dev/urandom 2>/dev/null |
+				"${trm_awkcmd}" -v local="2,6,A,E" 'BEGIN{srand()}NR==1{split(local,b,",");
 				seed=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s",substr($1,0,1),b[seed],$2,$3,$4,$5,$6}')"
 			uci_set "wireless" "${section}" "macaddr" "${result}"
+
+		# clear override, fall back to driver-assigned mac via ubus
+		#
 		else
 			uci_remove "wireless" "${section}" "macaddr" 2>/dev/null
 			raw="$("${trm_ubuscmd}" -S call network.wireless status 2>/dev/null)"
-			ifname="$(printf "%s" "${raw}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].ifname')"
 			result="$(printf "%s" "${raw}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].config.macaddr')"
 		fi
+
+	# get mac address for wifi station interface, with optional fallback to ubus
+	#
 	elif [ "${action}" = "get" ]; then
 		result="$(uci_get "wireless" "${section}" "macaddr")"
 		if [ -z "${result}" ]; then
 			raw="$("${trm_ubuscmd}" -S call network.wireless status 2>/dev/null)"
-			ifname="$(printf "%s" "${raw}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].ifname')"
 			result="$(printf "%s" "${raw}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].config.macaddr')"
 		fi
 	fi
 	printf "%s" "${result}"
+
 	f_log "debug" "f_mac       ::: action: ${action:-"-"}, section: ${section:-"-"}, macaddr: ${macaddr:-"-"}, result: ${result:-"-"}"
 }
 
-# set connection information
-#
-f_ctrack() {
-	local expiry action="${1}"
-
-	if [ -n "${trm_uplinkcfg}" ]; then
-		case "${action}" in
-			"start")
-				uci_remove "travelmate" "${trm_uplinkcfg}" "con_start" 2>/dev/null
-				uci_remove "travelmate" "${trm_uplinkcfg}" "con_end" 2>/dev/null
-				if [ -d "${trm_ntplock}" ]; then
-					uci_set "travelmate" "${trm_uplinkcfg}" "con_start" "$(date "+%Y.%m.%d-%H:%M:%S")"
-				fi
-				;;
-			"refresh")
-				if [ -d "${trm_ntplock}" ] && [ -z "$(uci_get "travelmate" "${trm_uplinkcfg}" "con_start")" ]; then
-					uci_set "travelmate" "${trm_uplinkcfg}" "con_start" "$(date "+%Y.%m.%d-%H:%M:%S")"
-				fi
-				;;
-			"end")
-				if [ -d "${trm_ntplock}" ]; then
-					uci_set "travelmate" "${trm_uplinkcfg}" "con_end" "$(date "+%Y.%m.%d-%H:%M:%S")"
-				fi
-				;;
-			"start_expiry")
-				if [ -d "${trm_ntplock}" ]; then
-					expiry="$(uci_get "travelmate" "${trm_uplinkcfg}" "con_start_expiry")"
-					uci_set "travelmate" "${trm_uplinkcfg}" "enabled" "0"
-					uci_set "travelmate" "${trm_uplinkcfg}" "con_end" "$(date "+%Y.%m.%d-%H:%M:%S")"
-					f_log "info" "uplink '${radio}/${essid}/${bssid:-"-"}' expired after ${expiry} minutes"
-				fi
-				;;
-			"end_expiry")
-				if [ -d "${trm_ntplock}" ]; then
-					expiry="$(uci_get "travelmate" "${trm_uplinkcfg}" "con_end_expiry")"
-					uci_set "travelmate" "${trm_uplinkcfg}" "enabled" "1"
-					uci_remove "travelmate" "${trm_uplinkcfg}" "con_start" 2>/dev/null
-					uci_remove "travelmate" "${trm_uplinkcfg}" "con_end" 2>/dev/null
-					f_log "info" "uplink '${radio}/${essid}/${bssid:-"-"}' re-enabled after ${expiry} minutes"
-				fi
-				;;
-			"disabled")
-				uci_set "travelmate" "${trm_uplinkcfg}" "enabled" "0"
-				if [ -d "${trm_ntplock}" ]; then
-					uci_set "travelmate" "${trm_uplinkcfg}" "con_end" "$(date "+%Y.%m.%d-%H:%M:%S")"
-				fi
-				;;
-		esac
-		if [ -n "$(uci -q changes "travelmate")" ]; then
-			uci_commit "travelmate"
-			if [ ! -f "${trm_refreshfile}" ]; then
-				printf "%s" "cfg_reload" >"${trm_refreshfile}"
-			fi
-		fi
-	fi
-}
-
 # get openvpn information
 #
 f_getovpn() {
 	local file instance device
 
+	# scan /etc/openvpn/*.conf and *.ovpn files, extract dev and instance name
+	#
 	for file in /etc/openvpn/*.conf /etc/openvpn/*.ovpn; do
 		if [ -f "${file}" ]; then
 			instance="${file##*/}"
 			instance="${instance%.conf}"
 			instance="${instance%.ovpn}"
 			device="$("${trm_awkcmd}" '/^[[:space:]]*dev /{print $2}' "${file}")"
+
+			# normalize bare tun/tap to tun0/tap0
+			#
 			[ "${device}" = "tun" ] && device="tun0"
 			[ "${device}" = "tap" ] && device="tap0"
-			if [ -n "${device}" ] && [ -n "${instance}" ] && ! printf "%s" "${trm_ovpninfolist}" | "${trm_grepcmd}" -q "${device}"; then
-				trm_ovpninfolist="${trm_ovpninfolist} ${device}&&${instance}"
+			if [ -n "${device}" ] && [ -n "${instance}" ]; then
+				case " ${trm_ovpninfolist} " in
+				*" ${device}&&"*) ;;
+				*) trm_ovpninfolist="${trm_ovpninfolist} ${device}&&${instance}" ;;
+				esac
 			fi
 		fi
 	done
 
+	# additionally merge uci-managed openvpn instances
+	#
 	uci_config() {
 		local device section="${1}"
 
 		device="$(uci_get "openvpn" "${section}" "dev")"
 		[ "${device}" = "tun" ] && device="tun0"
 		[ "${device}" = "tap" ] && device="tap0"
-		if [ -n "${device}" ] && ! printf "%s" "${trm_ovpninfolist}" | "${trm_grepcmd}" -q "${device}"; then
-			trm_ovpninfolist="${trm_ovpninfolist} ${device}&&${section}"
+		if [ -n "${device}" ]; then
+			case " ${trm_ovpninfolist} " in
+			*" ${device}&&"*) ;;
+			*) trm_ovpninfolist="${trm_ovpninfolist} ${device}&&${section}" ;;
+			esac
 		fi
 	}
 	if [ -f "/etc/config/openvpn" ]; then
 		config_load openvpn
 		config_foreach uci_config "openvpn"
 	fi
+
 	f_log "debug" "f_getovpn   ::: ovpn_infolist: ${trm_ovpninfolist:-"-"}"
 }
 
 # get logical vpn network interfaces
 #
 f_getvpn() {
-	local info proto device iface="${1}"
+	local info proto device iface="${1}" match="1"
 
+	# read proto and device from network config
+	#
 	proto="$(uci_get "network" "${iface}" "proto")"
 	device="$(uci_get "network" "${iface}" "device")"
-	if [ "${proto}" = "wireguard" ]; then
-		if [ -z "${trm_vpnifacelist}" ] || printf "%s" "${trm_vpnifacelist}" | "${trm_grepcmd}" -q "${iface}"; then
-			if ! printf "%s" "${trm_vpninfolist}" | "${trm_grepcmd}" -q "${iface}"; then
-				trm_vpninfolist="$(f_trim "${trm_vpninfolist} ${iface}")"
+
+	# optional filter: only handle ifaces listed in trm_vpnifacelist
+	#
+	if [ -n "${trm_vpnifacelist}" ]; then
+		match="0"
+		case " ${trm_vpnifacelist} " in
+		*" ${iface} "*) match="1" ;;
+		esac
+	fi
+
+	# only proceed if proto is wireguard or none with matching openvpn device, and optional iface filter matches
+	#
+	if [ "${match}" = "1" ]; then
+
+		# wireguard: append iface (no instance), deduped
+		#
+		if [ "${proto}" = "wireguard" ]; then
+			case " ${trm_vpninfolist} " in
+			*" ${iface} "* | *" ${iface}&&"*) ;;
+			*) trm_vpninfolist="$(f_trim "${trm_vpninfolist} ${iface}")" ;;
+			esac
+
+		# openvpn (proto=none + device): lazy-populate ovpn list, then map device -> instance
+		#
+		elif [ "${proto}" = "none" ] && [ -n "${device}" ]; then
+			if [ -z "${trm_ovpninfolist}" ]; then
+				f_getovpn
 			fi
-		fi
-	elif [ "${proto}" = "none" ] && [ -n "${device}" ]; then
-		if [ -z "${trm_ovpninfolist}" ]; then
-			f_getovpn
-		fi
-		if [ -z "${trm_vpnifacelist}" ] || printf "%s" "${trm_vpnifacelist}" | "${trm_grepcmd}" -q "${iface}"; then
 			for info in ${trm_ovpninfolist}; do
 				if [ "${info%%&&*}" = "${device}" ]; then
-					if ! printf "%s" "${trm_vpninfolist}" | "${trm_grepcmd}" -q "${iface}"; then
+					case " ${trm_vpninfolist} " in
+					*" ${iface} "* | *" ${iface}&&"*) ;;
+					*)
 						trm_vpninfolist="$(f_trim "${trm_vpninfolist} ${iface}&&${info##*&&}")"
 						break
-					fi
+						;;
+					esac
 				fi
 			done
 		fi
 	fi
+
 	f_log "debug" "f_getvpn    ::: iface: ${iface:-"-"}, proto: ${proto:-"-"}, device: ${device:-"-"}, vpn_ifacelist: ${trm_vpnifacelist:-"-"}, vpn_infolist: ${trm_vpninfolist:-"-"}"
 }
 
@@ -464,6 +501,7 @@ f_getgw() {
 		result="true"
 	fi
 	printf "%s" "${result}"
+
 	f_log "debug" "f_getgw     ::: wan4_gw: ${wan4_gw:-"-"}, wan6_gw: ${wan6_gw:-"-"}, result: ${result}"
 }
 
@@ -472,6 +510,7 @@ f_getgw() {
 f_getcfg() {
 	local t_radio t_essid t_bssid radio="${1}" essid="${2}" bssid="${3}" cnt="0"
 
+	trm_uplinkcfg=""
 	while uci_get "travelmate" "@uplink[${cnt}]" >/dev/null 2>&1; do
 		t_radio="$(uci_get "travelmate" "@uplink[${cnt}]" "device")"
 		t_essid="$(uci_get "travelmate" "@uplink[${cnt}]" "ssid")"
@@ -488,21 +527,36 @@ f_getcfg() {
 # get travelmate option value in 'uplink' sections
 #
 f_getval() {
-	local result t_option="${1}"
+	local option="${1}" default="${2}"
 
 	if [ -n "${trm_uplinkcfg}" ]; then
-		result="$(uci_get "travelmate" "${trm_uplinkcfg}" "${t_option}")"
-		printf "%s" "${result}"
+		uci_get "travelmate" "${trm_uplinkcfg}" "${option}" "${default}"
+	else
+		printf "%s" "${default}"
 	fi
 }
 
 # set 'wifi-device' sections
 #
 f_setdev() {
-	local disabled radio="${1}"
+	local disabled radio="${1}" match="0"
 
-	if { [ -z "${trm_radio}" ] && ! printf "%s" "${trm_radiolist}" | "${trm_grepcmd}" -q "${radio}"; } ||
-		{ [ -n "${trm_radio}" ] && printf "%s" "${trm_radio}" | "${trm_grepcmd}" -q "${radio}"; }; then
+	# match radio against optional filter (trm_radio); empty list -> match all not yet tracked
+	#
+	if [ -z "${trm_radio}" ]; then
+		case " ${trm_radiolist} " in
+		*" ${radio} "*) ;;
+		*) match="1" ;;
+		esac
+	else
+		case " ${trm_radio} " in
+		*" ${radio} "*) match="1" ;;
+		esac
+	fi
+
+	# append (or prepend on reverse mode) to radiolist and ensure device is enabled
+	#
+	if [ "${match}" = "1" ]; then
 		if [ "${trm_revradio}" = "1" ]; then
 			trm_radiolist="$(f_trim "${radio} ${trm_radiolist}")"
 		else
@@ -513,53 +567,45 @@ f_setdev() {
 			uci_set wireless "${radio}" "disabled" "0"
 		fi
 	fi
+
 	f_log "debug" "f_setdev    ::: device: ${radio:-"-"}, radio: ${trm_radio:-"-"}, radio_list: ${trm_radiolist:-"-"}, disabled: ${disabled:-"-"}"
 }
 
 # set 'wifi-iface' sections
 #
 f_setif() {
-	local mode radio essid bssid enabled disabled d1 d2 d3 con_start con_end con_start_expiry con_end_expiry section="${1}" proactive="${2}"
+	local mode radio essid bssid enabled disabled section="${1}" proactive="${2}"
 
+	# skip sections whose radio is not in the active radiolist
+	#
 	radio="$(uci_get "wireless" "${section}" "device")"
-	if ! printf "%s" "${trm_radiolist}" | "${trm_grepcmd}" -q "${radio}"; then
-		return
-	fi
+	case " ${trm_radiolist} " in
+	*" ${radio} "*) ;;
+	*) return ;;
+	esac
+
+	# read iface config and resolve uplink-enabled flag from travelmate config
+	#
 	mode="$(uci_get "wireless" "${section}" "mode")"
 	essid="$(uci_get "wireless" "${section}" "ssid")"
 	bssid="$(uci_get "wireless" "${section}" "bssid")"
 	disabled="$(uci_get "wireless" "${section}" "disabled")"
 
 	f_getcfg "${radio}" "${essid}" "${bssid}"
+	enabled="$(f_getval "enabled" "0")"
 
-	enabled="$(f_getval "enabled")"
-	con_start="$(f_getval "con_start")"
-	con_end="$(f_getval "con_end")"
-	con_start_expiry="$(f_getval "con_start_expiry")"
-	con_end_expiry="$(f_getval "con_end_expiry")"
-
-	if [ "${enabled}" = "0" ] && [ -n "${con_end}" ] && [ -n "${con_end_expiry}" ] && [ "${con_end_expiry}" != "0" ]; then
-		d1="$(date -d "${con_end}" "+%s")"
-		d2="$(date "+%s")"
-		d3="$(((d2 - d1) / 60))"
-		if [ "${d3}" -ge "${con_end_expiry}" ]; then
-			enabled="1"
-			f_ctrack "end_expiry"
-		fi
-	elif [ "${enabled}" = "1" ] && [ -n "${con_start}" ] && [ -n "${con_start_expiry}" ] && [ "${con_start_expiry}" != "0" ]; then
-		d1="$(date -d "${con_start}" "+%s")"
-		d2="$(date "+%s")"
-		d3="$((d1 + (con_start_expiry * 60)))"
-		if [ "${d2}" -gt "${d3}" ]; then
-			enabled="0"
-			f_ctrack "start_expiry"
-		fi
-	fi
-
+	# handle wifi-iface sections in 'sta' mode, apply uplink-enabled flag from travelmate config, and build active sta list for status reporting
+	#
 	if [ "${mode}" = "sta" ]; then
+
+		# disable iface when uplink is off, or when currently active but not in proactive-connected state
+		#
 		if [ "${enabled}" = "0" ] || { { [ -z "${disabled}" ] || [ "${disabled}" = "0" ]; } &&
 			{ [ "${proactive}" = "0" ] || [ "${trm_ifstatus}" != "true" ]; }; }; then
 			uci_set "wireless" "${section}" "disabled" "1"
+
+		# proactive mode while connected: keep first active sta, disable any further matches
+		#
 		elif [ "${enabled}" = "1" ] && [ "${disabled}" = "0" ] && [ "${trm_ifstatus}" = "true" ] && [ "${proactive}" = "1" ]; then
 			if [ -z "${trm_activesta}" ]; then
 				trm_activesta="${section}"
@@ -567,43 +613,73 @@ f_setif() {
 				uci_set "wireless" "${section}" "disabled" "1"
 			fi
 		fi
+
+		# track all enabled stations for the connection loop
+		#
 		if [ "${enabled}" = "1" ]; then
 			trm_stalist="$(f_trim "${trm_stalist} ${section}-${radio}")"
 		fi
 	fi
+
 	f_log "debug" "f_setif     ::: uplink_config: ${trm_uplinkcfg:-"-"}, section: ${section}, enabled: ${enabled}, active_sta: ${trm_activesta:-"-"}"
 }
 
-# check router/uplink subnet
+# subnet helper function
 #
 f_subnet() {
-	local lan lan_net wan wan_net
+	local lan wan wan_net conn_state="${trm_connection%%/*}"
 
+	# skip when connection state hasn't changed and subnet is already set
+	#
+	if [ "${conn_state}" = "${trm_subnet_last}" ] && [ -n "${trm_subnet}" ]; then
+		return
+	fi
+
+	# resolve uplink (wan) subnet via netifd, then ipcalc to network/cidr
+	#
 	network_flush_cache
 	network_get_subnet wan "${trm_iface:-"trm_wwan"}"
 	[ -n "${wan}" ] && wan_net="$("${trm_ipcalccmd}" "${wan}" | "${trm_awkcmd}" 'BEGIN{FS="="}/NETWORK/{printf "%s",$2}')"
-	network_get_subnet lan "${trm_laniface:-"lan"}"
-	[ -n "${lan}" ] && lan_net="$("${trm_ipcalccmd}" "${lan}" | "${trm_awkcmd}" 'BEGIN{FS="="}/NETWORK/{printf "%s",$2}')"
-	if [ -n "${lan_net}" ] && [ -n "${wan_net}" ] && [ "${lan_net}" = "${wan_net}" ]; then
+
+	# lazy-cache lan subnet (assumed stable for the lifetime of the daemon)
+	#
+	if [ -z "${trm_lannet}" ]; then
+		network_get_subnet lan "${trm_laniface:-"lan"}"
+		[ -n "${lan}" ] && trm_lannet="$("${trm_ipcalccmd}" "${lan}" | "${trm_awkcmd}" 'BEGIN{FS="="}/NETWORK/{printf "%s",$2}')"
+	fi
+
+	# warn on lan/wan subnet collision
+	#
+	if [ -n "${trm_lannet}" ] && [ -n "${wan_net}" ] && [ "${trm_lannet}" = "${wan_net}" ]; then
 		f_log "info" "uplink network '${wan_net}' conflicts with router LAN network, please adjust your network settings"
 	fi
-	printf "%s" "${wan_net:-"-"} (lan: ${lan_net:-"-"})"
-	f_log "debug" "f_subnet    ::: lan_net: ${lan_net:-"-"}, wan_net: ${wan_net:-"-"}"
+
+	# compose result and remember last state for cache
+	#
+	trm_subnet="${wan_net:-"-"} (lan: ${trm_lannet:-"-"})"
+	trm_subnet_last="${conn_state}"
+
+	f_log "debug" "f_subnet    ::: lan: ${trm_lannet:-"-"}, wan: ${wan_net:-"-"}"
 }
 
 # add open uplinks
 #
 f_addsta() {
-	local pattern wifi_cfg trm_cfg new_uplink="1" offset="1" radio="${1}" essid="${2}"
+	local cnt pattern wifi_cfg trm_cfg new_uplink="1" offset="1" radio="${1}" essid="${2}"
 
+	# ssid filter: skip if essid matches any pattern in trm_ssidfilter
+	#
 	for pattern in ${trm_ssidfilter}; do
 		case "${essid}" in
-			${pattern})
-				f_log "info" "open uplink filtered out '${radio}/${essid}/${pattern}'"
-				return 0
-				;;
+		${pattern})
+			f_log "info" "open uplink filtered out '${radio}/${essid}/${pattern}'"
+			return 0
+			;;
 		esac
 	done
+
+	# within quota, scan existing wifi-iface sections for duplicates and count offset
+	#
 	if [ "${trm_maxautoadd}" = "0" ] || [ "${trm_autoaddcnt:-0}" -lt "${trm_maxautoadd}" ]; then
 		config_cb() {
 			local type="${1}" name="${2}"
@@ -622,12 +698,17 @@ f_addsta() {
 		new_uplink="0"
 	fi
 
+	# pick first free 'trm_uplinkN' section name
+	#
 	if [ "${new_uplink}" = "1" ]; then
 		wifi_cfg="trm_uplink$((offset + 1))"
 		while [ -n "$(uci_get "wireless.${wifi_cfg}")" ]; do
 			offset="$((offset + 1))"
 			wifi_cfg="trm_uplink${offset}"
 		done
+
+		# create new wifi-iface section (sta, open, initially disabled)
+		#
 		uci -q batch <<-EOC
 			set wireless."${wifi_cfg}"="wifi-iface"
 			set wireless."${wifi_cfg}".mode="sta"
@@ -637,15 +718,19 @@ f_addsta() {
 			set wireless."${wifi_cfg}".encryption="none"
 			set wireless."${wifi_cfg}".disabled="1"
 		EOC
+
+		# create matching travelmate uplink section
+		#
 		trm_cfg="$(uci -q add travelmate uplink)"
 		uci -q batch <<-EOC
 			set travelmate."${trm_cfg}".device="${radio}"
 			set travelmate."${trm_cfg}".ssid="${essid}"
 			set travelmate."${trm_cfg}".opensta="1"
-			set travelmate."${trm_cfg}".con_start_expiry="0"
-			set travelmate."${trm_cfg}".con_end_expiry="0"
 			set travelmate."${trm_cfg}".enabled="1"
 		EOC
+
+		# inherit default vpn settings if globally configured
+		#
 		if [ -n "${trm_stdvpnservice}" ] && [ -n "${trm_stdvpniface}" ]; then
 			uci -q batch <<-EOC
 				set travelmate."${trm_cfg}".vpnservice="${trm_stdvpnservice}"
@@ -654,6 +739,8 @@ f_addsta() {
 			EOC
 		fi
 
+		# bump autoadd counter, commit, reload wifi, signal UI reload
+		#
 		cnt="$(uci_get "travelmate" "global" "trm_autoaddcnt" "0")"
 		cnt="$((cnt + 1))"
 		uci_set "travelmate" "global" "trm_autoaddcnt" "${cnt}"
@@ -667,24 +754,45 @@ f_addsta() {
 		f_log "info" "open uplink '${radio}/${essid}' added to wireless config"
 		printf "%s" "${wifi_cfg}-${radio}"
 	fi
+
 	f_log "debug" "f_addsta    ::: radio: ${radio:-"-"}, essid: ${essid}, autoaddcnt/maxautoadd: ${cnt:-"${trm_autoaddcnt}"}/${trm_maxautoadd:-"-"}, new_uplink: ${new_uplink}, offset: ${offset}"
 }
 
 # check net status
 #
 f_net() {
-	local err_msg raw json_raw html_raw html_cp js_cp json_ec json_rc json_cp json_ed result="net nok"
+	local parse err_msg raw json_raw html_raw html_cp js_cp json_ec json_rc json_cp json_cp_url json_ed result="net nok"
 
+	# fetch captive-detection url, curl appends '%{json}' metadata after the response body
+	#
 	raw="$("${trm_fetchcmd}" ${trm_fetchparm} --user-agent "${trm_useragent}" --header "Cache-Control: no-cache, no-store, must-revalidate, max-age=0" --write-out "%{json}" "${trm_captiveurl}")"
 	json_raw="${raw#*\{}"
 	html_raw="${raw%%\{*}"
+
+	# parse curl metadata: exit code, http response code, final redirect target
+	#
 	if [ -n "${json_raw}" ]; then
-		json_ec="$(printf "%s" "{${json_raw}" | "${trm_jsoncmd}" -ql1 -e '@.exitcode')"
-		json_rc="$(printf "%s" "{${json_raw}" | "${trm_jsoncmd}" -ql1 -e '@.response_code')"
-		json_cp="$(printf "%s" "{${json_raw}" | "${trm_jsoncmd}" -ql1 -e '@.redirect_url' | "${trm_awkcmd}" 'BEGIN{FS="/"}{printf "%s",tolower($3)}')"
+		parse="$(printf "%s" "{${json_raw}" | "${trm_jsoncmd}" -e '@.exitcode' -e '@.response_code' -e '@.redirect_url')"
+		{
+			IFS= read -r json_ec
+			IFS= read -r json_rc
+			IFS= read -r json_cp_url
+		} <<-EOF
+			${parse}
+		EOF
+
+		# extract lowercased host portion of the redirect url
+		#
+		json_cp="$(printf "%s" "${json_cp_url}" | "${trm_awkcmd}" 'BEGIN{FS="/"}{printf "%s",tolower($3)}')"
 		if [ "${json_ec}" = "0" ]; then
+
+			# http redirect present: captive portal at redirect host
+			#
 			if [ -n "${json_cp}" ]; then
 				result="net cp '${json_cp}'"
+
+			# no http redirect: scan body for meta-refresh / js location.href redirects
+			#
 			else
 				if [ "${json_rc}" = "200" ] || [ "${json_rc}" = "204" ]; then
 					html_cp="$(printf "%s" "${html_raw}" | "${trm_awkcmd}" 'match(tolower($0),/^.*/dev/null 2>&1
 		"${trm_ubuscmd}" -S call network.interface."${trm_iface}" up >/dev/null 2>&1
@@ -743,11 +866,16 @@ f_check() {
 		sleep 1
 	fi
 
+	# polling loop, bounded by trm_maxwait seconds
+	#
 	while [ "${wait_time}" -le "${trm_maxwait}" ]; do
 		[ "${wait_time}" -gt "0" ] && sleep 1
 		wait_time="$((wait_time + 1))"
 		dev_status="$("${trm_ubuscmd}" -S call network.wireless status 2>/dev/null)"
 		if [ -n "${dev_status}" ]; then
+
+			# dev mode: persist status change and exit
+			#
 			if [ "${mode}" = "dev" ]; then
 				if [ "${trm_ifstatus}" != "${status}" ]; then
 					trm_ifstatus="${status}"
@@ -757,21 +885,30 @@ f_check() {
 					sleep "$((trm_maxwait / 6))"
 				fi
 				break
+
+			# rev mode: drop connection state and exit
+			#
 			elif [ "${mode}" = "rev" ]; then
 				trm_connection=""
 				trm_ifstatus="${status}"
 				break
+
+			# initial/sta mode: query active sta interface
+			#
 			else
 				ifname="$(printf "%s" "${dev_status}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].ifname')"
 				if [ -n "${ifname}" ] && [ "${enabled}" = "1" ]; then
 					raw="$("${trm_ubuscmd}" -S call iwinfo info "{\"device\":\"${ifname}\"}" 2>/dev/null | "${trm_jsoncmd}" -ql1 -e '@.signal')"
 					if [ -n "${raw}" ] && [ "${raw}" -ge "-120" ]; then
-						trm_ifquality="$(( 2 * (raw + 100) ))"
-						[ "${trm_ifquality}" -gt "100" ] && trm_ifquality="100"
-						[ "${trm_ifquality}" -lt "0" ] && trm_ifquality="0"
+						ifquality="$((2 * (raw + 100)))"
+						[ "${ifquality}" -gt "100" ] && ifquality="100"
+						[ "${ifquality}" -lt "0" ] && ifquality="0"
 					fi
-					if [ -z "${trm_ifquality}" ]; then
-						trm_ifstatus="$("${trm_ubuscmd}" -S call network.interface dump 2>/dev/null | "${trm_jsoncmd}" -ql1 -e "@.interface[@.device=\"${ifname}\"].up")"
+
+					# no signal: detect connection drop or overall wait timeout
+					#
+					if [ -z "${ifquality}" ]; then
+						trm_ifstatus="$("${trm_ifstatuscmd}" "${trm_iface}" | "${trm_jsoncmd}" -ql1 -e '@.up')"
 						if { [ -n "${trm_connection}" ] && [ "${trm_ifstatus}" = "false" ]; } || [ "${wait_time}" -eq "${trm_maxwait}" ]; then
 							if [ -n "${trm_connection}" ] && [ "${trm_ifstatus}" = "false" ]; then
 								f_log "info" "no signal from uplink"
@@ -781,31 +918,37 @@ f_check() {
 							f_vpn "disable"
 							trm_connection=""
 							trm_ifstatus="${status}"
-							f_ctrack "end"
 							f_genstatus
 							break
 						fi
 						continue
-					elif [ "${trm_ifquality}" -ge "${trm_minquality}" ]; then
-						trm_ifstatus="$("${trm_ubuscmd}" -S call network.interface dump 2>/dev/null | "${trm_jsoncmd}" -ql1 -e "@.interface[@.device=\"${ifname}\"].up")"
+
+					# acceptable signal: verify ifup state and run net/captive checks
+					#
+					elif [ "${ifquality}" -ge "${trm_minquality}" ]; then
+						trm_ifstatus="$("${trm_ifstatuscmd}" "${trm_iface}" | "${trm_jsoncmd}" -ql1 -e '@.up')"
 						if [ "${trm_ifstatus}" = "true" ]; then
 							result="$(f_net)"
+
+							# captive portal: allow cp domain in dnsmasq, then optionally run login script
+							#
 							if [ "${trm_captive}" = "1" ]; then
 								while :; do
 									cp_domain="$(printf "%s" "${result}" | "${trm_awkcmd}" -F '['\''| ]' '/^net cp/{printf "%s",$4}')"
-									if [ -x "/etc/init.d/dnsmasq" ] && [ -f "/etc/config/dhcp" ] &&
-										[ -n "${cp_domain}" ] && ! uci_get "dhcp" "@dnsmasq[0]" "rebind_domain" | "${trm_grepcmd}" -q "${cp_domain}"; then
-										uci_add_list "dhcp" "@dnsmasq[0]" "rebind_domain" "${cp_domain}"
-										[ -n "$(uci -q changes "dhcp")" ] && uci_commit "dhcp"
-										/etc/init.d/dnsmasq reload
-										f_log "info" "captive portal domain '${cp_domain}' added to to dhcp rebind whitelist"
-									else
+									if [ ! -x "/etc/init.d/dnsmasq" ] || [ ! -f "/etc/config/dhcp" ] || [ -z "${cp_domain}" ]; then
 										break
 									fi
+									case " $(uci_get "dhcp" "@dnsmasq[0]" "rebind_domain") " in
+									*" ${cp_domain} "*) break ;;
+									esac
+									uci_add_list "dhcp" "@dnsmasq[0]" "rebind_domain" "${cp_domain}"
+									[ -n "$(uci -q changes "dhcp")" ] && uci_commit "dhcp"
+									/etc/init.d/dnsmasq reload
+									f_log "info" "captive portal domain '${cp_domain}' added to to dhcp rebind whitelist"
 									result="$(f_net)"
 								done
 								if [ -n "${cp_domain}" ]; then
-									trm_connection="${result:-"-"}/${trm_ifquality}"
+									trm_connection="${result:-"-"}/${ifquality}"
 									f_genstatus
 									login_script="$(f_getval "script")"
 									if [ -x "${login_script}" ]; then
@@ -819,6 +962,9 @@ f_check() {
 									fi
 								fi
 							fi
+
+							# no internet: tear down vpn, exit early if netcheck enabled
+							#
 							if [ "${result}" = "net nok" ]; then
 								f_vpn "disable"
 								if [ "${trm_netcheck}" = "1" ]; then
@@ -828,30 +974,45 @@ f_check() {
 									break
 								fi
 							fi
-							trm_connection="${result:-"-"}/${trm_ifquality}"
+
+							# success: persist connection state and exit
+							#
+							trm_connection="${result:-"-"}/${ifquality}"
 							f_genstatus
 							break
 						fi
+
+					# signal below minquality on existing link: drop and exit
+					#
 					elif [ -n "${trm_connection}" ] && { [ "${trm_netcheck}" = "1" ] || [ "${mode}" = "initial" ]; }; then
-						f_log "info" "uplink is out of range (${trm_ifquality}/${trm_minquality})"
+						f_log "info" "uplink is out of range (${ifquality}/${trm_minquality})"
 						f_vpn "disable"
 						trm_connection=""
 						trm_ifstatus="${status}"
-						f_ctrack "end"
 						f_genstatus
 						break
+
+					# signal below minquality on initial/sta probe: bail out
+					#
 					elif [ "${mode}" = "initial" ] || [ "${mode}" = "sta" ]; then
 						trm_connection=""
 						trm_ifstatus="${status}"
 						f_genstatus
 						break
 					fi
+
+				# sta interface vanished while connected
+				#
 				elif [ -n "${trm_connection}" ]; then
+					f_log "info" "uplink connection lost (interface gone)"
 					f_vpn "disable"
 					trm_connection=""
 					trm_ifstatus="${status}"
 					f_genstatus
 					break
+
+				# initial probe, no sta interface present yet
+				#
 				elif [ "${mode}" = "initial" ]; then
 					trm_ifstatus="${status}"
 					f_genstatus
@@ -859,13 +1020,22 @@ f_check() {
 				fi
 			fi
 		fi
+
+		# initial mode safety net: empty wireless status -> exit loop
+		#
 		if [ "${mode}" = "initial" ]; then
+			if [ -n "${trm_connection}" ]; then
+				f_log "info" "uplink connection lost (interface down)"
+				f_vpn "disable"
+				trm_connection=""
+			fi
 			trm_ifstatus="${status}"
 			f_genstatus
 			break
 		fi
 	done
-	f_log "debug" "f_check     ::: mode: ${mode}, name: ${ifname:-"-"}, status: ${trm_ifstatus}, enabled: ${enabled}, connection: ${trm_connection:-"-"}, wait: ${wait_time}, max_wait: ${trm_maxwait}, min_quality/quality: ${trm_minquality}/${trm_ifquality:-"-"}, captive: ${trm_captive}, netcheck: ${trm_netcheck}"
+
+	f_log "debug" "f_check     ::: mode: ${mode}, name: ${ifname:-"-"}, status: ${trm_ifstatus}, enabled: ${enabled}, connection: ${trm_connection:-"-"}, wait: ${wait_time}, max_wait: ${trm_maxwait}, min_quality/quality: ${trm_minquality}/${ifquality:-"-"}, captive: ${trm_captive}, netcheck: ${trm_netcheck}"
 }
 
 # get status information
@@ -873,7 +1043,7 @@ f_check() {
 f_getstatus() {
 	local key keylist value rtfile
 
-	rtfile="$(uci_get travelmate global trm_rtfile "/tmp/trm_runtime.json")"
+	rtfile="$(uci_get travelmate global trm_rtfile "${trm_rundir}/travelmate.runtime.json")"
 	json_load_file "${rtfile}" >/dev/null 2>&1
 	if json_select data >/dev/null 2>&1; then
 		printf "%s\n" "::: travelmate runtime information"
@@ -890,24 +1060,37 @@ f_getstatus() {
 # generate status information
 #
 f_genstatus() {
-	local vpn vpn_iface section last_date sta_iface sta_radio sta_essid sta_bssid sta_mac dev_status status="${trm_ifstatus}" ntp_done="0" vpn_done="0" mail_done="0"
+	local parse s_captive s_proactive s_netcheck s_autoadd s_randomize s_eviltwin s_ntp s_vpn s_mail vpn vpn_iface
+	local section last_date sta_iface sta_radio sta_essid sta_bssid sta_mac dev_status status="${trm_ifstatus}" ntp_done="0" vpn_done="0" mail_done="0"
 
+	# get current connection information
+	#
 	if [ "${status}" = "true" ]; then
 		status="connected, ${trm_connection:-"-"}"
 		dev_status="$("${trm_ubuscmd}" -S call network.wireless status 2>/dev/null)"
-		section="$(printf "%s" "${dev_status}" | "${trm_jsoncmd}" -ql1 -e '@.*.interfaces[@.config.mode="sta"].section')"
+		parse="$(printf "%s" "${dev_status}" | "${trm_jsoncmd}" \
+			-e '@.*.interfaces[@.config.mode="sta"].section' \
+			-e '@.*.interfaces[@.config.mode="sta"].config.ssid' \
+			-e '@.*.interfaces[@.config.mode="sta"].config.macaddr' \
+			-e '@.*.interfaces[@.config.mode="sta"].config.network[0]' \
+			-e '@.*.interfaces[@.config.mode="sta"].config.bssid')"
+		{
+			IFS= read -r section
+			IFS= read -r sta_essid
+			IFS= read -r sta_mac
+			IFS= read -r sta_iface
+			IFS= read -r sta_bssid
+		} <<-EOF
+			${parse}
+		EOF
 		if [ -n "${section}" ]; then
-			sta_iface="$(uci_get "wireless" "${section}" "network")"
 			sta_radio="$(uci_get "wireless" "${section}" "device")"
-			sta_essid="$(uci_get "wireless" "${section}" "ssid")"
-			sta_bssid="$(uci_get "wireless" "${section}" "bssid")"
-			sta_mac="$(f_mac "get" "${section}")"
 			f_getcfg "${sta_radio}" "${sta_essid}" "${sta_bssid}"
 		fi
 		json_get_var last_date "last_run"
 
 		vpn="$(f_getval "vpn")"
-		if  [ "${trm_vpn}" = "1" ] && [ -n "${trm_vpninfolist}" ] && [ "${vpn}" = "1" ] && [ -f "${trm_vpnfile}" ]; then
+		if [ "${trm_vpn}" = "1" ] && [ -n "${trm_vpninfolist}" ] && [ "${vpn}" = "1" ] && [ -f "${trm_vpnfile}" ]; then
 			vpn_iface="$(f_getval "vpniface")"
 			vpn_done="1"
 		fi
@@ -916,30 +1099,54 @@ f_genstatus() {
 		status="program error"
 	else
 		trm_connection=""
-		status="running, not connected"
+		status="processing"
 	fi
+
+	# fallback for missing last_run value
+	#
 	if [ -z "${last_date}" ]; then
 		last_date="$(date "+%Y.%m.%d-%H:%M:%S")"
 	fi
+
+	# check for presence of ntp lock file and mail notification conditions
+	#
 	if [ -d "${trm_ntplock}" ]; then
 		ntp_done="1"
 	fi
 	if [ "${trm_mail}" = "1" ] && [ -f "${trm_mailfile}" ]; then
 		mail_done="1"
 	fi
+
+	# convert flags to symbols
+	#
+	case "${trm_captive}" in "1") s_captive="✔" ;; *) s_captive="✘" ;; esac
+	case "${trm_proactive}" in "1") s_proactive="✔" ;; *) s_proactive="✘" ;; esac
+	case "${trm_netcheck}" in "1") s_netcheck="✔" ;; *) s_netcheck="✘" ;; esac
+	case "${trm_autoadd}" in "1") s_autoadd="✔" ;; *) s_autoadd="✘" ;; esac
+	case "${trm_randomize}" in "1") s_randomize="✔" ;; *) s_randomize="✘" ;; esac
+	case "${trm_eviltwin}" in "1") s_eviltwin="✔" ;; *) s_eviltwin="✘" ;; esac
+	case "${ntp_done}" in "1") s_ntp="✔" ;; *) s_ntp="✘" ;; esac
+	case "${vpn_done}" in "1") s_vpn="✔" ;; *) s_vpn="✘" ;; esac
+	case "${mail_done}" in "1") s_mail="✔" ;; *) s_mail="✘" ;; esac
+
+	# generate runtime status file
+	#
+	f_subnet
 	json_add_string "travelmate_status" "${status}"
 	json_add_string "frontend_ver" "${trm_fver}"
 	json_add_string "backend_ver" "${trm_bver}"
 	json_add_string "station_id" "${sta_radio:-"-"}/${sta_essid:-"-"}/${sta_bssid:-"-"}"
 	json_add_string "station_mac" "${sta_mac:-"-"}"
 	json_add_string "station_interfaces" "${sta_iface:-"-"}, ${vpn_iface:-"-"}"
-	json_add_string "station_subnet" "$(f_subnet)"
-	json_add_string "run_flags" "captive: $(f_char ${trm_captive}), proactive: $(f_char ${trm_proactive}), netcheck: $(f_char ${trm_netcheck}), autoadd: $(f_char ${trm_autoadd}), randomize: $(f_char ${trm_randomize})"
-	json_add_string "ext_hooks" "ntp: $(f_char ${ntp_done}), vpn: $(f_char ${vpn_done}), mail: $(f_char ${mail_done})"
+	json_add_string "station_subnet" "${trm_subnet:-"-"}"
+	json_add_string "run_flags" "captive: ${s_captive}, proactive: ${s_proactive}, netcheck: ${s_netcheck}, autoadd: ${s_autoadd}, randomize: ${s_randomize}, eviltwin: ${s_eviltwin}"
+	json_add_string "ext_hooks" "ntp: ${s_ntp}, vpn: ${s_vpn}, mail: ${s_mail}"
 	json_add_string "last_run" "${last_date}"
 	json_add_string "system" "${trm_sysver}"
 	json_dump >"${trm_rtfile}"
 
+	# send mail notification if enabled and conditions are met
+	#
 	if [ "${status%%, net ok/*}" = "connected" ] && [ "${trm_mail}" = "1" ] &&
 		[ -x "${trm_mailcmd}" ] && [ -n "${trm_mailreceiver}" ] && [ "${ntp_done}" = "1" ] && [ "${mail_done}" = "0" ]; then
 		if [ "${trm_vpn}" != "1" ] || [ "${vpn}" != "1" ] || [ -z "${trm_vpninfolist}" ] || [ "${vpn_done}" = "1" ]; then
@@ -984,9 +1191,9 @@ f_log() {
 		if [ -x "${trm_logcmd}" ]; then
 			"${trm_logcmd}" -p "${class}" -t "trm-${trm_bver}[${$}]" "${log_msg::512}"
 		else
-			printf "%s %s %s\n" "${class}" "trm-${trm_bver}[${$}]" "${log_msg::512}"
+			printf "%s %s %s\n" "${class}" "trm-${trm_bver}[${$}]" "${log_msg::512}" >&2
 		fi
-		if [ "${class}" = "err" ]; then
+		if [ "${class}" = "err" ] || [ "${class}" = "emerg" ]; then
 			trm_ifstatus="error"
 			f_genstatus
 			: >"${trm_pidfile}"
@@ -998,15 +1205,19 @@ f_log() {
 # wifi scan function
 #
 f_scan() {
-	local result key keylist ssid bssid quality wpa_arr cipher_arr auth_arr radio="${1}" mode="${2}"
+	local signal channel wpa_versions cipher auth result key keylist ssid bssid quality wpa_arr cipher_arr auth_arr radio="${1}" mode="${2}"
 
+	# return early on empty or failed scan result
+	#
 	result="$("${trm_ubuscmd}" -S call iwinfo scan "{\"device\":\"${radio}\"}" 2>/dev/null)"
 	[ -z "${result}" ] && return 0
 
+	# load and iterate over scan results and print relevant information
+	#
 	json_load "${result}" || return 0
 	json_select results 2>/dev/null || return 0
-
 	json_get_keys keylist
+
 	for key in ${keylist}; do
 		json_select "${key}" 2>/dev/null || continue
 		json_get_var bssid bssid
@@ -1014,6 +1225,8 @@ f_scan() {
 		json_get_var signal signal
 		json_get_var channel channel
 
+		# clean up ssid from control characters and trim whitespace, then quote it (empty ssids are marked as 'hidden')
+		#
 		ssid="$(printf "%s" "${ssid}" | "${trm_awkcmd}" '{
 			gsub(/[[:cntrl:]]/, "");
 			sub(/^[ \t]+/, "");
@@ -1027,12 +1240,18 @@ f_scan() {
 			ssid="\"${ssid}\""
 		fi
 
+		# format bssid to uppercase and without colons
+		#
 		bssid="$(printf "%s" "${bssid}" | "${trm_awkcmd}" '{print toupper($0)}')"
 
-		quality="$(( 2 * (signal + 100) ))"
+		# convert signal strength to quality percentage (assuming -100dBm = 0% and -50dBm = 100%)
+		#
+		quality="$((2 * (signal + 100)))"
 		[ "${quality}" -gt "100" ] && quality="100"
 		[ "${quality}" -lt "0" ] && quality="0"
 
+		# extract encryption information and convert to human-readable format (wpa versions, ciphers, authentication)
+		#
 		json_select encryption 2>/dev/null
 		json_get_values wpa_arr wpa 2>/dev/null
 		json_get_values cipher_arr ciphers 2>/dev/null
@@ -1071,14 +1290,16 @@ f_scan() {
 			}
 		')"
 
+		# print results in desired format (full or default), filling missing values with placeholders
+		#
 		case "${mode}" in
-			full)
-				printf "%s %s %s %s %s %s %s\n" \
-					"${quality:-"0"}" "${channel:-"0"}" "${bssid:-"-"}" "${wpa_versions:-"-"}" "${cipher:-"-"}" "${auth:-"-"}" "${ssid}"
+		full)
+			printf "%s %s %s %s %s %s %s\n" \
+				"${quality:-"0"}" "${channel:-"0"}" "${bssid:-"-"}" "${wpa_versions:-"-"}" "${cipher:-"-"}" "${auth:-"-"}" "${ssid}"
 			;;
-			*)
-				printf "%s %s %s %s %s\n" \
-					"${quality:-"0"}" "${wpa_versions:-"-"}" "-" "${bssid:-"-"}" "${ssid}"
+		*)
+			printf "%s %s %s %s %s\n" \
+				"${quality:-"0"}" "${wpa_versions:-"-"}" "-" "${bssid:-"-"}" "${ssid}"
 			;;
 		esac
 		json_select .. 2>/dev/null
@@ -1088,7 +1309,7 @@ f_scan() {
 # main function for connection handling
 #
 f_main() {
-	local radio cnt retrycnt scan_list scan_essid scan_bssid scan_rsn scan_wpa scan_quality scan_open station_id
+	local radio cnt retrycnt scan_list scan_essid scan_bssid scan_rsn scan_wpa scan_quality scan_open station_id retry_display
 	local section sta sta_essid sta_bssid sta_radio sta_mac open_sta open_essid config_radio config_essid config_bssid
 
 	# initial check
@@ -1101,7 +1322,7 @@ f_main() {
 			f_vpn "disable"
 		fi
 	fi
- 	f_log "debug" "f_main-1    ::: status: ${trm_ifstatus}, connection: ${trm_connection%%/*}, proactive: ${trm_proactive}"
+	f_log "debug" "f_main-1    ::: status: ${trm_ifstatus}, connection: ${trm_connection%%/*}, proactive: ${trm_proactive}"
 
 	# proactive connection handling
 	#
@@ -1126,15 +1347,18 @@ f_main() {
 		# radio loop
 		#
 		for radio in ${trm_radiolist}; do
-			if ! printf "%s" "${trm_stalist}" | "${trm_grepcmd}" -q "\\-${radio}"; then
+			case " ${trm_stalist} " in
+			*"-${radio} "*) ;;
+			*)
 				if [ "${trm_autoadd}" = "0" ]; then
 					continue
 				fi
-			fi
-			scan_list=""
+				;;
+			esac
 
 			# station loop
 			#
+			scan_list=""
 			for sta in ${trm_stalist:-"${radio}"}; do
 				if [ "${sta}" != "${radio}" ]; then
 					section="${sta%%-*}"
@@ -1146,9 +1370,9 @@ f_main() {
 						f_log "info" "invalid wireless section '${section}'"
 						continue
 					fi
+					f_getcfg "${sta_radio}" "${sta_essid}" "${sta_bssid}"
 					if [ -n "${trm_connection}" ] && [ "${radio}" = "${config_radio}" ] && [ "${sta_radio}" = "${config_radio}" ] &&
 						[ "${sta_essid}" = "${config_essid}" ] && [ "${sta_bssid}" = "${config_bssid}" ]; then
-						f_ctrack "refresh"
 						f_vpn "enable_keep"
 						f_log "debug" "f_main-4    ::: config_radio: ${config_radio}, config_essid: ${config_essid}, config_bssid: ${config_bssid:-"-"}"
 						return 0
@@ -1177,6 +1401,10 @@ f_main() {
 							continue 2
 						elif [ "${scan_quality}" -ge "${trm_minquality}" ]; then
 							if [ "${trm_autoadd}" = "1" ] && [ "${scan_open}" = "+" ] && [ "${scan_essid}" != "hidden" ]; then
+								if [ "${trm_eviltwin}" = "1" ] && [ "$((0x${scan_bssid%%:*} & 2))" != "0" ]; then
+									f_log "info" "skipped autoadd of LAA candidate (evil-twin) '${radio}/${scan_essid}/${scan_bssid}'"
+									continue
+								fi
 								open_essid="${scan_essid%?}"
 								open_essid="${open_essid:1}"
 								open_sta="$(f_addsta "${radio}" "${open_essid}")"
@@ -1188,14 +1416,29 @@ f_main() {
 									sta_mac=""
 								fi
 							fi
+							if [ -n "${sta_bssid}" ] && [ "${radio}" = "${sta_radio}" ] &&
+								[ "${scan_bssid}" != "${sta_bssid}" ] && [ "${scan_essid}" = "\"${sta_essid}\"" ]; then
+								if [ -n "${trm_uplinkcfg}" ]; then
+									uci_set "travelmate" "${trm_uplinkcfg}" "enabled" "0"
+									uci_commit "travelmate"
+									[ ! -f "${trm_refreshfile}" ] && printf "%s" "cfg_reload" >"${trm_refreshfile}"
+								fi
+								f_log "info" "bssid mismatch (evil-twin) '${sta_radio}/${sta_essid}/${sta_bssid} => ${scan_bssid}'"
+								continue
+							fi
 							if { { [ "${scan_essid}" = "\"${sta_essid}\"" ] && { [ -z "${sta_bssid}" ] || [ "${scan_bssid}" = "${sta_bssid}" ]; }; } ||
 								{ [ "${scan_bssid}" = "${sta_bssid}" ] && [ "${scan_essid}" = "hidden" ]; }; } && [ "${radio}" = "${sta_radio}" ]; then
+								if [ "${trm_eviltwin}" = "1" ] && [ -z "${sta_bssid}" ] && [ "${scan_essid}" != "hidden" ]; then
+									if [ "$((0x${scan_bssid%%:*} & 2))" != "0" ]; then
+										f_log "info" "skipped LAA candidate (evil-twin) '${sta_radio}/${sta_essid}/${sta_bssid:-"-"} => ${scan_bssid}'"
+										continue
+									fi
+								fi
 								if [ -n "${config_radio}" ]; then
 									f_vpn "disable"
 									uci_set "wireless" "${trm_activesta}" "disabled" "1"
 									[ -n "$(uci -q changes "wireless")" ] && uci_commit "wireless"
 									f_check "rev" "false"
-									f_ctrack "end"
 									f_log "info" "uplink connection terminated '${config_radio}/${config_essid}/${config_bssid:-"-"}'"
 									unset config_radio config_essid config_bssid
 								fi
@@ -1204,27 +1447,31 @@ f_main() {
 								#
 								retrycnt="1"
 								f_getcfg "${sta_radio}" "${sta_essid}" "${sta_bssid}"
-								while [ "${retrycnt}" -le "${trm_maxretry}" ]; do
+								[ "${trm_maxretry}" = "0" ] && retry_display="-" || retry_display="${trm_maxretry}"
+								while [ "${trm_maxretry}" = "0" ] || [ "${retrycnt}" -le "${trm_maxretry}" ]; do
 									sta_mac="$(f_mac "set" "${section}")"
 									uci_set "wireless" "${section}" "disabled" "0"
 									f_check "sta" "false" "${sta_radio}" "${sta_essid}" "${sta_bssid}"
 									if [ "${trm_ifstatus}" = "true" ]; then
 										rm -f "${trm_mailfile}"
 										[ -n "$(uci -q changes "wireless")" ] && uci_commit "wireless"
-										f_ctrack "start"
-										f_log "info" "connected to uplink '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' with mac '${sta_mac:-"-"}' (${retrycnt}/${trm_maxretry})"
+										f_log "info" "connected to uplink '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' with mac '${sta_mac:-"-"}' (${retrycnt}/${retry_display})"
 										f_vpn "enable"
 										return 0
 									else
 										uci -q revert "wireless"
 										f_check "rev" "false"
-										if [ "${retrycnt}" = "${trm_maxretry}" ]; then
-											f_ctrack "disabled"
-											f_log "info" "uplink has been disabled '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' (${retrycnt}/${trm_maxretry})"
+										if [ "${retrycnt}" -eq "${trm_maxretry}" ]; then
+											if [ -n "${trm_uplinkcfg}" ]; then
+												uci_set "travelmate" "${trm_uplinkcfg}" "enabled" "0"
+												uci_commit "travelmate"
+												[ ! -f "${trm_refreshfile}" ] && printf "%s" "cfg_reload" >"${trm_refreshfile}"
+											fi
+											f_log "info" "uplink has been disabled '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' (${retrycnt}/${retry_display})"
 											continue 2
 										else
 											f_genstatus
-											f_log "info" "can't connect to uplink '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' (${retrycnt}/${trm_maxretry})"
+											f_log "info" "can't connect to uplink '${sta_radio}/${sta_essid}/${sta_bssid:-"-"}' (${retrycnt}/${retry_display})"
 										fi
 									fi
 									retrycnt="$((retrycnt + 1))"
@@ -1253,13 +1500,12 @@ fi
 
 # reference required system utilities
 #
+trm_catcmd="$(f_cmd cat)"
 trm_awkcmd="$(f_cmd gawk awk)"
 trm_sortcmd="$(f_cmd sort)"
-trm_grepcmd="$(f_cmd grep)"
 trm_pgrepcmd="$(f_cmd pgrep)"
 trm_killcmd="$(f_cmd kill)"
 trm_jsoncmd="$(f_cmd jsonfilter)"
-trm_lookupcmd="$(f_cmd nslookup)"
 trm_ubuscmd="$(f_cmd ubus)"
 trm_logcmd="$(f_cmd logger)"
 trm_wificmd="$(f_cmd wifi)"
diff --git a/net/travelmate/files/travelmate-service.sh b/net/travelmate/files/travelmate-service.sh
index a7571f6456..c7957fcf85 100755
--- a/net/travelmate/files/travelmate-service.sh
+++ b/net/travelmate/files/travelmate-service.sh
@@ -20,7 +20,7 @@ f_conf
 while :; do
 	if [ "${trm_action}" = "stop" ]; then
 		if [ -s "${trm_pidfile}" ]; then
-			f_log "info" "travelmate instance stopped ::: action: ${trm_action}, pid: $(< ${trm_pidfile})"
+			f_log "info" "travelmate instance stopped ::: action: ${trm_action}, pid: $("${trm_catcmd}" "${trm_pidfile}")"
 			: >"${trm_rtfile}"
 			: >"${trm_pidfile}"
 		fi
diff --git a/net/travelmate/files/travelmate.init b/net/travelmate/files/travelmate.init
index de09d14ccc..1addcca424 100755
--- a/net/travelmate/files/travelmate.init
+++ b/net/travelmate/files/travelmate.init
@@ -61,10 +61,14 @@ status_service() {
 scan() {
 	local result radio="${1:-radio0}"
 
+	case "${radio}" in
+	*[!a-z0-9]*) return 1 ;;
+	esac
+
 	result="$(f_scan "${radio}" full)"
 	if [ -z "${result}" ]; then
-		: > "${trm_tmpfile}"
-		mv -f  "${trm_tmpfile}" "${trm_scanfile}"
+		: >"${trm_tmpfile}"
+		mv -f "${trm_tmpfile}" "${trm_scanfile}"
 		return 0
 	fi
 
@@ -83,20 +87,20 @@ scan() {
 		sub(/[ \t]+$/, "", ssid)
 		printf "%3s %3s %17s %-12s %-10s %-10s %s\n",
 			quality, channel, bssid, rsn, cipher, auth, ssid
-	}' | "${trm_sortcmd}" -rn > "${trm_tmpfile}"
+	}' | "${trm_sortcmd}" -rn >"${trm_tmpfile}"
 
-	mv -f  "${trm_tmpfile}" "${trm_scanfile}"
+	mv -f "${trm_tmpfile}" "${trm_scanfile}"
 }
 
 setup() {
 	local rc cnt net iface input="${1:-"trm_wwan"}" zone="${2:-"wan"}" metric="${3:-"100"}"
 
-	input="${input//[+*~%&\$@\"\' ]/}"
-	zone="${zone//[+*~%&\$@\"\' ]/}"
-	metric="${metric//[^0-9]/}"
+	input="${input//[!a-zA-Z0-9_]/}"
+	zone="${zone//[!a-zA-Z0-9_]/}"
+	metric="${metric//[!0-9]/}"
 	iface="$(uci_get travelmate global trm_iface)"
- 
-	if [ -n "${iface}" ] && [ "${iface}" = "${input}" ]; then
+
+	if [ -z "${input}" ] || { [ -n "${iface}" ] && [ "${iface}" = "${input}" ]; }; then
 		return 1
 	fi
 
@@ -124,19 +128,23 @@ setup() {
 	fi
 
 	cnt="0"
-	while [ -n "$(uci_get firewall @zone[${cnt}] name)" ]; do
-		if [ "$(uci_get firewall @zone[${cnt}] name)" = "${zone}" ]; then
+	zone_name="$(uci_get firewall @zone[${cnt}] name)"
+	while [ -n "${zone_name}" ]; do
+		if [ "${zone_name}" = "${zone}" ]; then
 			net="$(uci_get firewall @zone[${cnt}] network)"
-			if ! printf "%s" "${net}" | grep -qw "${input}"; then
-				uci -q add_list firewall.@zone[${cnt}].network="${input}"
-			fi
-			if ! printf "%s" "${net}" | grep -qw "${input}6"; then
-				uci -q add_list firewall.@zone[${cnt}].network="${input}6"
-			fi
+			case " ${net} " in
+			*" ${input} "*) ;;
+			*) uci -q add_list firewall.@zone[${cnt}].network="${input}" ;;
+			esac
+			case " ${net} " in
+			*" ${input}6 "*) ;;
+			*) uci -q add_list firewall.@zone[${cnt}].network="${input}6" ;;
+			esac
 			[ -n "$(uci -q changes "firewall")" ] && uci_commit firewall
 			break
 		fi
 		cnt="$((cnt + 1))"
+		zone_name="$(uci_get firewall @zone[${cnt}] name)"
 	done
 
 	cnt="0"
diff --git a/net/travelmate/files/travelmate.vpn b/net/travelmate/files/travelmate.vpn
index bfeea17e4b..7b1abc1fa2 100755
--- a/net/travelmate/files/travelmate.vpn
+++ b/net/travelmate/files/travelmate.vpn
@@ -13,6 +13,7 @@ vpn_action="${2}"
 vpn_service="${3}"
 vpn_iface="${4}"
 vpn_instance="${5}"
+vpn_status=""
 trm_funlib="/usr/lib/travelmate-functions.sh"
 if [ -z "${trm_bver}" ]; then
 	. "${trm_funlib}"
@@ -21,7 +22,7 @@ fi
 
 f_net() {
 	local json_rc
-	json_rc="$(${trm_fetchcmd} ${trm_fetchparm} --user-agent "${trm_useragent}" --header "Cache-Control: no-cache, no-store, must-revalidate, max-age=0" --write-out "%{response_code}" --output /dev/null "${trm_captiveurl}")"
+	json_rc="$("${trm_fetchcmd}" ${trm_fetchparm} --user-agent "${trm_useragent}" --header "Cache-Control: no-cache, no-store, must-revalidate, max-age=0" --write-out "%{response_code}" --output /dev/null "${trm_captiveurl}")"
 	if [ "${json_rc}" = "200" ] || [ "${json_rc}" = "204" ]; then
 		json_rc="net ok"
 	fi
@@ -49,7 +50,7 @@ if [ "${vpn}" = "1" ] && [ "${vpn_action%_*}" = "enable" ]; then
 		if ! "${trm_ubuscmd}" -t "$((trm_maxwait / 6))" wait_for network.interface."${vpn_iface}" >/dev/null 2>&1; then
 			f_log "info" "travelmate vpn interface '${vpn_iface}' does not appear on ubus on ifup event"
 		fi
-		cnt=0
+		cnt="0"
 		while :; do
 			vpn_status="$("${trm_ubuscmd}" -S call network.interface."${vpn_iface}" status 2>/dev/null | "${trm_jsoncmd}" -ql1 -e '@.up')"
 			if [ "${vpn_status}" = "true" ]; then
@@ -67,8 +68,8 @@ if [ "${vpn}" = "1" ] && [ "${vpn_action%_*}" = "enable" ]; then
 					/etc/init.d/openvpn stop "${vpn_instance}"
 				fi
 				rm -f "${trm_vpnfile}"
-				f_log "info" "${vpn_service} client connection can't be established '${vpn_iface}/${vpn_instance:-"-", rc: ${net_status:-"-"}}'"
-				return 1
+				f_log "info" "${vpn_service} client connection can't be established '${vpn_iface}/${vpn_instance:-"-"}, rc: ${net_status:-"-"}'"
+				exit 1
 			fi
 			cnt="$((cnt + 1))"
 			sleep 1