#!/bin/sh /etc/rc.common
# vim: set noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 :
START=95
STOP=10
PROG_NAME=sslocal
PROG=/usr/bin/$PROG_NAME
PROG_CONF=/tmp/shadowsocks.conf
TRACKER=/usr/bin/ss-track
USE_PROCD=1
EXTRA_COMMANDS="ifup ifdown"
EXTRA_HELP="        ifup    Enable the hook to route traffic through sslocal
        ifdown  Disable the hook to prevent traffic from being routed through sslocal"

# nftable architecture used in this script for sslocal:
# PREROUTING/nat
#   - delegate_prerouting (for OpenWRT and user DNAT)
#   - socks_hook (This is the main "cable" when we want to enable LAN routing through shadowsocks)
#       - socks_decision decides while bypass, accept or go to shadowsocks
#       - ipset socks_bypass lookup (if dst addr match, ACCEPT: exit PREROUTING/nat altogether)
# OUTPUT/nat
# 	- socks_tracker: outgoing packets for shadowsocks-tracker shall be routed through shadowsocks
#	- socks_emitted_by_myself: either @socks_bypass or routed through shadowsocks
#	- socks_iperf: this chain ensures speedtest to go through shadowsocks for more reliability
#

# When reloading:
# nftable global skeleton doesn't change.
# Only socks_hook **content** change
# There is also an ipset swap between socks_staging <-> socks_bypass
# PREROUTING only changes upon ifup/ifdown

_log() {
	# 1.alert
	# 2.crit
	# 3.err
	# 4.warn
	# 5.notice
	# 6.info
	# 7.debug
	logger -t $PROG_NAME-initd -p "$@"
}

CHAINS="socks_hook socks_decision socks_emitted_by_myself socks_tracker socks_iperf"

# List of destinations ports in tcp that will bypassed shadowsocks
BYPASSED_PORTS="1723"

# Nets that should always bypass shadowsocks
LOCALNETS="
0.0.0.0/8
10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.168.0.0/16
224.0.0.0/4"

IPERF_PORT="5008"

# When following config files change and reload_config is called, we trigger a reload here
service_triggers() {
	procd_add_reload_trigger "shadowsocks" "network" "firewall"
}

_running() {
	pgrep -x $PROG > /dev/null
}

start_service() {
	_log 6 "Starting service..."
	config_load shadowsocks
	config_foreach _shadow_init_once client
}

stop_service() {
	_log 6 "Stopping service..."
	_nft_clean_all
}

# Called from tracker when it sees sslocal/ssserver communication works
ifup() {
	# We don't want to ifup if we are not running :)
	_running || return 0
	_shadow_cable plug
}

# Called from tracker when it sees sslocal/ssserver communication doesn't work anymore
# This prevent traffic from flowing through shadowsocks
ifdown() {
	# We don't want to ifdown if we are not running :)
	_running || return 0
	_shadow_cable unplug
}

# This function enable or disable global routing through shadowsocks
# $1 is "plug" or "unplug"
_shadow_cable() {
	# Do nothing and say nothing if we don't understand $1
	if [ "$1" != "plug" ] && [ "$1" != "unplug" ]; then
		return 0
	fi

	# Add PREROUTING chain if not present ("create" adds a chain if not existing and returns error otherwise)
	nft create chain ip nat PREROUTING '{ type nat hook prerouting priority dstnat; policy accept; }' 2> /dev/null

	# Check if cable is already plugged
	already=$(nft -a list chain ip nat PREROUTING | grep -o 'handle' | wc -l)

	# Plug/Unplug
	if [ "$1" = "plug" ] && [ "$already" -lt 2 ]; then
		_log 6 "Plugging shadowsocks cable in..."
		nft add rule ip nat PREROUTING ip protocol tcp counter jump socks_hook
		nft add rule ip nat OUTPUT ip protocol tcp counter jump socks_emitted_by_myself
	elif [ "$1" = "unplug" ] && [ "$already" -gt 1 ]; then
		_log 6 "Unplugging shadowsocks cable..."

		# These rules will unplug the cable
		nft flush chain ip nat PREROUTING
		handle=$(nft -a list table ip nat | grep "jump socks_emitted_by_myself" | cut -d ' ' -f 13)
		nft delete rule ip nat OUTPUT handle "$handle"
	fi

	# Do nothing and say nothing if we don't need to do anything (idempotent behaviour)
}

