#!/bin/sh
# aurora-vpn (B15): full-tunnel VPN for the Wi-Fi LAN. Two modes (/etc/aurora/vpn.conf VPN_MODE):
#   vless = sing-box VLESS-over-WS-over-TLS via CDN, TUN singtun0 (config /etc/sing-box/config.json)
#   wg    = kernel WireGuard wg0 (config /etc/wireguard/wg0.conf) [+ B22: wg1 from /etc/wireguard/wg1.conf if present]
# B22: with wg1 present both tunnels are up; the LAN default route (table 51820) starts on wg0 and aurora-vpnsel moves it between
#   wg1 (direct WireGuard over LTE, preferred) and wg0 (via the free-turn relay on 127.0.0.1). The firewall allows both.
# LAN clients are policy-routed into the tunnel; the board itself, USB NCM management and the tunnel's own
# outbound (to the VPN server/CDN) stay DIRECT on wwan0 (source-based rule -> no loop).
# Fail-closed: while up, LAN may leave ONLY via the tunnel; if it is down, LAN has no internet (never LTE).
#   aurora-vpn up | down | status.  Needs kernel 7.2.7-aurora-b15 (TUN; WireGuard for wg mode).
TBL=51820; RULE_PRI=100; S=/run/aurora-vpn; mkdir -p $S; LOG=$S/vpn.log
[ -f /etc/aurora/router.conf ] && . /etc/aurora/router.conf
[ -f /etc/aurora/vpn.conf ] && . /etc/aurora/vpn.conf
VPN_MODE=${VPN_MODE:-wg}; LAN_IF=${LAN_IF:-wlan0}; LAN_NET=${LAN_NET:-192.168.77.0/24}; LAN_ADDR=${LAN_ADDR:-192.168.77.1}
WAN_IF=${WAN_IF:-wwan0}; MGMT_IF=${MGMT_IF:-usb0}; TUN_IF=${TUN_IF:-singtun0}; TUN_MTU=${TUN_MTU:-1400}
SB=/etc/sing-box/config.json; WGC=/etc/wireguard/wg0.conf
now() { cut -d' ' -f1 /proc/uptime; }
log() { m="[$(now)] $*"; echo "$m"; echo "$m" >> $LOG; echo "<5>[aurora-vpn] $*" > /dev/kmsg 2>/dev/null; }
fail() { log "FAIL: $*"; exit 1; }
tif() { [ "$VPN_MODE" = wg ] && echo wg0 || echo "$TUN_IF"; }
# all tunnel interfaces the LAN may leave through, as an nft list body
tl() { if [ "$VPN_MODE" = wg ]; then [ -f /etc/wireguard/wg1.conf ] && echo '"wg0", "wg1"' || echo '"wg0"'; else echo "\"$TUN_IF\""; fi; }
# bring up one WireGuard interface from /etc/wireguard/<if>.conf; non-loopback endpoint gets a /32 direct route via WAN
wg_up() {
	i=$1; c=/etc/wireguard/$i.conf
	ip link add dev $i type wireguard 2>/dev/null || ip link show $i >/dev/null 2>&1 || fail "no WireGuard"
	[ -f $c ] || fail "$c missing"
	A=$(sed -n 's/^Address[ ]*=[ ]*//p' $c | head -1); M=$(sed -n 's/^MTU[ ]*=[ ]*//p' $c | head -1); EP=$(sed -n 's/^Endpoint[ ]*=[ ]*//p' $c | head -1 | sed 's/:[0-9]*$//')
	wg-quick strip $i 2>/dev/null | wg setconf $i /dev/stdin || { ip link del $i; fail "wg setconf $i"; }
	ip addr flush dev $i; ip addr add "$A" dev $i; ip link set $i mtu "${M:-$TUN_MTU}" up
	# loopback endpoint = local relay (free-turn-client on 127.0.0.1): its own uplink is board traffic -> main -> wwan0
	case "$EP" in 127.*) log "$i endpoint $EP is local relay: no direct route";;
	*) GW=$(ip route show default dev $WAN_IF | sed -n 's/.* via \([^ ]*\).*/\1/p' | head -1)   # point-to-point wwan0: usually no gateway
	   ip route replace "$EP/32" dev $WAN_IF ${GW:+via $GW}; echo "$EP" > $S/endpoint-$i
	   log "$i endpoint $EP direct via $WAN_IF";; esac
}

