#!/bin/sh /etc/rc.common
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org

USE_PROCD=1

START=99
STOP=10

CONF="homeproxy"
PROG="/usr/bin/sing-box"

HP_DIR="/etc/homeproxy"
RUN_DIR="/var/run/homeproxy"
LOG_PATH="$RUN_DIR/homeproxy.log"

# we don't know which is the default server, just take the first one
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then
	DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-homeproxy.d"
else
	DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
fi

log() {
	echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
}

start_service() {
	config_load "$CONF"

	local routing_mode proxy_mode
	config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
	config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"

	local outbound_node
	if [ "$routing_mode" != "custom" ]; then
		config_get outbound_node "config" "main_node" "nil"
	else
		config_get outbound_node "routing" "default_outbound" "nil"
	fi

	local server_enabled
	config_get_bool server_enabled "server" "enabled" "0"

	if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
		return 1
	fi

	mkdir -p "$RUN_DIR"

	if [ "$outbound_node" != "nil" ]; then
		# Generate/Validate client config
		ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"

		if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then
			log "Error: failed to generate client configuration."
			return 1
		elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
			log "Error: wrong client configuration detected."
			return 1
		fi

		# Auto update
		local auto_update auto_update_time
		config_get_bool auto_update "subscription" "auto_update" "0"
		if [ "$auto_update" = "1" ]; then
			config_get auto_update_time "subscription" "auto_update_time" "2"
			sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"
			echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root"
			/etc/init.d/cron restart
		fi

		# DNSMasq rules
		local ipv6_support dns_port
		config_get_bool ipv6_support "config" "ipv6_support" "0"
		config_get dns_port "infra" "dns_port" "5333"
		mkdir -p "$DNSMASQ_DIR"
		echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
		case "$routing_mode" in
		"bypass_mainland_china"|"custom"|"global")
			cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
				no-poll
				no-resolv
				server=127.0.0.1#$dns_port
			EOF
			;;
		"gfwlist")
			[ "$ipv6_support" -eq "0" ] || local gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
			sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \
				"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
			;;
		"proxy_mainland_china")
			sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
				"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
			;;
		esac

		if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then
			[ "$ipv6_support" -eq "0" ] || local wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6"
			sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
				"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
		fi
		/etc/init.d/dnsmasq restart >"/dev/null" 2>&1

		# Setup routing table
		local table_mark
		config_get table_mark "infra" "table_mark" "100"
		case "$proxy_mode" in
		"redirect_tproxy")
			local outbound_udp_node
			config_get outbound_udp_node "config" "main_udp_node" "nil"
			if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then
				local tproxy_mark
				config_get tproxy_mark "infra" "tproxy_mark" "101"

				ip rule add fwmark "$tproxy_mark" table "$table_mark"
				ip route add local 0.0.0.0/0 dev lo table "$table_mark"

				if [ "$ipv6_support" -eq "1" ]; then
					ip -6 rule add fwmark "$tproxy_mark" table "$table_mark"
					ip -6 route add local ::/0 dev lo table "$table_mark"
				fi
			fi
			;;
		"redirect_tun"|"tun")
			local tun_name tun_mark
			config_get tun_name "infra" "tun_name" "singtun0"
			config_get tun_mark "infra" "tun_mark" "102"

			ip tuntap add mode tun user root name "$tun_name"
			sleep 1s
			ip link set "$tun_name" up

			ip route replace default dev "$tun_name" table "$table_mark"
			ip rule add fwmark "$tun_mark" lookup "$table_mark"

			ip -6 route replace default dev "$tun_name" table "$table_mark"
			ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
			;;
		esac

		# sing-box (client)
		procd_open_instance "sing-box-c"

		procd_set_param command "$PROG"
		procd_append_param command run --config "$RUN_DIR/sing-box-c.json"

		if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
			procd_add_jail "sing-box-c" log procfs
			procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
			procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
			[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$RUN_DIR/cache.db"
			procd_add_jail_mount "$HP_DIR/certs/"
			procd_add_jail_mount "/etc/ssl/"
			procd_add_jail_mount "/etc/localtime"
			procd_add_jail_mount "/etc/TZ"
			procd_set_param capabilities "/etc/capabilities/homeproxy.json"
			procd_set_param no_new_privs 1
			procd_set_param user sing-box
			procd_set_param group sing-box
		fi

		procd_set_param limits core="unlimited"
		procd_set_param limits nofile="1000000 1000000"
		procd_set_param stderr 1
		procd_set_param respawn

		procd_close_instance
	fi

	if [ "$server_enabled" = "1" ]; then
		# Generate/Validate server config
		ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"

		if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then
			log "Error: failed to generate server configuration."
			return 1
		elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then
			log "Error: wrong server configuration detected."
			return 1
		fi

		# sing-box (server)
		procd_open_instance "sing-box-s"

		procd_set_param command "$PROG"
		procd_append_param command run --config "$RUN_DIR/sing-box-s.json"

		if [ -x "/sbin/ujail" ]; then
			procd_add_jail "sing-box-s" log procfs
			procd_add_jail_mount "$RUN_DIR/sing-box-s.json"
			procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log"
			procd_add_jail_mount_rw "$HP_DIR/certs/"
			procd_add_jail_mount "/etc/acme/"
			procd_add_jail_mount "/etc/ssl/"
			procd_add_jail_mount "/etc/localtime"
			procd_add_jail_mount "/etc/TZ"
			procd_set_param capabilities "/etc/capabilities/homeproxy.json"
			procd_set_param no_new_privs 1
			procd_set_param user sing-box
			procd_set_param group sing-box
		fi

		procd_set_param limits core="unlimited"
		procd_set_param limits nofile="1000000 1000000"
		procd_set_param stderr 1
		procd_set_param respawn

		procd_close_instance
	fi

	# log-cleaner
	procd_open_instance "log-cleaner"
	procd_set_param command "$HP_DIR/scripts/clean_log.sh"
	procd_set_param respawn
	procd_close_instance

	case "$routing_mode" in
	"bypass_mainland_china")
		# Prepare cache db
		[ -e "$RUN_DIR/cache.db" ] || touch "$RUN_DIR/cache.db"
		;;
	"custom")
		# Prepare ruleset directory
		[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
		;;
	esac

	[ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log"
	if [ "$server_enabled" = "1" ]; then
		echo > "$RUN_DIR/sing-box-s.log"
		mkdir -p "$HP_DIR/certs"
	fi

	# Update permissions for ujail
	chown -R sing-box:sing-box "$RUN_DIR"

	# Setup firewall
	ucode "$HP_DIR/scripts/firewall_pre.uc"
	[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
	fw4 reload >"/dev/null" 2>&1

	log "$(sing-box version | awk 'NR==1{print $1,$3}') started."
}

stop_service() {
	sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"
	/etc/init.d/cron restart >"/dev/null" 2>&1

	# Setup firewall
	# Load config
	config_load "$CONF"
	local table_mark tproxy_mark tun_mark tun_name
	config_get table_mark "infra" "table_mark" "100"
	config_get tproxy_mark "infra" "tproxy_mark" "101"
	config_get tun_mark "infra" "tun_mark" "102"
	config_get tun_name "infra" "tun_name" "singtun0"

	# Tproxy
	ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
	ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
	ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
	ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"

	# TUN
	ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
	ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"

	ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
	ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"

	# Nftables rules
	for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
		 "homeproxy_redirect" "homeproxy_redirect_proxy" \
		 "homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \
		 "homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
		 "homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
		 "homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
		 "homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
		nft flush chain inet fw4 "$i"
		nft delete chain inet fw4 "$i"
	done 2>"/dev/null"
	for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
		 "homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
		 "homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
		 "homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
		 "homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
		 "homeproxy_routing_port"; do
		nft flush set inet fw4 "$i"
		nft delete set inet fw4 "$i"
	done 2>"/dev/null"
	echo 2>"/dev/null" > "$RUN_DIR/fw4_forward.nft"
	echo 2>"/dev/null" > "$RUN_DIR/fw4_input.nft"
	echo 2>"/dev/null" > "$RUN_DIR/fw4_post.nft"
	fw4 reload >"/dev/null" 2>&1

	# Remove DNS hijack
	rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
	/etc/init.d/dnsmasq restart >"/dev/null" 2>&1

	rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
		"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"

	log "Service stopped."
}

service_stopped() {
	# Load config
	config_load "$CONF"
	local tun_name
	config_get tun_name "infra" "tun_name" "singtun0"

	# TUN
	ip link set "$tun_name" down 2>"/dev/null"
	ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
}

reload_service() {
	log "Reloading service..."

	stop
	start
}

service_triggers() {
	procd_add_reload_trigger "$CONF"
	procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload
}
