#!/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)
# 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"; }

firewall() {
	T=$(tif); 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", "$T" } 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 "$T" ip saddr $LAN_NET tcp flags syn tcp option maxseg size set $MSS
		iifname "$LAN_IF" oifname "$T" ip saddr $LAN_NET counter accept
		iifname "$T" 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 "$T" 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
		ip link add dev wg0 type wireguard 2>/dev/null || ip link show wg0 >/dev/null 2>&1 || fail "no WireGuard"
		[ -f $WGC ] || fail "$WGC missing"
		A=$(sed -n 's/^Address[ ]*=[ ]*//p' $WGC | head -1); M=$(sed -n 's/^MTU[ ]*=[ ]*//p' $WGC | head -1); EP=$(sed -n 's/^Endpoint[ ]*=[ ]*//p' $WGC | head -1 | sed 's/:[0-9]*$//')
		wg-quick strip wg0 2>/dev/null | wg setconf wg0 /dev/stdin || { ip link del wg0; fail "wg setconf"; }
		ip addr flush dev wg0; ip addr add "$A" dev wg0; ip link set wg0 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 "endpoint $EP is local relay: no direct route";;
		*) GW=$(ip route show default dev $WAN_IF | awk '{print $3; exit}'); ip route replace "$EP/32" dev $WAN_IF ${GW:+via $GW}; echo "$EP" > $S/endpoint;; esac
	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
	unroute
	[ -f $S/endpoint ] && { ip route del "$(cat $S/endpoint)/32" dev $WAN_IF 2>/dev/null; rm -f $S/endpoint; }
	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
			HS=$(wg show wg0 latest-handshakes 2>/dev/null | awk '{print $2}' | head -1)
			echo "handshake_age=$([ -n "$HS" ] && [ "$HS" != 0 ] && echo $(( $(date +%s)-HS ))s || echo never) transfer=$(wg show wg0 transfer 2>/dev/null | awk '{print $2"/"$3}')"
		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