# Initialize shadowsocks for the first time
# Call me only once at startup, never during reload.
# I make sure I'm called once even if the conf has several shadowsocks
_shadow_init_once() {
	disabled=
	config_get_bool disabled "$1" disabled "false"

	if [ "$disabled" = "1" ]; then
		_log 6 "Disabling service..."
		_nft_clean_all
		return 0
	fi

	# If the config cannot be validated, abort the starting operation altogether
	_validate_conf "$1" || return 0

	# Launch shadowsocks first to minimize the time during which traffic is routed to a non-listening-yet-process
	_launch_shadow "$1"
	_nft_init_skeleton "$1"
	_update_conf_and_nft "$1"
	_launch_tracker "$1"
}

# I'm stateless, I don't make any assumptions on nftable.
# Call me whenever you want to transition from unknown state to a known, clean one
_nft_clean_all() {
    #Flush table NAT
    nft flush table ip nat 2> /dev/null

    #Delete table NAT's added chains
	for chain in $CHAINS; do
        nft delete chain ip nat "$chain" 2> /dev/null
	done

    # Now let's delete NAT table
    nft delete table ip nat 2> /dev/null

	# Finally let's delete the set that we created !
    nft delete set ip nat socks_bypass 2> /dev/null
}

# This validates shadowsocks uci configuration
# It only checks the type of the params present in the config
# Missing params are not an error
# $1 is a ref to the shadowsocks uci section (type =client)
_validate_conf() {
	[ "$1" = proxy ] || return 1
	local port lport password method timeout server reuse_port fast_open
	uci_validate_section shadowsocks client "$1" \
		'port:port'          \
		'lport:port'         \
		'password:string'    \
		'method:string'      \
		'timeout:uinteger'   \
		'server:host'        \
		'reuse_port:bool'    \
		'fast_open:bool'     \
		'monitoring_ip:host' || {
			_log 3 "Cannot validate uci configuration. Are the types correct? ABORTED reload or start action."
			return 1
		}
}

# This function launches one shadowsocks per CPU
# $1 is a ref to the shadowsocks uci section (type =client)
_launch_shadow() {
	# ss-rust Tokio runtime manages well multi-threading (one instance per cpu) 
	procd_open_instance
	procd_set_param command "$PROG" -c "$PROG_CONF"
	procd_set_param file "$PROG_CONF"
	procd_set_param limits nofile="51200 51200"
	procd_set_param respawn 0 10 0
	procd_set_param stderr 1
	procd_close_instance
}

# This sets up the common nft skeleton that doesn't change across reloads
# Don't call me upon reload. Call me only when starting the service
_nft_init_skeleton() {

	# Retrieve shadowsocks local port from config
	config_get lport "$1" lport ""

	# First, make sure we start from a known state.
	# Maybe we crashed last time and no one called stop_service
	_nft_clean_all

    # Create table NAT. This Table is of type ip with symbolic name "nat"
    nft create table ip nat 2> /dev/null

	# Create all custom chains in the NAT table
	for chain in $CHAINS; do
        nft create chain ip nat "$chain" 2> /dev/null
	done

    # Create now the set of IPs
    nft add set ip nat socks_bypass '{ type ipv4_addr ; flags interval ; comment "List of bypass firewall hosts" ; }'

	# Glue all chains and ipset together

    # Glue bypass set to walk through
    nft add rule ip nat socks_decision ip protocol tcp ip daddr @socks_bypass counter return
	for port in $BYPASSED_PORTS; do
        nft add rule ip nat socks_decision tcp dport "{ $port }" counter accept
	done

    # Glue redirect
    nft add rule ip nat socks_decision ip protocol tcp counter redirect to :"$lport"

	# Create base chain OUTPUT
	nft create chain ip nat OUTPUT '{ type nat hook output priority -100; policy accept; }' 2> /dev/null
	nft add rule ip nat OUTPUT ip protocol tcp counter jump socks_tracker
	nft add rule ip nat OUTPUT ip protocol tcp counter jump socks_iperf

	# Now, everything is glued together except socks_hook which is not connected to socks_decision yet.
	# PREROUTING isn't linked to socks_hook either, traffic doesn't flow through sslocal for now.
}

# $1 is a ref to the shadowsocks uci section (type =client)
_update_conf_and_nft() {
	_generate_conf "$1"
	_populate_ipset_socks_bypass "$1"
	_populate_nft_socks_hook
	_populate_nft_socks_emitted_by_myself
	_populate_nft_socks_iperf "$1"
	_update_nft_monitoring_ip_rule "$1"
}