firewall() {
	T=$(tif); TL=$(tl); MSS=$(( ${1:-$TUN_MTU} - 40 ))
	nft list table inet aurora_router >/dev/null 2>&1 && nft delete table inet aurora_router
	nft -f - <<NFT || fail "nft"
table inet aurora_vpn {
	chain input {
		type filter hook input priority filter; policy accept;
		ct state established,related accept
		iifname "lo" accept
		iifname "$MGMT_IF" accept
		iifname "$LAN_IF" jump lan_in
		iifname { "$WAN_IF", $TL } jump wan_in
	}
	chain lan_in {
		udp sport 68 udp dport 67 accept
		ip daddr != $LAN_ADDR counter drop
		udp dport 53 accept
		tcp dport 22 accept comment "B17: SSH (dropbear) from Wi-Fi to the board LAN address only"
		tcp dport 53 accept
		icmp type echo-request limit rate 20/second accept
		counter drop
	}
	chain wan_in {
		icmp type { echo-reply, destination-unreachable, time-exceeded } accept
		ct state established,related accept
		counter drop
	}
	chain forward {
		type filter hook forward priority filter; policy drop;
		ct state invalid counter drop
		iifname "$LAN_IF" oifname { $TL } ip saddr $LAN_NET tcp flags syn tcp option maxseg size set $MSS
		iifname "$LAN_IF" oifname { $TL } ip saddr $LAN_NET counter accept
		iifname { $TL } oifname "$LAN_IF" ct state established,related counter accept
		iifname "$LAN_IF" oifname "$WAN_IF" counter drop comment "fail-closed: no LTE leak"
		iifname "$LAN_IF" oifname "$MGMT_IF" counter drop
		counter drop
	}
	chain postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		oifname { $TL } ip saddr $LAN_NET counter masquerade
	}
}
NFT
}
route_lan() {
	T=$(tif)
	ip route replace default dev "$T" table $TBL
	ip rule show | grep -q "lookup $TBL" || ip rule add from "$LAN_NET" lookup $TBL priority $RULE_PRI
	ip rule show | grep -q "from $LAN_NET to $LAN_NET lookup main" || ip rule add from "$LAN_NET" to "$LAN_NET" lookup main priority $((RULE_PRI-1))
	echo 1 > /proc/sys/net/ipv4/ip_forward
}
unroute() {
	while ip rule del from "$LAN_NET" lookup $TBL 2>/dev/null; do :; done
	while ip rule del from "$LAN_NET" to "$LAN_NET" lookup main 2>/dev/null; do :; done
	ip route flush table $TBL 2>/dev/null
}
case "$1" in
up)
	log "=== VPN UP ($VPN_MODE)"
	if [ "$VPN_MODE" = vless ]; then
		[ -f $SB ] || fail "$SB missing"
		command -v sing-box >/dev/null || fail "sing-box not installed"
		pidof sing-box >/dev/null && { log "sing-box already running"; } || {
			sing-box check -c $SB 2>>$LOG || fail "sing-box config invalid"
			setsid sh -c "trap '' HUP; exec sing-box run -c $SB" >> $S/sing-box.log 2>&1 < /dev/null &
			echo $! > $S/sing-box.pid
		}
		i=0; while [ ! -e /sys/class/net/$TUN_IF ] && [ $i -lt 100 ]; do sleep 0.1; i=$((i+1)); done
		[ -e /sys/class/net/$TUN_IF ] || { tail -8 $S/sing-box.log; fail "$TUN_IF did not appear"; }
		ip link set $TUN_IF mtu $TUN_MTU 2>/dev/null
	else
		wg_up wg0; M0=$M
		[ -f /etc/wireguard/wg1.conf ] && wg_up wg1
		M=$M0   # MSS clamp from wg0 (the smaller MTU, 1280) covers both tunnels
	fi
	route_lan; firewall $M
	log "=== VPN UP OK mode=$VPN_MODE tun=$(tif) LAN $LAN_NET -> $(tif) (table $TBL); fail-closed on"
	;;
down)
	log "=== VPN DOWN"
	[ "$VPN_MODE" = vless ] && { pidof sing-box >/dev/null && kill $(pidof sing-box); sleep 1; pidof sing-box >/dev/null && kill -9 $(pidof sing-box); rm -f $S/sing-box.pid; }
	[ "$VPN_MODE" = wg ] && { ip link del wg0 2>/dev/null; ip link del wg1 2>/dev/null; }
	unroute
	for e in $S/endpoint $S/endpoint-*; do [ -f $e ] && { ip route del "$(cat $e)/32" dev $WAN_IF 2>/dev/null; rm -f $e; }; done
	nft list table inet aurora_vpn >/dev/null 2>&1 && nft delete table inet aurora_vpn
	/usr/sbin/aurora-router up >/dev/null 2>&1 || log "note: run 'aurora-router up' to restore LAN->LTE"
	log "=== VPN DOWN OK (router restored: $(nft list tables 2>/dev/null | tr '\n' ' '))"
	;;
status)
	T=$(tif); echo "kernel $(uname -r) mode $VPN_MODE"
	if [ -e /sys/class/net/$T ]; then
		echo "$T UP mtu=$(cat /sys/class/net/$T/mtu)"
		if [ "$VPN_MODE" = vless ]; then
			echo "sing-box pid=$(pidof sing-box) rss_kB=$(awk '/VmRSS/{print $2}' /proc/$(pidof sing-box 2>/dev/null)/status 2>/dev/null)"
			tail -3 $S/sing-box.log 2>/dev/null | sed 's/^/  /'
		else
			for i in wg0 wg1; do [ -e /sys/class/net/$i ] || continue
				HS=$(wg show $i latest-handshakes 2>/dev/null | awk '{print $2}' | head -1)
				echo "$i handshake_age=$([ -n "$HS" ] && [ "$HS" != 0 ] && echo $(( $(date +%s)-HS ))s || echo never) transfer=$(wg show $i transfer 2>/dev/null | awk '{print $2"/"$3}')"
			done
			echo "active: $(ip route show table $TBL | awk '/^default/{print $3}') ($(cat /run/aurora-vpnsel/mode 2>/dev/null || echo no-selector))"
		fi
		echo "rules:"; ip rule show | grep -E "lookup $TBL|$LAN_NET"
		echo "table $TBL:"; ip route show table $TBL
	else echo "$T DOWN"; fi
	nft list table inet aurora_vpn 2>/dev/null | grep -E "oifname|masquerade|drop" | sed 's/^[ \t]*//' | head
	;;
*) echo "usage: $0 up|down|status"; exit 2;;
esac
