travelmate: release 2.4.5-1
authorDirk Brenken <redacted>
Sat, 9 May 2026 19:38:20 +0000 (21:38 +0200)
committerDirk Brenken <redacted>
Sat, 9 May 2026 19:39:29 +0000 (21:39 +0200)
- added opt-in protection against access points with locally-administered (LAA) BSSIDs
- added a special trm_maxretry value '0', enabling unlimited connection retries
- removed obsolete connection-tracking functions (too many uci updates/flash wear)
- all runtime files now live under a single /var/run/travelmate/ directory
- various code cleanups & fixes
- LuCI: made the new UCI option 'trm_eviltwin' available
- LuCI: more cleanups
- readme update

Signed-off-by: Dirk Brenken <redacted>
net/travelmate/Makefile
net/travelmate/files/25-travelmate.hotplug
net/travelmate/files/README.md
net/travelmate/files/travelmate-functions.sh
net/travelmate/files/travelmate-service.sh
net/travelmate/files/travelmate.init
net/travelmate/files/travelmate.vpn

index 17e8cac360acfce0d429057ca05e5bed9e8e30be..4bc8f96251fe4c9d2f91f38e98e70b6c2b4a272d 100644 (file)
@@ -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 <dev@brenken.org>
 
index a61ed3e42aa1847946f0527b0e294c76dee6f342..ff3517577d7f9db48745258a0cc1f50df7eec3f6 100755 (executable)
@@ -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}"
index a934f52189649320c0024478c9554d0e320d073b..b8f7351842a0b10976041c87ef2d3a636e407d0a 100644 (file)
@@ -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:
+
 <pre><code>
 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)
index 22fa440a87ac0b902d431dd721aa1cb3b056a689..e1336d5dba9a4bffa8425da24e4ae2fff5f4272d 100644 (file)
@@ -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,116 +311,76 @@ 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
+       f_log "debug" "f_mac       ::: action: ${action:-"-"}, section: ${section:-"-"}, macaddr: ${macaddr:-"-"}, result: ${result:-"-"}"
 }
 
 # get openvpn information
@@ -388,65 +388,102 @@ f_ctrack() {
 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"
+
+       # 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
 
-       if { [ -z "${trm_radio}" ] && ! printf "%s" "${trm_radiolist}" | "${trm_grepcmd}" -q "${radio}"; } ||
-               { [ -n "${trm_radio}" ] && printf "%s" "${trm_radio}" | "${trm_grepcmd}" -q "${radio}"; }; then
+       # 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),/^.*<meta[ \t]+http-equiv=['\''"]*refresh.*[ \t;]url=/){print substr(tolower($0),RLENGTH+1)}' | "${trm_awkcmd}" 'BEGIN{FS="[:/]"}{printf "%s",$4;exit}')"
@@ -698,6 +806,9 @@ f_net() {
                                        fi
                                fi
                        fi
+
+               # curl error path: extract errormsg and any trailing domain token
+               #
                else
                        err_msg="$(printf "%s" "{${json_raw}" | "${trm_jsoncmd}" -ql1 -e '@.errormsg')"
                        json_ed="$(printf "%s" "{${err_msg}" | "${trm_awkcmd}" '/([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+$/{printf "%s",tolower($NF)}')"
@@ -709,14 +820,18 @@ f_net() {
                fi
        fi
        printf "%s" "${result}"
+
        f_log "debug" "f_net       ::: timeout: $((trm_maxwait / 6)), cp (json/html/js): ${json_cp:-"-"}/${html_cp:-"-"}/${js_cp:-"-"}, result: ${result}, error (rc/msg): ${json_ec}/${err_msg:-"-"}, url: ${trm_captiveurl}"
 }
 
 # check interface status
 #
 f_check() {
-       local raw ifname radio dev_status result login_script login_script_args cp_domain wait_time="0" enabled="1" mode="${1}" status="${2}" sta_radio="${3}" sta_essid="${4}" sta_bssid="${5}"
+       local rc raw ifname dev_status result login_script login_script_args cp_domain station_id ifquality
+       local wait_time="0" enabled="1" mode="${1}" status="${2}" sta_radio="${3}" sta_essid="${4}" sta_bssid="${5}"
 
+       # parse station id from runtime json (initial/dev mode only)
+       #
        if [ "${mode}" = "initial" ] || [ "${mode}" = "dev" ]; then
                json_get_var station_id "station_id"
                sta_radio="${station_id%%/*}"
@@ -727,13 +842,21 @@ f_check() {
        fi
        f_getcfg "${sta_radio}" "${sta_essid}" "${sta_bssid}"
 
+       # resolve uplink 'enabled' flag (skip for rev mode and unset stations)
+       #
        if [ "${mode}" != "rev" ] && [ -n "${sta_radio}" ] && [ "${sta_radio}" != "-" ] && [ -n "${sta_essid}" ] && [ "${sta_essid}" != "-" ]; then
-               enabled="$(f_getval "enabled")"
+               enabled="$(f_getval "enabled" "0")"
        fi
+
+       # trigger wifi reload on disconnects (non-initial/dev) or on disabled-uplink dev events
+       #
        if { [ "${mode}" != "initial" ] && [ "${mode}" != "dev" ] && [ "${status}" = "false" ]; } ||
                { [ "${mode}" = "dev" ] && { [ "${status}" = "false" ] || { [ "${trm_ifstatus}" != "${status}" ] && [ "${enabled}" = "0" ]; }; }; }; then
                f_wifi
        fi
+
+       # sta mode: bounce travelmate interface via ubus
+       #
        if [ "${mode}" = "sta" ]; then
                "${trm_ubuscmd}" -S call network.interface."${trm_iface}" down >/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)"
index a7571f6456464c39c18fb6fd872c3fd0878cb554..c7957fcf85885b896f174c0f209e3e6cfb2f2a42 100755 (executable)
@@ -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
index de09d14cccf03fe9764ad6e2c580468efa8d7f5f..1addcca424eb6a2b4787ca454007bfb07c4436c8 100755 (executable)
@@ -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"
index bfeea17e4b835d661b31b3c7934b0f441b94b7ab..7b1abc1fa232f830f0becdc47312b8ef46b21fc3 100755 (executable)
@@ -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
git clone https://git.99rst.org/PROJECT