# Generate the JSON configuration file that sslocal binary can understand
# $1 is a ref to the shadowsocks uci section (type =client)
_generate_conf() {
	local server port lport password timeout method reuse_port fast_open
	config_get server "$1" server "127.0.0.1"
	config_get port "$1" port ""
	config_get lport "$1" lport ""
	config_get password "$1" password ""
	config_get timeout "$1" timeout ""
	config_get method "$1" method ""

	config_get_bool reuse_port "$1" reuse_port 0
	if [ "$reuse_port" -eq 1 ]; then
		reuse_port="true"
	else
		reuse_port="false"
	fi

	config_get_bool fast_open "$1" fast_open 0
	if [ "$fast_open" -eq 1 ]; then
		fast_open="true"
	else
		fast_open="false"
	fi

	cat > "$PROG_CONF" <<-EOF
	{
	  "server": "$server",
	  "server_port": $port,
	  "local_address": "0.0.0.0",
	  "local_port": $lport,
	  "password": "$password",
	  "timeout": $timeout,
	  "method": "$method",
	  "fast_open": $fast_open,
	  "disable_sni": true,
	  "mptcp": true,
	  "protocol": "redir",
      "tcp-redir": "redirect"
	}
	EOF
}

# Here is the update of the ipset "socks_bypass"
_populate_ipset_socks_bypass() {

	# Add the local nets to the exceptions list
	for net in $LOCALNETS; do
		nft add element ip nat socks_bypass "{ $net }"
	done

	server= ; config_get server "$1" server
	[ -n "$server" ] && nft add element ip nat socks_bypass "{ $server/32 }"

	# Add the static routes and the WAN ips to the exceptions list
	config_load network
	config_foreach _populate_ipset_socks_bypass_static_routes_one route
	config_foreach _populate_ipset_socks_bypass_wan_one interface
}


# $1 is a ref to the network route uci element (type =route)
# If the client adds static routes via the luci interface, we need
# to let the packets escape sslocal so that the intended routing can take place.
_populate_ipset_socks_bypass_static_routes_one() {
	local target netmask gateway

	config_get target "$1" target ''
	config_get netmask "$1" netmask '255.255.255.255'
	config_get gateway "$1" gateway ''

	# Skip strange buggy route configuration
	if [ -z "$gateway" ] || [ -z "$target" ]; then
		_log 4 "Skipping strange static route with missing gateway or target..."
		return 0
	fi

	# Check the target and network are not random strange strings
	# If they are not valid, the corresponding value will turn to 0
	eval "$(ipcalc.sh "$target" "$netmask")"
	if [ "$NETWORK" = "0.0.0.0" ] || [ "$PREFIX" = "0" ]; then
		_log 4 "Skipping garbage or wildcard static route ($target/$netmask)..."
		return 0
	fi

	# Check the network part is really the network address of this network/mask
	# Because when it's not the case, the route is not added to the routing table, even if it's in uci config
	# If the route is not in the routing table, we don't want to add a bypass rule for sslocal
	# An example of wrong route is 9.9.9.9/30. The correct one is 9.9.9.8/30.
	if [ "$NETWORK" != "$IP" ]; then
		_log 4 "Skipping static route with wrong network ($IP/$PREFIX instead of $NETWORK/$PREFIX)"
		return 0
	fi

	# If the route made it here and survived, let's add it! :)
	nft add element ip nat socks_bypass "{ $NETWORK/$PREFIX }"
}

# Local nets are already bypassed. However some clients may have public IPs directly on a WAN interface
# This may happen for example with a modem in bridge mode. We want to bypass shadowsocks for each WAN address.
# This avoids infinite network loop
_populate_ipset_socks_bypass_wan_one() {
	local device subnet

	# Get the current interface name
	config_get device "$1" device

	# Fetch IP/Netmask but skip interfaces with no IP
	# shellcheck disable=SC1091
	. /lib/functions/network.sh
	network_get_subnet subnet "$device" || return 0

	# Find the network address and check everything is valid :)
	# $subnet is in the form a.b.c.d/z
	eval "$(ipcalc.sh "$subnet")"
	if [ "$NETWORK" = "0.0.0.0" ] || [ "$PREFIX" = "0" ]; then
		_log 4 "Skipping WAN IP ($subnet) that I don't understand..."
		return 0
	fi

	# Skip bypassing tun0 as it was already taken into account with the LOCALNETS
	if [ "$NETWORK" = "10.166.179.0" ]; then
		_log 7 "skipping tunnel adress: $NETWORK as it is already skipped"
		return 0
	fi

	# Add the WAN IP to the exceptions list
	# We use -q here: most of the time, WAN interface IPs are local and may already be excluded in the set
	nft add element ip nat socks_bypass "{ $NETWORK/$PREFIX }"
}

# This updates the socks_hook chain with interfaces the user wants to route through shadowsocks
# This is handled in a by-interface basis.
# Should only be executed when starting or reloading shadowsocks
_populate_nft_socks_hook() {
	# First, reset the chain
	nft flush chain ip nat socks_hook

	# For each zone, find if we got a lan zone
	# And activate the routing to shadowsocks for all its interfaces
	# (This is not the main plug, traffic may not flow through sslocal after this)
	config_load firewall
	config_foreach _update_socks_hook_one_zone zone
}

# $1 is a ref to the current zone section being processed
_update_socks_hook_one_zone() {
	# Let's fetch the current zone name
	local zone zone_members
	config_get zone "$1" name

	# Only traffic coming from a lan zone's interface will be routed to shadowsocks
	if [ "$zone" = "lan" ]; then
		# We don't use config_list_foreach so that we are compatible with lists AND single string with spaces
		config_get zone_members "$1" network

		# Call this callback for each interface belonging to the lan zone
		# Don't put double quotes around $zone_members or the loop will iterate only once
		for member in $zone_members; do
			_update_socks_hook_one_if "$member"
		done
	fi
}

# $1 is one of the interfaces belonging to the lan zone
_update_socks_hook_one_if() {
	# Plug socks_hook with socks_decision for that interface
	local device
	config_load network
	config_get device "$1" device "$1"

	nft add rule ip nat socks_hook iifname "$device" counter jump socks_decision
}

# Add an nft rule to nat/OUTPUT for monitoring_ip
# $1 is a ref to the shadowsocks uci section (type =client)
_update_nft_monitoring_ip_rule() {
	local lport monitoring_ip

	config_get lport "$1" lport ""
	config_get monitoring_ip "$1" monitoring_ip ""

	# First, let's empty the chain!
	# Note that any running overthebox_test_download_proof will be pwned here :p This is fine.
	nft flush chain ip nat socks_tracker

	# Only add the rule if monitoring_ip is set and the tracker binary is there
	if [ -n "$monitoring_ip" ] && [ -x "$TRACKER" ]; then
		# Add the special rule so that when we send a packet to the monitoring_ip, it flows through sslocal
		# Note that the whole traffic to the API config will also go through shadowsocks (IP is the same)
		# If shadowsocks is manually stopped, mud will provide network access to API config
		nft add rule ip nat socks_tracker ip protocol tcp ip daddr "$monitoring_ip/32" counter redirect to :8388 comment \"shadowsocks_tracker\"
	else
		# When there is no monitoring, don't wait for a tracker to enable routing through shadowsocks
		# First add provisioning API rule &
		# Let's plug the cable in! :p
		_shadow_cable plug
	fi
}

_populate_nft_socks_emitted_by_myself () {

	# First, let's empty the chain!
	# Note that any running overthebox_test_download_proof will be pwned here :p This is fine.
	nft flush chain ip nat socks_emitted_by_myself

	# Add necessary rules that redirects outcomming TCP packets to shadowsocks
	nft add rule ip nat socks_emitted_by_myself ip protocol tcp ip daddr @socks_bypass counter return
}

_populate_nft_socks_iperf () {

	config_get lport "$1" lport ""

	# First, let's empty the chain!
	# Note that any running overthebox speed test will be pwned here :p This is fine.
	nft flush chain ip nat socks_iperf

	# Add necessary rules that redirects iperf outcomming TCP packets to shadowsocks
	nft add rule ip nat socks_iperf tcp dport "$IPERF_PORT" counter redirect to :"$lport" comment \"speedTests\"
}

# This function launches the shadowsocks tracker
# $1 is a ref to the shadowsocks uci section (type =client)
_launch_tracker() {
	local lport monitoring_ip track_timeout track_interval track_retry
	config_get lport "$1" lport ""
	config_get monitoring_ip "$1" monitoring_ip ""
	config_get track_timeout "$1" track_timeout "5"
	config_get track_interval "$1" track_interval "10"
	config_get track_retry "$1" track_retry "0"

	# Only launch the tracker if monitoring_ip is set and the tracker binary is there
	if [ -n "$monitoring_ip" ] && [ -x "$TRACKER" ]; then
		procd_open_instance
		procd_set_param command "$TRACKER" -t "$track_timeout" -v "$track_interval" -r "$track_retry" "$monitoring_ip"
		procd_set_param respawn 0 10 0
		procd_set_param stderr 1
		procd_close_instance
	fi
}
