#!/bin/sh /etc/rc.common
#
# Copyright 2019-2020 Xingwang Liao <kuoruan@gmail.com>
# Copyright 2020-2022 Ycarus (Yannick Chabanois) <ycarus@zugaina.org> for OpenMPTCProuter
# Licensed to the public under the MIT License.
#

START=99
USE_PROCD=1
EXTRA_COMMANDS="rules_up rules_down rules_exist tls_pin_retry"

NAME=v2ray
CONFIG_FOLDER=/var/etc/$NAME
TLS_PIN_DIR=/etc/$NAME/tls-pins
TLS_PIN_PENDING=/var/run/$NAME-tls-pending
TLS_RETRY_PID=/var/run/$NAME-tls-retry.pid
# The certificate each server was pinned with by the last start
TLS_PIN_STARTED=/var/run/$NAME-tls-pinned
# The last start ended with no transparent proxy rules: see rules_up
NORULES=/var/run/$NAME-norules

v2rrules_uc="/usr/share/v2ray-rules/v2r-rules.uc"
v2rrules_nft="/etc/nftables.d/90-proxy-v2ray-rules.nft"

FILE_V2RAY_DNSMASQ=/tmp/dnsmasq.d/$NAME
FILE_V2RAY_DNSMASQ_CACHE=/tmp/$NAME.dnsmasq.cache

IPSET_SRC_IGNORE_V4=v2ray_src_ignore_v4
IPSET_SRC_IGNORE_V6=v2ray_src_ignore_v6
IPSET_DST_PROXY_V4=v2ray_dst_proxy_v4
IPSET_DST_PROXY_V6=v2ray_dst_proxy_v6
IPSET_SRC_DIRECT_V4=ss_rules_src_bypass
IPSET_DST_DIRECT_V4=ss_rules_dst_bypass
IPSET_DST_DIRECT_V6=ss_rules6_dst_bypass

OUTBOUND_SERVERS=
OUTBOUND_SERVERS_V4=
OUTBOUND_SERVERS_V6=

TRANSPARENT_PROXY_EXPECTED=0
TRANSPARENT_PROXY_PORT=
TRANSPARENT_PROXY_USE_TPROXY=
TRANSPARENT_PROXY_ADDITION=

DNSMASQ_RESTART_EXPECTED=0

. /usr/share/libubox/jshn.sh

_log() {
	local level="$1" ; shift
	local msg="$@"
	logger -p "daemon.$level" -t "$NAME" "$msg"

	echo "[$level] $msg" >&2
}

_info() {
	_log "info" "$@"
}

_err() {
	_log "err" "$@"
}

# _tls_pin_cache <address> <port>
# Where the certificate verified for that server is kept
_tls_pin_cache() {
	echo "$TLS_PIN_DIR/$(printf '%s_%s' "$1" "$2" | tr -c 'A-Za-z0-9._-' '_').pem"
}

# _tls_vps_live <address> <port> <server name> <file>
# Writes the certificate the server presents to <file>. Succeeds only when the
# CA the VPS API sent issued it as a server certificate
_tls_vps_live() {
	local addr="$1" port="$2" sni="$3" out="$4" ca="/etc/luci-uploads/ca.crt"
	case "$addr" in
		*:*) addr="[$addr]" ;;
	esac
	echo "Q" | timeout 10 openssl s_client -connect "${addr}:${port}" \
		${sni:+-servername "$sni"} 2>/dev/null \
		| openssl x509 -out "$out" 2>/dev/null
	[ -s "$out" ] && openssl verify -no_check_time -purpose sslserver \
		-CAfile "$ca" "$out" >/dev/null 2>&1
}

# _tls_vps_pin <hex|chain> <address> <port> [<server name>]
# The VPS proxy certificate only has a CN, no SAN, which Go refuses to check
# by name: OMR connects with the certificate pinned instead. The pin used to
# be whatever answered when the proxy started, so anyone on the path then
# chose it. Only pin a certificate the CA the VPS API sent (over the pinned
# API connection) issued as a server certificate; validity dates are left out,
# the clock may not be set yet at boot and a pin does not check them either.
# The certificate is kept once verified: when the VPS can't be reached as the
# proxy starts (boot before the WANs, a VPS reboot), nothing else lets the
# proxy verify it, and it would stay down until the next restart. The VPS may
# have been given another certificate since: tls_pin_retry restarts the proxy
# if it presents one.
# Within a start (TLS_PROBE_MEMO set), each server is probed once: the
# outbounds, the reverse bridge and the tunnels of the other public IPs often
# name the same one, and an unreachable VPS costs 10 s per probe.
# The verified certificate is remembered in the memo (tmpfs), not only in the
# cache: when /etc can't be written (overlay full or read-only), the other
# outbounds to that server must still get it.
# hex: xray's pinnedPeerCertSha256, chain: v2ray's
# pinnedPeerCertificateChainSha256 (base64, the VPS sends no chain)
_tls_vps_pin() {
	local format="$1" addr="$2" port="$3" sni="$4" ca="/etc/luci-uploads/ca.crt"
	local peer="" cache memo="" state="" pem
	[ -s "$ca" ] && [ -n "$addr" ] && [ -n "$port" ] || return 1
	command -v openssl >/dev/null 2>&1 || return 1
	cache="$(_tls_pin_cache "$addr" "$port")"
	if [ -n "$TLS_PROBE_MEMO" ] ; then
		memo="$TLS_PROBE_MEMO/${cache##*/}"
		state="$(cat "$memo" 2>/dev/null)"
	fi
	if [ -z "$state" ] ; then
		peer="$(mktemp /tmp/$NAME-peer.XXXXXX)" || return 1
		if _tls_vps_live "$addr" "$port" "$sni" "$peer" ; then
			state=live
			cmp -s "$peer" "$cache" || _tls_pin_keep "$peer" "$cache" "$addr" "$port"
		else
			state=down
			rm -f "$peer"
			peer=""
		fi
		# live is only remembered with the certificate next to it
		if [ -n "$memo" ] && mkdir -p "$TLS_PROBE_MEMO" ; then
			if [ "$state" = "down" ] || cp "$peer" "$memo.pem" ; then
				echo "$state" >"$memo"
			fi
		fi
	fi
	if [ -n "$peer" ] ; then
		pem="$peer"
	elif [ "$state" = "live" ] && [ -s "$memo.pem" ] ; then
		pem="$memo.pem"
	elif [ -s "$cache" ] && openssl verify -no_check_time -purpose sslserver \
		-CAfile "$ca" "$cache" >/dev/null 2>&1 ; then
		_info "${addr}:${port} certificate not received, pinning the one verified earlier"
		_tls_pin_pending "$addr" "$port" "$sni"
		pem="$cache"
	else
		return 1
	fi
	# for tls_pin_retry: this server needs a restart only for another one
	mkdir -p "$TLS_PIN_STARTED" && cp "$pem" "$TLS_PIN_STARTED/${cache##*/}"
	if [ "$format" = "chain" ] ; then
		openssl x509 -in "$pem" -outform DER | openssl dgst -sha256 -binary | base64
	else
		openssl x509 -in "$pem" -outform DER | openssl dgst -sha256 | awk '{print $NF}'
	fi
	[ -z "$peer" ] || rm -f "$peer"
}

# _tls_pin_keep <file> <cache> <address> <port>
# Keeps a verified certificate for the next starts. Failing that (overlay full
# or read-only) only costs the fallback when the VPS can't be reached at start
_tls_pin_keep() {
	mkdir -p "$TLS_PIN_DIR" 2>/dev/null && cp "$1" "$2.new" 2>/dev/null \
		&& mv -f "$2.new" "$2" 2>/dev/null && return 0
	rm -f "$2.new"
	_err "${3}:${4} certificate verified but could not be kept in $TLS_PIN_DIR"
	return 1
}

# _tls_pin_pending <address> <port> [<server name>]
# No certificate could be verified for this server as the proxy started:
# tls_pin_retry probes it again in the background
_tls_pin_pending() {
	local entry="$1 $2 $3"
	grep -qxF "$entry" "$TLS_PIN_PENDING" 2>/dev/null || echo "$entry" >>"$TLS_PIN_PENDING"
}

# _tls_pin_changed <address> <port> [<server name>]
# 0: the server presents a CA-verified certificate other than the one the
# last start pinned for it, or that start pinned none (the CA file empty then,
# as openmptcprouter-vps rewrites it); 2: it presents the one pinned; 1:
# nothing verified. Compared with what was pinned, not with the cache: a cache
# that can't be written must not make every probe a change, and a restart.
# The cache is still updated
_tls_pin_changed() {
	local cache peer rc=1
	cache="$(_tls_pin_cache "$1" "$2")"
	peer="$(mktemp /tmp/$NAME-peer.XXXXXX)" || return 1
	if _tls_vps_live "$1" "$2" "$3" "$peer" ; then
		cmp -s "$peer" "$cache" || _tls_pin_keep "$peer" "$cache" "$1" "$2"
		if cmp -s "$peer" "$TLS_PIN_STARTED/${cache##*/}" ; then
			rc=2
		else
			rc=0
		fi
	fi
	rm -f "$peer"
	return $rc
}

_tls_retry_running() {
	local pid
	pid="$(cat "$TLS_RETRY_PID" 2>/dev/null)"
	[ -n "$pid" ] && [ "$pid" != "$$" ] && grep -q tls_pin_retry "/proc/$pid/cmdline" 2>/dev/null
}

_tls_retry_schedule() {
	[ -s "$TLS_PIN_PENDING" ] || return 0
	_tls_retry_running && return 0
	_info "VPS certificate not verified, retrying in the background"
	# Out of the caller's cgroup and stdio, and without the service lock
	# (fd 1000) the caller holds: every other init call would wait on it
	sh -c 'echo $$ >/sys/fs/cgroup/cgroup.procs 2>/dev/null; exec /etc/init.d/'"$NAME"' tls_pin_retry' \
		</dev/null >/dev/null 2>&1 1000>&- 9>&- &
}

_tls_retry_stop() {
	_tls_retry_running && kill "$(cat "$TLS_RETRY_PID")" 2>/dev/null
	rm -f "$TLS_RETRY_PID" "$TLS_PIN_PENDING"
	rm -rf "$TLS_PIN_STARTED"
}

# Restarts the proxy once a pending server presents a verified certificate
# other than the one pinned at start (none, or the kept one). A server back
# with the certificate it was pinned with needs nothing more
tls_pin_retry() {
	local delay=15 try=0 addr port sni found left rc
	# rc.common took the service lock on load: held while this sleeps, it
	# would block every restart
	exec 1000>&-
	exec 9>"/var/lock/$NAME-tls-retry.lock"
	flock -n 9 || return 0
	echo $$ >"$TLS_RETRY_PID"
	while [ "$try" -lt 20 ] ; do
		# a sleep left behind by a stop must not keep the lock
		sleep "$delay" 9>&-
		try=$((try + 1))
		delay=$((delay * 2))
		[ "$delay" -gt 300 ] && delay=300
		[ "$(uci -q get "$NAME.main.enabled")" = "1" ] || break
		[ -s "$TLS_PIN_PENDING" ] || break
		found=""
		left=""
		while read -r addr port sni ; do
			_tls_pin_changed "$addr" "$port" "$sni"
			rc=$?
			[ "$rc" = "0" ] && found=1
			[ "$rc" = "1" ] && left="$left$addr $port $sni
"
		done <"$TLS_PIN_PENDING"
		if [ -n "$found" ] ; then
			rm -f "$TLS_RETRY_PID"
			_info "VPS certificate verified, restarting"
			exec 9>&-
			exec /etc/init.d/$NAME restart
		fi
		printf '%s' "$left" >"$TLS_PIN_PENDING"
		[ -n "$left" ] || break
	done
	rm -f "$TLS_RETRY_PID"
	[ "$try" -lt 20 ] || _err "VPS certificate still not verified, giving up until the next start"
}

# _tls_pin_list <hex|base64> <pins>
# Pins are SHA-256 digests of the server certificate, separated by commas, each
# in hex (xray's pinnedPeerCertSha256) or in base64 (v2ray's
# pinnedPeerCertificateChainSha256, the same digest when the server sends a
# single certificate). Prints them in the requested form, one per line. A pin
# the proxy cannot parse stops it from loading its configuration, so it is
# logged and left out instead
_tls_pin_list() {
	local pin
	echo "$2" | awk -v format="$1" '
		BEGIN {
			b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
			hex = "0123456789abcdef"
		}
		{
			n = split($0, pins, /[, \t]+/)
			for (p = 1; p <= n; p++) {
				pin = pins[p]
				if (pin == "")
					continue
				digest = ""
				if (pin ~ /^[0-9a-fA-F]+$/ && length(pin) == 64) {
					digest = tolower(pin)
				} else if (pin ~ /^[A-Za-z0-9+\/]+=$/ && length(pin) == 44) {
					bits = 0; nbits = 0
					for (i = 1; i < 44; i++) {
						bits = bits * 64 + index(b64, substr(pin, i, 1)) - 1; nbits += 6
						if (nbits >= 8) {
							nbits -= 8
							digest = digest sprintf("%02x", int(bits / 2 ^ nbits))
							bits = bits % (2 ^ nbits)
						}
					}
				}
				if (length(digest) != 64) {
					print "!" pin
					continue
				}
				if (format == "hex") {
					print digest
					continue
				}
				out = ""; bits = 0; nbits = 0
				for (i = 1; i <= 64; i++) {
					bits = bits * 16 + index(hex, substr(digest, i, 1)) - 1; nbits += 4
					if (nbits >= 6) {
						nbits -= 6
						out = out substr(b64, int(bits / 2 ^ nbits) + 1, 1)
						bits = bits % (2 ^ nbits)
					}
				}
				print out substr(b64, bits * 4 + 1, 1) "="
			}
		}' | while read -r pin ; do
		case "$pin" in
			"!"*) _err "TLS pin ${pin#!} is not a hex or base64 SHA-256, ignored" ;;
			*) echo "$pin" ;;
		esac
	done
}

# _path_allowed <binary|log> <path>
# Both come from uci and are used as root: the binary is run and made
# executable, the logs are created or appended to
_path_allowed() {
	case "$2" in
		*/../*|*/..) return 1 ;;
	esac
	case "$1:$2" in
		binary:/usr/bin/?*|binary:/usr/sbin/?*|binary:/bin/?*|binary:/sbin/?*|binary:/tmp/?*) return 0 ;;
		log:|log:/dev/null|log:/tmp/?*|log:/var/log/?*) return 0 ;;
	esac
	return 1
}

# _nft_set_populate <set> <type> <addr-list-on-stdin>
# Idempotently (re)creates a native nft set in table "inet fw4" and loads
# addresses/CIDRs read from stdin into it -- the nft-native replacement for
# `ipset -! restore` (this package no longer depends on the ipset binary).
_nft_set_populate() {
	local set="$1" type="$2"
	{
		echo "add set inet fw4 $set { type $type; flags interval; }"
		sed "s/.*/add element inet fw4 $set { & }/"
	} | nft -f - 2>/dev/null
}


get_value_from_json() {
	local json="$1"
	local key="$2"

	test -n "$json" || return

	local value=""

	local old_ns
	json_set_namespace "json_key" old_ns
	json_load "$json"
	json_get_var "$key" value
	json_cleanup
	json_set_namespace "$old_ns"

	echo "$value"
}

get_commands_from_json() {
	local json="$1"

	test -n "$json" || return

	jshn -r "$json" 2>/dev/null | grep -v "json_init"
}

get_file_content() {
	local filename="$1"

	test -n "$filename" || return
	test -r "/etc/v2ray/${filename}.txt" || return

	cat "/etc/v2ray/${filename}.txt" | grep -v "^$" | grep -v "^#"
}

append_server_ipv4() {
	local addr="$1"

	test -n "$addr" || return

	if [ -z "$OUTBOUND_SERVERS_V4" ] ; then
		OUTBOUND_SERVERS_V4="$addr"
	else
		OUTBOUND_SERVERS_V4="$(cat >&1 <<-EOF
			$OUTBOUND_SERVERS_V4
			$addr
		EOF
		)"
	fi
}

append_server_ipv6() {
	local addr="$1"

	test -n "$addr" || return

	if [ -z "$OUTBOUND_SERVERS_V6" ] ; then
		OUTBOUND_SERVERS_V6="$addr"
	else
		OUTBOUND_SERVERS_V6="$(cat >&1 <<-EOF
			$OUTBOUND_SERVERS_V6
			$addr
		EOF
		)"
	fi
}

append_server_address() {
	local addr="$1"

	test -n "$addr" || return

	local ipv4
	for ipv4 in $(resolveip -4 -t 5 "$addr") ; do
		append_server_ipv4 "$ipv4"
		OUTBOUND_SERVERS="$OUTBOUND_SERVERS $ipv4"
	done

	local ipv6
	for ipv6 in $(resolveip -6 -t 5 "$addr") ; do
		append_server_ipv6 "$ipv6"
		OUTBOUND_SERVERS="$OUTBOUND_SERVERS $ipv6"
	done
}

v2ray_section_validate() {
	uci_validate_section "$NAME" "v2ray" "$1" \
		'enabled:bool:0' \
		'v2ray_file:string' \
		'asset_location:directory' \
		'mem_percentage:and(uinteger, max(100)):0' \
		'config_file:file' \
		'loglevel:or("debug", "info", "warning", "error", "none")' \
		'access_log:string' \
		'error_log:string' \
		'stats_enabled:bool:0' \
		'transport_enabled:bool:0' \
		'inbounds:list(uci("v2ray", "@inbound"))' \
		'outbounds:list(uci("v2ray", "@outbound"))'
}

dns_section_validate() {
	uci_validate_section "$NAME" "dns" "$1" \
		'enabled:bool:0' \
		'tag:string' \
		'client_ip:ipaddr' \
		'hosts:list(string)' \
		'servers:list(uci("v2ray", "@dns_server"))'
}

dns_server_section_validate() {
	uci_validate_section "$NAME" "dns_server" "$1" \
		'address:string' \
		'port:port' \
		'domains:list(string)' \
		'expect_ips:list(string)'
}

routing_section_validate() {
	uci_validate_section "$NAME" "routing" "$1" \
		'enabled:bool:0' \
		'domain_strategy:or("AsIs", "IPIfNonMatch", "IPOnDemand")' \
		'rules:list(uci("v2ray", "@routing_rule"))' \
		'balancers:list(uci("v2ray", "@routing_balancer"))'
}

routing_rule_section_validate() {
	uci_validate_section "$NAME" "routing_rule" "$1" \
		'type:"field"' \
		'domain:list(string)' \
		'ip:list(string)' \
		'port:or(port, portrange)' \
		'network:list(or("tcp", "udp"))' \
		'source:list(string)' \
		'user:list(string)' \
		'inbound_tag:list(string)' \
		'protocol:list(or("http", "tls", "bittorrent"))' \
		'attrs:string' \
		'outbound_tag:string' \
		'balancer_tag:string'
}

routing_balancer_section_validate() {
	uci_validate_section "$NAME" "routing_balancer" "$1" \
		'tag:string' \
		'selector:list(string)'
}

policy_section_validate() {
	uci_validate_section "$NAME" "policy" "$1" \
		'enabled:bool:0' \
		'levels:list(uci("v2ray", "@policy_level"))' \
		'system_stats_inbound_uplink:bool:0' \
		'system_stats_inbound_downlink:bool:0'
}

policy_level_section_validate() {
	uci_validate_section "$NAME" "policy_level" "$1" \
		'level:uinteger' \
		'handshake:uinteger:4' \
		'conn_idle:uinteger:300' \
		'uplink_only:uinteger:2' \
		'downlink_only:uinteger:5' \
		'stats_user_uplink:bool:0' \
		'stats_user_downlink:bool:0' \
		'buffer_size:uinteger'
}

reverse_section_validate() {
	uci_validate_section "$NAME" "reverse" "$1" \
		'enabled:bool:0' \
		'bridges:list(string)' \
		'portals:list(string)'
}

inbound_section_validate() {
	uci_validate_section "$NAME" "inbound" "$1" \
		'port:or(port, portrange, string)' \
		'listen:ipaddr' \
		'protocol:string' \
		's_dokodemo_door_address:host' \
		's_dokodemo_door_port:port' \
		's_dokodemo_door_network:list(or("tcp", "udp"))' \
		's_dokodemo_door_timeout:uinteger' \
		's_dokodemo_door_follow_redirect:bool:0' \
		's_dokodemo_door_user_level:uinteger' \
		's_http_account_user:string' \
		's_http_account_pass:string' \
		's_http_allow_transparent:bool:0' \
		's_http_timeout:uinteger' \
		's_http_user_level:uinteger' \
		's_mtproto_user_email:string' \
		's_mtproto_user_secret:string' \
		's_mtproto_user_level:uinteger' \
		's_shadowsocks_email:string' \
		's_shadowsocks_method:string' \
		's_shadowsocks_password:string' \
		's_shadowsocks_level:uinteger' \
		's_shadowsocks_ota:bool:0' \
		's_shadowsocks_network:list(or("tcp", "udp")):tcp' \
		's_socks_auth:or("noauth", "password")' \
		's_socks_account_user:string' \
		's_socks_account_pass:string' \
		's_socks_udp:bool:0' \
		's_socks_ip:host' \
		's_socks_user_level:uinteger' \
		's_vmess_client_id:string' \
		's_vmess_client_alter_id:and(uinteger, max(65535))' \
		's_vmess_client_email:string' \
		's_vmess_client_user_level:uinteger' \
		's_vmess_default_alter_id:and(uinteger, max(65535))' \
		's_vmess_default_user_level:uinteger' \
		's_vmess_detour_to:string' \
		's_vmess_disable_insecure_encryption:bool:0' \
		's_vless_client_id:string' \
		's_vless_client_alter_id:and(uinteger, max(65535))' \
		's_vless_client_email:string' \
		's_vless_client_user_level:uinteger' \
		's_vless_default_alter_id:and(uinteger, max(65535))' \
		's_vless_default_user_level:uinteger' \
		's_vless_detour_to:string' \
		's_vless_disable_insecure_encryption:bool:0' \
		's_trojan_client_id:string' \
		's_trojan_client_alter_id:and(uinteger, max(65535))' \
		's_trojan_client_email:string' \
		's_trojan_client_user_level:uinteger' \
		's_trojan_default_alter_id:and(uinteger, max(65535))' \
		's_trojan_default_user_level:uinteger' \
		's_trojan_detour_to:string' \
		's_trojan_disable_insecure_encryption:bool:0' \
		's_socks_client_id:string' \
		's_socks_client_email:string:openmptcprouter' \
		'ss_network:or("tcp", "kcp", "ws", "http", "domainsocket", "quic")' \
		'ss_security:or("none", "tls")' \
		'ss_tls_server_name:host' \
		'ss_tls_alpn:string' \
		'ss_tls_allow_insecure:bool:0' \
		'ss_tls_allow_insecure_ciphers:bool:0' \
		'ss_tls_disable_system_root:bool:0' \
		'ss_tls_cert_usage:or("encipherment", "verify", "issue")' \
		'ss_tls_cert_file:string' \
		'ss_tls_key_file:string' \
		'ss_tcp_header_type:or("none", "http")' \
		'ss_tcp_header_request_version:string' \
		'ss_tcp_header_request_method:string:GET' \
		'ss_tcp_header_request_path:string' \
		'ss_tcp_header_request_headers:list(string)' \
		'ss_tcp_header_response_version:string' \
		'ss_tcp_header_response_status:string' \
		'ss_tcp_header_response_reason:string' \
		'ss_tcp_header_response_headers:list(string)' \
		'ss_kcp_mtu:and(min(576), max(1460))' \
		'ss_kcp_tti:and(min(10), max(100))' \
		'ss_kcp_uplink_capacity:uinteger' \
		'ss_kcp_downlink_capacity:uinteger' \
		'ss_kcp_congestion:bool:0' \
		'ss_kcp_read_buffer_size:uinteger' \
		'ss_kcp_write_buffer_size:uinteger' \
		'ss_kcp_header_type:or("none", "srtp", "utp", "wechat-video", "dtls", "wireguard")' \
		'ss_websocket_path:string' \
		'ss_websocket_headers:list(string)' \
		'ss_http_host:list(host)' \
		'ss_http_path:string' \
		'ss_domainsocket_path:string' \
		'ss_quic_security:or("aes-128-gcm", "chacha20-poly1305", "none")' \
		'ss_quic_key:string' \
		'ss_quic_header_type:or("none", "srtp", "utp", "wechat-video", "dtls", "wireguard")' \
		'ss_sockopt_tcp_fast_open:or("0", "1")' \
		'ss_sockopt_mptcp:or("0", "1")' \
		'ss_sockopt_tproxy:or("redirect", "tproxy", "off")' \
		'tag:string' \
		'sniffing_enabled:bool:0' \
		'sniffing_dest_override:list(or("http", "tls"))' \
		'allocate_strategy:or("always", "random")' \
		'allocate_refresh:uinteger' \
		'allocate_concurrency:uinteger'
}

outbound_section_validate() {
	uci_validate_section "$NAME" "outbound" "$1" \
		'send_through:ipaddr' \
		'protocol:string' \
		'tag:string' \
		's_blackhole_reponse_type:or("none", "http")' \
		's_dns_network:or("tcp", "udp")' \
		's_dns_address:string' \
		's_dns_port:port' \
		's_freedom_domain_strategy:or("AsIs", "UseIP", "UseIPv4", "UseIPv6")' \
		's_freedom_redirect:string' \
		's_freedom_user_level:uinteger' \
		's_http_server_address:host' \
		's_http_server_port:port' \
		's_http_account_user:string' \
		's_http_account_pass:string' \
		's_shadowsocks_email:string' \
		's_shadowsocks_address:host' \
		's_shadowsocks_port:port' \
		's_shadowsocks_method:string' \
		's_shadowsocks_password:string' \
		's_shadowsocks_level:uinteger' \
		's_shadowsocks_ota:bool:0' \
		's_socks_server_address:host' \
		's_socks_server_port:port' \
		's_socks_account_user:string' \
		's_socks_account_pass:string' \
		's_socks_user_level:uinteger' \
		's_vmess_address:host' \
		's_vmess_port:port' \
		's_vmess_user_id:string' \
		's_vmess_user_alter_id:and(uinteger, max(65535))' \
		's_vmess_user_security:or("auto", "aes-128-gcm", "chacha20-poly1305", "none")' \
		's_vmess_user_level:uinteger' \
		's_vless_address:host' \
		's_vless_port:port' \
		's_vless_user_id:string' \
		's_vless_user_alter_id:and(uinteger, max(65535))' \
		's_vless_user_security:or("auto", "aes-128-gcm", "chacha20-poly1305", "none")' \
		's_vless_user_encryption:or("auto", "none")' \
		's_vless_user_level:uinteger' \
		's_trojan_address:host' \
		's_trojan_port:port' \
		's_trojan_user_id:string' \
		's_trojan_user_alter_id:and(uinteger, max(65535))' \
		's_trojan_user_security:or("auto", "aes-128-gcm", "chacha20-poly1305", "none")' \
		's_trojan_user_encryption:or("auto", "none")' \
		's_trojan_user_level:uinteger' \
		's_socks_address:host' \
		's_socks_port:port' \
		's_socks_user_id:string' \
		's_socks_email:string:openmptcprouter' \
		'ss_network:or("tcp", "kcp", "ws", "http", "domainsocket", "quic")' \
		'ss_security:or("none", "tls")' \
		'ss_tls_server_name:host' \
		'ss_tls_alpn:string' \
		'ss_tls_allow_insecure:bool:0' \
		'ss_tls_allow_insecure_ciphers:bool:0' \
		'ss_tls_disable_system_root:bool:0' \
		'ss_tls_cert_usage:or("encipherment", "verify", "issue")' \
		'ss_tls_cert_file:string' \
		'ss_tls_key_file:string' \
		'ss_tls_pinned_sha256:string' \
		'ss_tcp_header_type:or("none", "http")' \
		'ss_tcp_header_request_version:string' \
		'ss_tcp_header_request_method:string:GET' \
		'ss_tcp_header_request_path:string' \
		'ss_tcp_header_request_headers:list(string)' \
		'ss_tcp_header_response_version:string' \
		'ss_tcp_header_response_status:string' \
		'ss_tcp_header_response_reason:string' \
		'ss_tcp_header_response_headers:list(string)' \
		'ss_kcp_mtu:and(min(576), max(1460))' \
		'ss_kcp_tti:and(min(10), max(100))' \
		'ss_kcp_uplink_capacity:uinteger' \
		'ss_kcp_downlink_capacity:uinteger' \
		'ss_kcp_congestion:bool:0' \
		'ss_kcp_read_buffer_size:uinteger' \
		'ss_kcp_write_buffer_size:uinteger' \
		'ss_kcp_header_type:or("none", "srtp", "utp", "wechat-video", "dtls", "wireguard")' \
		'ss_websocket_path:string' \
		'ss_websocket_headers:list(string)' \
		'ss_http_host:list(host)' \
		'ss_http_path:string' \
		'ss_domainsocket_path:string' \
		'ss_quic_security:or("aes-128-gcm", "chacha20-poly1305", "none")' \
		'ss_quic_key:string' \
		'ss_quic_header_type:or("none", "srtp", "utp", "wechat-video", "dtls", "wireguard")' \
		'ss_sockopt_mark:uinteger' \
		'ss_sockopt_tcp_fast_open:or("0", "1")' \
		'ss_sockopt_mptcp:or("0", "1")' \
		'stream_settings:string' \
		'proxy_settings_tag:string' \
		'mux_enabled:bool:0' \
		'mux_concurrency:uinteger:8'
}

# The public IPs of the VPS (openmptcprouter-vps _get_gre_tunnel): each has
# a transparent proxy oip<i>_transparent_proxy, redirecting to the inbound
# of the v2ray user of that IP. Which LAN sources use it is set in the
# shadowsocks rules of the same public IP (oip<i>_rule: Src ip/net forward,
# Ingress interfaces), shadowsocks-rust first.
v2ray_extra_rule_of() {
	local rule="${1%_transparent_proxy}_rule" pkg
	for pkg in shadowsocks-rust shadowsocks-libev; do
		[ -n "$(uci -q get "$pkg.$rule")" ] || continue
		[ "$(uci -q get "$pkg.$rule.disabled")" = "1" ] && continue
		echo "$pkg.$rule"
		return
	done
}

# IPv4 only, as the GRE tunnels
v2ray_extra_src() {
	uci -q get "$1.src_ips_forward" | tr ' ' '\n' \
		| grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$' | tr '\n' ' ' | sed 's/ *$//'
}

# Rendered in the main rules, ahead of the catch-all
v2ray_extra_json() {
	local tp rule src ifnames port udp
	json_add_array o_extra_rules
	for tp in $(uci -q show v2ray | sed -n "s/^v2ray\.\([^.=]*\)=transparent_proxy$/\1/p"); do
		[ "$tp" = "main_transparent_proxy" ] && continue
		port="$(uci -q get "v2ray.$tp.redirect_port")"
		[ -n "$port" ] || continue
		rule="$(v2ray_extra_rule_of "$tp")"
		[ -n "$rule" ] || continue
		src="$(v2ray_extra_src "$rule")"
		ifnames="$(uci -q get "$rule.ifnames")"
		[ -n "$src$ifnames" ] || continue
		udp=""
		[ "$(uci -q get "v2ray.$tp.redirect_udp")" = "1" ] && [ "$(uci -q get v2ray.omrout.protocol)" != "socks" ] && udp="$port"
		json_add_object ""
		json_add_string name "$tp"
		json_add_string src "$src"
		json_add_string ifnames "$ifnames"
		json_add_string tcp "$port"
		json_add_string udp "$udp"
		json_close_object
	done
	json_close_array
}

add_v2ray_redirect_rules() {
	local ext_args="$1"
	local lan_devices="$2"
	local lan_ipaddrs="$3"

	local port="$TRANSPARENT_PROXY_PORT"
	local addition="$TRANSPARENT_PROXY_ADDITION"
	local tproxy="$TRANSPARENT_PROXY_USE_TPROXY"
	local ipset_src_direct="$IPSET_SRC_DIRECT_V4"
	local ipset_dst_direct="$IPSET_DST_DIRECT_V4"

	test -n "$port" || return

	## This part need a rewrite
	#v2ray-rules -f
	##logger -t "v2ray" "v2ray-rules -l ${port} -L ${port} -s $OUTBOUND_SERVERS_V4 --rule-name def --src-default forward --dst-default forward --local-default forward"
	#commandline="-l ${port} -s $OUTBOUND_SERVERS_V4 --rule-name def --src-default forward --dst-default forward --local-default forward"
	#[ "$(uci -q get v2ray.main_transparent_proxy.redirect_udp)" = "1" ] && [ "$(uci -q get v2ray.omrout.protocol)" != "socks" ] && commandline="$commandline -L ${port}"
	#v2ray-rules $commandline
	#[ "$(uci -q get v2ray.main.inbounds | grep omr6)" != "" ] && [ -n "$OUTBOUND_SERVERS_V6" ] && {
	#	v2ray-rules6 -f
	#	commandline="-l $((port+1)) -L $((port+1)) -s $OUTBOUND_SERVERS_V6 --rule-name def --src-default forward --dst-default forward --local-default forward"
	#	[ "$(uci -q get v2ray.main_transparent_proxy.redirect_udp)" = "1" ] && ([ "$(uci -q get v2ray.omrout.protocol)" = "vless" ] || [ "$(uci -q get v2ray.omrout.protocol)" = "vmess" ]) && commandline="$commandline -L ${port+1}"
	#	v2ray-rules6 $commandline
	#}

	remote_servers="$(echo $OUTBOUND_SERVERS \
		| tr ' ' '\n' \
		| sort -u \
		| xargs -n 1 resolveip -t 5 \
		| sort -u)"

	[ "$(uci -q get v2ray.main_transparent_proxy.redirect_udp)" = "1" ] && [ "$(uci -q get v2ray.omrout.protocol)" != "socks" ] && portudp="$port"
	ifnames="$(uci -q get shadowsocks-libev.ss_rules.ifnames)"
	[ -z "$ifnames" ] && ifnames="$(uci -q get shadowsocks-rust.ss_rules.ifnames)"

	local tmpdir tmp
	tmpdir="$(mktemp -d /tmp/v2rrules.XXXXXX)" || return 0
	tmp="$tmpdir/rules"
	json_init
	json_add_string o_remote_servers "$remote_servers"
	json_add_int o_redir_tcp_port "$port"
	json_add_int o_redir_udp_port "$portudp"
	json_add_int o_tproxy "$tproxy"
	json_add_string o_ifnames "$ifnames"
	json_add_string o_local_default "forward"
	json_add_string o_src_bypass "$src_ips_bypass"
	json_add_string o_src_forward "$src_ips_forward"
	json_add_string o_src_checkdst "$src_ips_checkdst"
	json_add_string o_src_default "checkdst"
	json_add_string o_dst_bypass "$dst_ips_bypass"
	json_add_string o_dst_forward "$dst_ips_forward"
	json_add_string o_dst_bypass_file "$dst_ips_bypass_file"
	json_add_string o_dst_forward_file "$dst_ips_forward_file"
	json_add_string o_dst_default "forward"
	json_add_string o_nft_tcp_extra "$nft_tcp_extra"
	json_add_string o_nft_udp_extra "$nft_udp_extra"
	v2ray_extra_json
	json_dump -i >"$tmp.json"

	if utpl -S -F "$tmp.json" "$v2rrules_uc" >"$tmp.nft"; then
		grep -q tproxy "$tmp.nft" || v2ray_tproxy_route_del
		if ! cmp -s "$tmp.nft" "$v2rrules_nft"; then
			echo "table inet chk {include \"$tmp.nft\";}" >"$tmp.nft.chk"
			if nft -f "$tmp.nft.chk" -c; then
				mv "$tmp.nft" "$v2rrules_nft"
				fw4 -q reload
			fi
		fi
	fi
	rm -rf "$tmpdir"

	# Trailing space = the init script's own cmdline only. A bare "omr-bypass"
	# also matches /usr/lib/omr-bypass/ndpid-flows-tracker, which runs forever.
	[ -f /etc/init.d/omr-bypass ] && [ -z "$(pgrep -f '/etc/init.d/omr-bypass ')" ] && {
		logger -t "v2ray" "Reload omr-bypass rules"
		/etc/init.d/omr-bypass reload_rules
	}
}

init_rules_for_listfile() {
	local direct_list_dns="$1"
	local proxy_list_dns="$2"

	echo "# AUTO-GENERATED FILE. DO NOT MODIFY." >"$FILE_V2RAY_DNSMASQ_CACHE"

	# For direct list
	local direct_content
	direct_content="$(get_file_content "directlist")"

	if [ -n "$direct_content" ] ; then
		echo "$direct_content" | \
			grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?" | \
			_nft_set_populate "$IPSET_DST_DIRECT_V4" ipv4_addr

		echo "$direct_content" | \
			grep -oE "([0-9a-fA-F]{0,4}:){1,7}([0-9a-fA-F]){0,4}(/[0-9]{1,2})?" | \
			_nft_set_populate "$IPSET_DST_DIRECT_V6" ipv6_addr

		if [ -n "$direct_list_dns" ] ; then
			echo "$direct_content" | \
				grep -oE "([0-9a-zA-Z_-]+\.)+[a-zA-Z]{2,}$" | \
				sed "s|.*|server=/&/$direct_list_dns\nnftset=/&/4#inet#fw4#$IPSET_DST_DIRECT_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_DIRECT_V6|" \
				>>"$FILE_V2RAY_DNSMASQ_CACHE"
		else
			echo "$direct_content" | \
				grep -oE "([0-9a-zA-Z_-]+\.)+[a-zA-Z]{2,}$" | \
				sed "s|.*|nftset=/&/4#inet#fw4#$IPSET_DST_DIRECT_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_DIRECT_V6|" \
				>>"$FILE_V2RAY_DNSMASQ_CACHE"
		fi
	fi

	# For proxy list
	local proxy_content
	proxy_content="$(get_file_content "proxylist")"

	if [ -n "$proxy_content" ] ; then
		echo "$proxy_content" | \
			grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?" | \
			_nft_set_populate "$IPSET_DST_PROXY_V4" ipv4_addr

		echo "$proxy_content" | \
			grep -oE "([0-9a-fA-F]{0,4}:){1,7}([0-9a-fA-F]){0,4}(/[0-9]{1,2})?" | \
			_nft_set_populate "$IPSET_DST_PROXY_V6" ipv6_addr

		if [ -n "$proxy_list_dns" ] ; then
			echo "$proxy_content" | \
				grep -oE "([0-9a-zA-Z_-]+\.)+[a-zA-Z]{2,}$" | \
				sed "s|.*|server=/&/$proxy_list_dns\nnftset=/&/4#inet#fw4#$IPSET_DST_PROXY_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_PROXY_V6|" \
				>>"$FILE_V2RAY_DNSMASQ_CACHE"
		else
			echo "$proxy_content" | \
				grep -oE "([0-9a-zA-Z_-]+\.)+[a-zA-Z]{2,}$" | \
				sed "s|.*|nftset=/&/4#inet#fw4#$IPSET_DST_PROXY_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_PROXY_V6|" \
				>>"$FILE_V2RAY_DNSMASQ_CACHE"
		fi
	fi

  # For local devices outbound list
	local src_content
	src_content="$(get_file_content "srcdirectlist")"

	if [ -n "$src_content" ] ; then
		echo "$src_content" | \
			grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?" | \
			_nft_set_populate "$IPSET_SRC_DIRECT_V4" ipv4_addr
	fi

}

# rules_up/rules_down restart fw4 instead of reloading it: a reload keeps the
# chains of an include it no longer loads, and the next include declaring
# one of them with another type (TPROXY switched on or off) fails the whole
# reload. A restart creates table inet fw4 again, empty: the addresses
# dnsmasq added to the sets of a dhcp ipset section with domains
# (omr-bypass's, omr-dscp's, the user's) are gone, and dnsmasq adds them
# again only from an upstream answer, not from its cache. Its reload clears
# the cache, so the next query of each domain fills the sets again.
_fw4_restart() {
	fw4 -q restart
	[ -x /etc/init.d/dnsmasq ] || return 0
	uci -q show dhcp 2>/dev/null | awk -F= '
		/^dhcp\.[^.=]+=ipset$/ { s[substr($1, 6)] = 1; next }
		/^dhcp\.[^.=]+\.domain=/ {
			k = substr($1, 6); sub(/\..*$/, "", k)
			if (k in s) { f = 1; exit }
		}
		END { exit !f }' || return 0
	_info "Reloading dnsmasq to refill its sets"
	/etc/init.d/dnsmasq reload >/dev/null 2>&1
}

gracefully_restart_dnsmasq() {
	if [ "x$DNSMASQ_RESTART_EXPECTED" = "x1" ] && [ -x "/etc/init.d/dnsmasq" ] ; then
		_info "Restarting dnsmasq..."
		/etc/init.d/dnsmasq restart >/dev/null 2>&1
		DNSMASQ_RESTART_EXPECTED=0
	fi
}

add_dns_settings() {
	local section="${1}_dns"

	if ! dns_section_validate "$section" ; then
		_err "Invalid DNS config: $section, skip"
		return 1
	fi

	if [ "x$enabled" != "x1" ] ; then
		_info "DNS disabled: $section"
		return 0
	fi

	json_add_object "dns"

	test -n "$tag" && \
		json_add_string "tag" "$tag"
	test -n "$client_ip" && \
		json_add_string "clientIp" "$client_ip"

	if [ -n "$hosts" ] ; then
		json_add_object "hosts"

		local h
		for h in $hosts ; do
			local domain="$(echo "$h" | cut -d'|' -f1)"
			local ip="$(echo "$h" | cut -d'|' -f2)"

			if [ -n "$domain" ] && [ -n "$ip" ] ; then
				json_add_string "$domain" "$ip"
			fi
		done

		json_close_object # hosts
	fi

	if [ -n "$servers" ] ; then
		json_add_array "servers"

		for ss in $servers ; do
			if dns_server_section_validate "$ss" ; then
				if [ -z "$address" ] ; then
					continue
				fi

				if [ -z "${port}${domains}${expect_ips}" ] ; then
					json_add_string "" "$address"
				else
					json_add_object ""
					json_add_string "address" "$address"

					if [ -n "$port" ] ; then
						json_add_int "port" "$port"
					else
						json_add_int "port" "53"
					fi

					if [ -n "$domains" ] ; then
						json_add_array "domains"

						local d
						for d in $domains ; do
							json_add_string "" "$d"
						done

						json_close_array # domains
					fi

					if [ -n "$expect_ips" ] ; then
						json_add_array "expectIPs"

						local e
						for e in $expect_ips ; do
							json_add_string "" "$e"
						done

						json_close_array # expectIPs
					fi

					json_close_object
				fi
			fi
		done

		json_close_array # servers
	fi

	json_close_object # dns
}

add_routing_settings() {
	local section="${1}_routing"

	if ! routing_section_validate "$section" ; then
		_err "Invalid routing config: $section, skip"
		return 1
	fi

	if [ "x$enabled" != "x1" ] ; then
		_info "Routing disabled: $section"
		return 0
	fi

	json_add_object "routing"

	test -n "$domain_strategy" && \
		json_add_string "domainStrategy" "$domain_strategy"

	if [ -n "$rules" ] ; then
		json_add_array "rules"

		local rs
		for rs in $rules ; do
			if routing_rule_section_validate "$rs" ; then
				json_add_object ""

				json_add_string "type" "$type"

				if [ -n "$domain" ] ; then
					json_add_array "domain"

					local d
					for d in $domain ; do
						json_add_string "" "$d"
					done

					json_close_array # domain
				fi

				if [ -n "$ip" ] ; then
					json_add_array "ip"

					local i
					for i in $ip ; do
						json_add_string "" "$i"
					done

					json_close_array # ip
				fi

				if [ -n "$port" ] ; then
					json_add_string "port" "$(echo "$port" | tr -s ' ' ',')"
				fi

				if [ -n "$network" ] ; then
					json_add_string "network" "$(echo "$network" | tr -s ' ' ',')"
				fi

				if [ -n "$source" ] ; then
					json_add_array "source"

					local s
					for s in $source ; do
						json_add_string "" "$s"
					done

					json_close_array # source
				fi

				if [ -n "$user" ] ; then
					json_add_array "user"

					local u
					for u in $user ; do
						json_add_string "" "$u"
					done

					json_close_array # user
				fi

				if [ -n "$inbound_tag" ] ; then
					json_add_array "inboundTag"

					local it
					for it in $inbound_tag ; do
						json_add_string "" "$it"
					done

					json_close_array # inboundTag
				fi

				if [ -n "$protocol" ] ; then
					json_add_array "protocol"
					local p
					for p in $protocol ; do
						json_add_string "" "$p"
					done
					json_close_array # protocol
				fi

				test -n "$attrs" && \
					json_add_string "attrs" "$attrs"
				test -n "$outbound_tag" && \
					json_add_string "outboundTag" "$outbound_tag"
				test -n "$balancer_tag" && \
					json_add_string "balancerTag" "$balancer_tag"

				json_close_object
			fi
		done

		json_close_array # rules
	fi

	if [ -n "$balancers" ] ; then
		json_add_array "balancers"

		local bs
		for bs in $balancers ; do
			if routing_balancer_section_validate "$bs" ; then
				json_add_object ""
				json_add_string "tag" "$tag"

				json_add_array "selector"

				local s
				for s in $selector ; do
					json_add_string "" "$s"
				done

				json_close_array # selector
				json_close_object
			fi
		done

		json_close_array # balancers
	fi

	json_close_object
}

add_policy_settings() {
	local section="${1}_policy"

	if ! policy_section_validate "$section" ; then
		_err "Invalid policy config: $section, skip"
		return 1
	fi

	if [ "x$enabled" != "x1" ] ; then
		_info "Policy disabled: $section"
		return 0
	fi

	json_add_object "policy"

	if [ -n "$levels" ] ; then
		json_add_object "levels"

		local l_s
		for l_s in $levels ; do
			if policy_level_section_validate "$l_s" ; then
				json_add_object "$level"
				json_add_int "handshake" "$handshake"
				json_add_int "connIdle" "$conn_idle"
				json_add_int "uplinkOnly" "$uplink_only"
				json_add_int "downlinkOnly" "$downlink_only"
				json_add_boolean "statsUserUplink" "$stats_user_uplink"
				json_add_boolean "statsUserDownlink" "$stats_user_downlink"
				test -n "$buffer_size" && \
					json_add_int "bufferSize" "$buffer_size"
				json_close_object
			fi
		done

		json_close_object # levels
	fi

	json_add_object "system"
	json_add_boolean "statsInboundUplink" "$system_stats_inbound_uplink"
	json_add_boolean "statsInboundDownlink" "$system_stats_inbound_downlink"
	json_close_object # system

	json_close_object # policy
}

add_reverse_settings() {
	local section="${1}_reverse"

	if ! reverse_section_validate "$section" ; then
		_err "Invalid reverse config: $section, skip"
		return 1
	fi

	if [ "x$enabled" != "x1" ] ; then
		_info "Reverse disabled: $section"
		return 0
	fi

	json_add_object "reverse"

	if [ -n "$bridges" ] ; then
		json_add_array "bridges"

		local b
		for b in $bridges ; do
			local tag="$(echo "$b" | cut -d'|' -f1)"
			local domain="$(echo "$b" | cut -d'|' -f2)"
			if [ -n "$tag" ] && [ -n "$domain" ] ; then
				json_add_object ""
				json_add_string "tag" "$tag"
				json_add_string "domain" "$domain"
				json_close_object
			fi
		done

		json_close_array # bridges
	fi

	if [ -n "$portals" ] ; then
		json_add_array "portals"

		local p
		for p in $portals ; do
			local tag="$(echo "$p" | cut -d'|' -f1)"
			local domain="$(echo "$p" | cut -d'|' -f2)"
			if [ -n "$tag" ] && [ -n "$domain" ] ; then
				json_add_object ""
				json_add_string "tag" "$tag"
				json_add_string "domain" "$domain"
				json_close_object
			fi
		done

		json_close_array # portals
	fi

	json_close_object # reverse
}

add_transport_settings() {
	local json
	json="$(get_file_content "transport")"

	if [ -z "$json" ] ; then
		_err "Invalid transport config: $key"
		return 1
	fi

	json_add_object "transport"
	eval "$(get_commands_from_json "$json")"
	json_close_object # transport
}

add_inbound_setting() {
	local section="$1"

	if ! inbound_section_validate "$section" ; then
		_err "Invalid inbound section: $section"
		return 1
	fi

	json_add_object ""

	test -n "$listen" && \
		json_add_string "listen" "$listen"
	json_add_int "port" "$port"
	json_add_string "protocol" "$protocol"

	case "${protocol:-x}" in
		"dokodemo-door")
			json_add_object "settings"

			if [ -n "$port" ] && [ "x$port" = "x$TRANSPARENT_PROXY_PORT" ] ; then
				local settings_network="tcp"

				test -n "$TRANSPARENT_PROXY_ADDITION" && \
					settings_network="$settings_network,udp"

				json_add_boolean "followRedirect" "1"
				json_add_string "network" "$settings_network"
			else
				test -n "$s_dokodemo_door_address" && \
					json_add_string "address" "$s_dokodemo_door_address"

				test -n "$s_dokodemo_door_port" && \
					json_add_int "port" "$s_dokodemo_door_port"

				test -n "$s_dokodemo_door_follow_redirect" && \
					json_add_boolean "followRedirect" "$s_dokodemo_door_follow_redirect"

				test -n "$s_dokodemo_door_network" && \
					json_add_string "network" "$(echo "$s_dokodemo_door_network" | tr -s ' ' ',')"
			fi

			test -n "$s_dokodemo_door_timeout" && \
				json_add_int "timeout" "$s_dokodemo_door_timeout"

			test -n "$s_dokodemo_door_user_level" && \
				json_add_int "userLevel" "$s_dokodemo_door_user_level"

			json_close_object # settings
			;;
		"http")
			json_add_object "settings"

			if [ -n "$s_http_account_user" ] ; then
				json_add_array "accounts"

				json_add_object ""
				json_add_string "user" "$s_http_account_user"
				json_add_string "pass" "$s_http_account_pass"
				json_close_object

				json_close_array # accounts
			fi

			json_add_boolean "allowTransparent" "$s_http_allow_transparent"

			test -n "$s_http_timeout" && \
				json_add_int "timeout" "$s_http_timeout"
			test -n "$s_http_user_level" && \
				json_add_int "userLevel" "$s_http_user_level"

			json_close_object # settings
			;;
		"mtproto")
			json_add_object "settings"

			if [ -n "$s_mtproto_user_email" ] ; then
				json_add_array "users"
				json_add_object ""

				json_add_string "email" "$s_mtproto_user_email"
				json_add_string "secret" "$s_mtproto_user_secret"

				test -n "$s_mtproto_user_level" && \
					json_add_int "level" "$s_mtproto_user_level"

				json_close_object
				json_close_array # users
			fi

			json_close_object # settings
			;;
		"shadowsocks")
			json_add_object "settings"

			json_add_string "method" "$s_shadowsocks_method"
			json_add_string "password" "$s_shadowsocks_password"

			test -n "$s_shadowsocks_email" && \
				json_add_string "email" "$s_shadowsocks_email"
			test -n "$s_shadowsocks_level" && \
				json_add_int "level" "$s_shadowsocks_level"

			json_add_boolean "ota" "$s_shadowsocks_ota"
			json_add_string "network" "$(echo "$s_shadowsocks_network" | tr -s ' ' ',')"

			json_close_object # settings
			;;
		"socks")
			json_add_object "settings"

			json_add_string "auth" "$s_socks_auth"

			if [ -n "$s_socks_account_user" ] ; then
				json_add_array "accounts"
				json_add_object ""
				json_add_string "user" "$s_socks_account_user"
				json_add_string "pass" "$s_socks_account_pass"
				json_close_object
				json_close_array # accounts
			fi
			if [ -n "$s_socks_client_id" ] ; then
				json_add_array "accounts"
				json_add_object ""
				json_add_string "user" "$s_socks_client_email"
				json_add_string "pass" "$s_socks_client_id"
				json_close_object
				json_close_array # accounts
			fi

			json_add_boolean "udp" "$s_socks_udp"

			test -n "$s_socks_ip" && \
				json_add_string "ip" "$s_socks_ip"
			test -n "$s_socks_user_level" && \
				json_add_int "userLevel" "$s_socks_user_level"

			json_close_object # settings
			;;
		"vmess")
			json_add_object "settings"

			if [ -n "$s_vmess_client_id" ] ; then
				json_add_array "clients"
				json_add_object ""

				json_add_string "id" "$s_vmess_client_id"

				test -n "$s_vmess_client_alter_id" && \
					json_add_int "alterId" "$s_vmess_client_alter_id"
				test -n "$s_vmess_client_email" && \
					json_add_string "email" "$s_vmess_client_email"
				test -n "$s_vmess_client_user_level" && \
					json_add_int "level" "$s_vmess_client_user_level"

				json_close_object
				json_close_array # clients
			fi

			json_add_object "default"

			test -n "$s_vmess_default_alter_id" && \
				json_add_int "alterId" "$s_vmess_default_alter_id"
			test -n "$s_vmess_default_user_level" && \
				json_add_int "level" "$s_vmess_default_user_level"

			json_close_object # default

			if [ -n "$s_vmess_detour_to" ] ; then
				json_add_object "detour"
				json_add_string "to" "$s_vmess_detour_to"
				json_close_object # detour
			fi

			json_add_boolean "disableInsecureEncryption" "$s_vmess_disable_insecure_encryption"

			json_close_object # settings
			;;
		"vless")
			json_add_object "settings"

			if [ -n "$s_vless_client_id" ] ; then
				json_add_array "clients"
				json_add_object ""

				json_add_string "id" "$s_vless_client_id"

				test -n "$s_vless_client_alter_id" && \
					json_add_int "alterId" "$s_vless_client_alter_id"
				test -n "$s_vless_client_email" && \
					json_add_string "email" "$s_vless_client_email"
				test -n "$s_vless_client_user_level" && \
					json_add_int "level" "$s_vless_client_user_level"

				json_close_object
				json_close_array # clients
			fi

			json_add_object "default"

			test -n "$s_vless_default_alter_id" && \
				json_add_int "alterId" "$s_vless_default_alter_id"
			test -n "$s_vless_default_user_level" && \
				json_add_int "level" "$s_vless_default_user_level"

			json_close_object # default

			if [ -n "$s_vless_detour_to" ] ; then
				json_add_object "detour"
				json_add_string "to" "$s_vless_detour_to"
				json_close_object # detour
			fi

			json_add_boolean "disableInsecureEncryption" "$s_vless_disable_insecure_encryption"

			json_close_object # settings
			;;
		"trojan")
			json_add_object "settings"

			if [ -n "$s_trojan_client_id" ] ; then
				json_add_array "clients"
				json_add_object ""

				json_add_string "password" "$s_trojan_client_id"

				test -n "$s_trojan_client_alter_id" && \
					json_add_int "alterId" "$s_trojan_client_alter_id"
				test -n "$s_trojan_client_email" && \
					json_add_string "email" "$s_trojan_client_email"
				test -n "$s_trojan_client_user_level" && \
					json_add_int "level" "$s_trojan_client_user_level"

				json_close_object
				json_close_array # clients
			fi

			json_add_object "default"

			test -n "$s_trojan_default_alter_id" && \
				json_add_int "alterId" "$s_trojan_default_alter_id"
			test -n "$s_trojan_default_user_level" && \
				json_add_int "level" "$s_trojan_default_user_level"

			json_close_object # default

			if [ -n "$s_trojan_detour_to" ] ; then
				json_add_object "detour"
				json_add_string "to" "$s_trojan_detour_to"
				json_close_object # detour
			fi

			json_add_boolean "disableInsecureEncryption" "$s_trojan_disable_insecure_encryption"

			json_close_object # settings
			;;
	esac

	json_add_object "streamSettings"

	test -n "$ss_network" && \
		json_add_string "network" "$ss_network"

	test -n "$ss_security" && \
		json_add_string "security" "$ss_security"

	if [ "x$ss_security" = "xtls" ] ; then
		json_add_object "tlsSettings"

		test -n "$ss_tls_server_name" && \
			json_add_string "serverName" "$ss_tls_server_name"

		if [ -n "$ss_tls_alpn" ] ; then
			json_add_array "alpn"
			json_add_string "" "$ss_tls_alpn"
			json_close_array # alpn
		fi

		json_add_boolean "allowInsecure" "$ss_tls_allow_insecure"
		json_add_boolean "allowInsecureCiphers" "$ss_tls_allow_insecure_ciphers"
		json_add_boolean "disableSystemRoot" "$ss_tls_disable_system_root"

		json_add_array "certificates"
		if [ -n "$ss_tls_cert_file" ] ; then
			json_add_object ""

			json_add_string "certificateFile" "$ss_tls_cert_file"
			json_add_string "keyFile" "$ss_tls_key_file"
			test -n "$ss_tls_cert_usage" && \
				json_add_string "usage" "$ss_tls_cert_usage"

			json_close_object
		fi
		json_close_array # certificates

		json_close_object # tlsSettings
	fi

	case "${ss_network:-x}" in
		"tcp")
			json_add_object "tcpSettings"

			if [ -n "$ss_tcp_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_tcp_header_type"

				if [ "$ss_tcp_header_type" = "http" ] ; then
					json_add_object "request"
					test -n "$ss_tcp_header_request_version" && \
						json_add_string "version" "$ss_tcp_header_request_version"
					json_add_string "method" "$ss_tcp_header_request_method"

					if [ -n "$ss_tcp_header_request_path" ] ; then
						json_add_array "path"
						json_add_string "" "$ss_tcp_header_request_path"
						json_close_array # path
					fi

					if [ -n "$ss_tcp_header_request_headers" ] ; then
						json_add_object "headers"

						handle_request_header() {
							local h="$1"

							local name="$(echo "$h" | cut -d'=' -f1)"
							local value="$(echo "$h" | cut -d'=' -f2)"

							if [ -n "$name" ] && [ -n "$value" ] ; then
								json_add_array "$name"
								json_add_string "" "$value"
								json_close_array
							fi
						}
						config_list_foreach "$section" "ss_tcp_header_request_headers" handle_request_header

						json_close_object # headers
					fi

					json_close_object # request

					json_add_object "response"

					test -n "$ss_tcp_header_response_version" && \
						json_add_string "version" "$ss_tcp_header_response_version"
					test -n "$ss_tcp_header_response_status" && \
						json_add_string "status" "$ss_tcp_header_response_status"
					test -n "$ss_tcp_header_response_reason" && \
						json_add_string "reason" "$ss_tcp_header_response_reason"

					if [ -n "$ss_tcp_header_response_headers" ] ; then
						json_add_object "headers"

						handle_response_header() {
							local h="$1"

							local name="$(echo "$h" | cut -d'=' -f1)"
							local value="$(echo "$h" | cut -d'=' -f2)"

							if [ -n "$name" ] && [ -n "$value" ] ; then
								json_add_array "$name"
								json_add_string "" "$value"
								json_close_array
							fi
						}
						config_list_foreach "$section" "ss_tcp_header_response_headers" handle_response_header

						json_close_object # headers
					fi

					json_close_object # response
				fi
				json_close_object # header
			fi

			json_close_object # tcpSettings
			;;
		"kcp")
			json_add_object "kcpSettings"

			test -n "$ss_kcp_mtu" && \
				json_add_int "mtu" "$ss_kcp_mtu"
			test -n "$ss_kcp_tti" && \
				json_add_int "tti" "$ss_kcp_tti"
			test -n "$ss_kcp_uplink_capacity" && \
				json_add_int "uplinkCapacity" "$ss_kcp_uplink_capacity"
			test -n "$ss_kcp_downlink_capacity" && \
				json_add_int "downlinkCapacity" "$ss_kcp_downlink_capacity"
			json_add_boolean "congestion" "$ss_kcp_congestion"
			test -n "$ss_kcp_read_buffer_size" && \
				json_add_int "readBufferSize" "$ss_kcp_read_buffer_size"
			test -n "$ss_kcp_write_buffer_size" && \
				json_add_int "writeBufferSize" "$ss_kcp_write_buffer_size"

			if [ -n "$ss_kcp_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_kcp_header_type"
				json_close_object # header
			fi

			json_close_object # kcpSettings
			;;
		"ws")
			json_add_object "wsSettings"

			test -n "$ss_websocket_path" && \
				json_add_string "path" "$ss_websocket_path"

			if [ -n "$ss_websocket_headers" ] ; then
				json_add_object "headers"

				handle_websocket_header() {
					local h="$1"

					local name="$(echo "$h" | cut -d'=' -f1)"
					local value="$(echo "$h" | cut -d'=' -f2)"

					if [ -n "$name" ] && [ -n "$value" ] ; then
						json_add_string "$name" "$value"
					fi
				}
				config_list_foreach "$section" "ss_websocket_headers" handle_websocket_header

				json_close_object # headers
			fi

			json_close_object # wsSettings
			;;
		"http")
			json_add_object "httpSettings"

			if [ -n "$ss_http_host" ] ; then
				json_add_array "host"

				local h
				for h in $ss_http_host ; do
					json_add_string "" "$h"
				done

				json_close_array # host
			fi

			test -n "$ss_http_path" && \
				json_add_string "path" "$ss_http_path"

			json_close_object # httpSettings
			;;
		"domainsocket")
			json_add_object "dsSettings"

			test -n "$ss_domainsocket_path" && \
				json_add_string "path" "$ss_domainsocket_path"

			json_close_object # dsSettings
			;;
		"quic")
			json_add_object "quicSettings"

			test -n "$ss_quic_security" && \
				json_add_string "security" "$ss_quic_security"
			test -n "$ss_quic_key" && \
				json_add_string "key" "$ss_quic_key"

			if [ -n "$ss_quic_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_quic_header_type"
				json_close_object # header
			fi

			json_close_object # quicSettings
			;;
	esac

	json_add_object "sockopt"

#	if [ -n "$port" ] && [ "x$port" = "x$TRANSPARENT_PROXY_PORT" ] ; then
#		if [ "x$TRANSPARENT_PROXY_USE_TPROXY" = "x1" ] ; then
#			json_add_string "tproxy" "tproxy"
#		else
#			json_add_string "tproxy" "redirect"
#		fi
#	else
		test -n "$ss_sockopt_tcp_fast_open" && \
			json_add_boolean "tcpFastOpen" "$ss_sockopt_tcp_fast_open"
		test -n "$ss_sockopt_mptcp" && \
			json_add_boolean "mptcp" "$ss_sockopt_mptcp"
		test -n "$ss_sockopt_tproxy" && \
			json_add_string "tproxy" "$ss_sockopt_tproxy"
#	fi

	json_close_object # sockopt

	json_close_object # streamSettings

	test -n "$tag" && \
		json_add_string "tag" "$tag"

	json_add_object "sniffing"

	json_add_boolean "enabled" "$sniffing_enabled"

	if [ -n "$sniffing_dest_override" ] ; then
		json_add_array "destOverride"
		local d
		for d in $sniffing_dest_override ; do
			json_add_string "" "$d"
		done
		json_close_array # destOverride
	fi

	json_close_object # sniffing

	if [ -n "$allocate_strategy" ] ; then
		json_add_object "allocate"
		json_add_string "strategy" "$allocate_strategy"
		test -n "$allocate_refresh" && \
			json_add_int "refresh" "$allocate_refresh"
		test -n "$allocate_concurrency" &&	\
			json_add_int "concurrency" "$allocate_concurrency"
		json_close_object # allocate
	fi

	json_close_object
}

add_outbound_setting() {
	local section="$1"

	if ! outbound_section_validate "$section" ; then
		_err "Invalid outbound section: $section"
		return 1
	fi

	json_add_object ""

	test -n "$send_through" && \
		json_add_string "sendThrough" "$send_through"
	json_add_string "protocol" "$protocol"

	case "${protocol:-x}" in
		"blackhole")
			json_add_object "settings"

			if [ -n "$s_blackhole_reponse_type" ] ; then
				json_add_object "response"
				json_add_string "type" "$s_blackhole_reponse_type"
				json_close_object # response
			fi

			json_close_object # settings
			;;
		"dns")
			json_add_object "settings"

			test -n "$s_dns_network" && \
				json_add_string "network" "$s_dns_network"

			if [ -n "$s_dns_address" ] ; then
				json_add_string "address" "$s_dns_address"
				append_server_address "$s_dns_address"
			fi

			test -n "$s_dns_port" && \
				json_add_int "port" "$s_dns_port"

			json_close_object # settings
			;;
		"freedom")
			json_add_object "settings"

			test -n "$s_freedom_domain_strategy" && \
				json_add_string "domainStrategy" "$s_freedom_domain_strategy"
			test -n "$s_freedom_redirect" && \
				json_add_string "redirect" "$s_freedom_redirect"
			test -n "$s_freedom_user_level" && \
				json_add_int "userLevel" "$s_freedom_user_level"

			json_close_object # settings
			;;
		"http")
			json_add_object "settings"
			json_add_array "servers"

			json_add_object ""

			json_add_string "address" "$s_http_server_address"
			append_server_address "$s_http_server_address"

			test -n "$s_http_server_port" && \
				json_add_int "port" "$s_http_server_port"

			if [ -n "$s_http_account_user" ] ; then
				json_add_array "users"
				json_add_object ""

				json_add_string "user" "$s_http_account_user"
				json_add_string "pass" "$s_http_account_pass"

				json_close_object
				json_close_array # users
			fi
			json_close_object

			json_close_array # servers
			json_close_object # settings
			;;
		"mtproto")
			json_add_object "settings"
			json_close_object
			;;
		"shadowsocks")
			json_add_object "settings"
			json_add_array "servers"

			json_add_object ""
			test -n "$s_shadowsocks_email" && \
				json_add_string "email" "$s_shadowsocks_email"
			json_add_string "address" "$s_shadowsocks_address"
			append_server_address "$s_shadowsocks_address"

			json_add_int "port" "$s_shadowsocks_port"
			json_add_string "method" "$s_shadowsocks_method"
			json_add_string "password" "$s_shadowsocks_password"

			test -n "$s_shadowsocks_level" && \
				json_add_int "level" "$s_shadowsocks_level"
			json_add_boolean "ota" "$s_shadowsocks_ota"
			json_close_object

			json_close_array # servers
			json_close_object # settings
			;;
		"vmess")
			json_add_object "settings"

			json_add_array "vnext"
			json_add_object ""

			json_add_string "address" "$s_vmess_address"
			append_server_address "$s_vmess_address"

			json_add_int "port" "$s_vmess_port"

			json_add_array "users"
			json_add_object ""
			json_add_string "id" "$s_vmess_user_id"
			json_add_int "alterId" "$s_vmess_user_alter_id"
			test -n "$s_vmess_user_security" && \
				json_add_string "security" "$s_vmess_user_security"
			test -n "$s_vmess_user_level" && \
				json_add_int "level" "$s_vmess_user_level"
			json_close_object
			json_close_array # users

			json_close_object

			json_close_array # vnext
			json_close_object # settings
			;;
		"vless")
			json_add_object "settings"

			json_add_array "vnext"
			json_add_object ""

			json_add_string "address" "$s_vless_address"
			append_server_address "$s_vless_address"

			json_add_int "port" "$s_vless_port"

			json_add_array "users"
			json_add_object ""
			json_add_string "id" "$s_vless_user_id"
			json_add_int "alterId" "$s_vless_user_alter_id"
			test -n "$s_vless_user_security" && \
				json_add_string "security" "$s_vless_user_security"
			# "none" is the only value the validator allows that v2ray
			# takes for VLESS: "auto" or no encryption refuses the config
			json_add_string "encryption" "none"
			test -n "$s_vless_user_level" && \
				json_add_int "level" "$s_vless_user_level"
			json_close_object
			json_close_array # users

			json_close_object

			json_close_array # vnext
			json_close_object # settings
			;;
		"trojan")
			json_add_object "settings"

			json_add_array "servers"
			json_add_object ""

			json_add_string "address" "$s_trojan_address"
			append_server_address "$s_trojan_address"

			json_add_int "port" "$s_trojan_port"

			json_add_string "password" "$s_trojan_user_id"

			json_close_object

			json_close_array # trojan
			json_close_object # settings
			;;
		"socks")
			json_add_object "settings"

			json_add_array "servers"
			json_add_object ""

			json_add_string "address" "$s_socks_address"
			append_server_address "$s_socks_address"

			json_add_int "port" "$s_socks_port"

			json_add_array "users"
			json_add_object ""
			json_add_string "pass" "$s_socks_user_id"
			json_add_string "user" "$s_socks_email"
			json_close_object
			json_close_array # users

			json_close_object

			json_close_array # socks
			json_close_object # settings
			;;
	esac

	json_add_object "streamSettings"
	test -n "$ss_network" && \
		json_add_string "network" "$ss_network"

	test -n "$ss_security" && \
		json_add_string "security" "$ss_security"

	if [ "x$ss_security" = "xtls" ] ; then
		json_add_object "tlsSettings"

		test -n "$ss_tls_server_name" && \
			json_add_string "serverName" "$ss_tls_server_name"

		if [ -n "$ss_tls_alpn" ] ; then
			json_add_array "alpn"
			json_add_string "" "$ss_tls_alpn"
			json_close_array
		fi

		# allowInsecure is how OMR reaches its VPS (a CN only certificate):
		# with nothing pinned, any certificate would do
		local _allow_insecure="$ss_tls_allow_insecure" _pinned_chain="" _pin_addr="" _pin_port="" _pin
		if [ -n "$ss_tls_pinned_sha256" ] ; then
			# The pins name the certificates to accept. v2ray still checks
			# the chain and the name unless allowInsecure, which a CN only
			# certificate fails, so the pins replace them as xray's do
			_pinned_chain="$(_tls_pin_list base64 "$ss_tls_pinned_sha256")"
			[ -n "$_pinned_chain" ] && _allow_insecure=1
		fi
		# Every outbound, as xray: openmptcprouter-vps copies omrout, its
		# allowInsecure included, into the tunnel of each other public IP of
		# the VPS (oip<i>server_tunnel). A server the VPS CA did not certify
		# needs ss_tls_pinned_sha256 to keep allowInsecure
		if [ -z "$_pinned_chain" ] && [ "$_allow_insecure" = "1" ] ; then
			case "$protocol" in
				vless)  _pin_addr="$s_vless_address"  ; _pin_port="$s_vless_port" ;;
				vmess)  _pin_addr="$s_vmess_address"  ; _pin_port="$s_vmess_port" ;;
				trojan) _pin_addr="$s_trojan_address" ; _pin_port="$s_trojan_port" ;;
				socks)  _pin_addr="$s_socks_address"  ; _pin_port="$s_socks_port" ;;
			esac
			if [ -n "$_pin_addr" ] && [ -n "$_pin_port" ] ; then
				_pinned_chain="$(_tls_vps_pin chain "$_pin_addr" "$_pin_port" "$ss_tls_server_name")"
				[ "${#_pinned_chain}" -ne 44 ] && _pinned_chain=""
				if [ -z "$_pinned_chain" ] ; then
					_err "$section: ${_pin_addr}:${_pin_port} certificate not verified by /etc/luci-uploads/ca.crt, allowInsecure ignored (set ss_tls_pinned_sha256 for another server)"
					_tls_pin_pending "$_pin_addr" "$_pin_port" "$ss_tls_server_name"
				fi
			fi
			# no server to pin (yet): nothing to retry, and no certificate
			# accepted unchecked either
			[ -n "$_pinned_chain" ] || _allow_insecure=0
		fi

		json_add_boolean "allowInsecure" "$_allow_insecure"
		json_add_boolean "allowInsecureCiphers" "$ss_tls_allow_insecure_ciphers"
		json_add_boolean "disableSystemRoot" "$ss_tls_disable_system_root"

		if [ -n "$_pinned_chain" ] ; then
			json_add_array "pinnedPeerCertificateChainSha256"
			for _pin in $_pinned_chain ; do
				json_add_string "" "$_pin"
			done
			json_close_array
		fi

		json_add_array "certificates"
		if [ -n "$ss_tls_cert_file" ] ; then
			json_add_object ""
			json_add_string "certificateFile" "$ss_tls_cert_file"
			json_add_string "keyFile" "$ss_tls_key_file"
			test -n "$ss_tls_cert_usage" && \
				json_add_string "usage" "$ss_tls_cert_usage"
			json_close_object
		fi
		json_close_array # certificates

		json_close_object # tlsSettings
	fi

	case "${ss_network:-x}" in
		"tcp")
			json_add_object "tcpSettings"

			if [ -n "$ss_tcp_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_tcp_header_type"

				if [ "$ss_tcp_header_type" = "http" ] ; then
					json_add_object "request"
					test -n "$ss_tcp_header_request_version" && \
						json_add_string "version" "$ss_tcp_header_request_version"
					json_add_string "method" "$ss_tcp_header_request_method"

					if [ -n "$ss_tcp_header_request_path" ] ; then
						json_add_array "path"
						json_add_string "" "$ss_tcp_header_request_path"
						json_close_array
					fi

					if [ -n "$ss_tcp_header_request_headers" ] ; then
						json_add_object "headers"

						handle_request_header() {
							local h="$1"

							local name="$(echo "$h" | cut -d'=' -f1)"
							local value="$(echo "$h" | cut -d'=' -f2)"

							if [ -n "$name" ] && [ -n "$value" ] ; then
								json_add_array "$name"
								json_add_string "" "$value"
								json_close_array
							fi
						}
						config_list_foreach "$section" "ss_tcp_header_request_headers" handle_request_header

						json_close_object # headers
					fi

					json_close_object # request

					json_add_object "response"
					test -n "$ss_tcp_header_response_version" && \
						json_add_string "version" "$ss_tcp_header_response_version"
					test -n "$ss_tcp_header_response_status" && \
						json_add_string "status" "$ss_tcp_header_response_status"
					test -n "$ss_tcp_header_response_reason" && \
						json_add_string "reason" "$ss_tcp_header_response_reason"

					if [ -n "$ss_tcp_header_response_headers" ] ; then
						json_add_object "headers"

						handle_response_header() {
							local h="$1"

							local name="$(echo "$h" | cut -d'=' -f1)"
							local value="$(echo "$h" | cut -d'=' -f2)"

							if [ -n "$name" ] && [ -n "$value" ] ; then
								json_add_array "$name"
								json_add_string "" "$value"
								json_close_array
							fi
						}
						config_list_foreach "$section" "ss_tcp_header_response_headers" handle_response_header

						json_close_object # headers
					fi

					json_close_object # response
				fi

				json_close_object # header
			fi

			json_close_object # tcpSettings
			;;
		"kcp")
			json_add_object "kcpSettings"

			test -n "$ss_kcp_mtu" && \
				json_add_int "mtu" "$ss_kcp_mtu"
			test -n "$ss_kcp_tti" && \
				json_add_int "tti" "$ss_kcp_tti"
			test -n "$ss_kcp_uplink_capacity" && \
				json_add_int "uplinkCapacity" "$ss_kcp_uplink_capacity"
			test -n "$ss_kcp_downlink_capacity" && \
				json_add_int "downlinkCapacity" "$ss_kcp_downlink_capacity"

			json_add_boolean "congestion" "$ss_kcp_congestion"

			test -n "$ss_kcp_read_buffer_size" && \
				json_add_int "readBufferSize" "$ss_kcp_read_buffer_size"
			test -n "$ss_kcp_write_buffer_size" && \
				json_add_int "writeBufferSize" "$ss_kcp_write_buffer_size"

			if [ -n "$ss_kcp_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_kcp_header_type"
				json_close_object
			fi

			json_close_object # kcpSettings
			;;
		"ws")
			json_add_object "wsSettings"

			test -n "$ss_websocket_path" && \
				json_add_string "path" "$ss_websocket_path"

			if [ -n "$ss_websocket_headers" ] ; then
				json_add_object "headers"

				handle_websocket_header() {
					local h="$1"

					local name="$(echo "$h" | cut -d'=' -f1)"
					local value="$(echo "$h" | cut -d'=' -f2)"

					if [ -n "$name" ] && [ -n "$value" ] ; then
						json_add_string "$name" "$value"
					fi
				}
				config_list_foreach "$section" "ss_websocket_headers" handle_websocket_header

				json_close_object # headers
			fi

			json_close_object # wsSettings
			;;
		"http")
			json_add_object "httpSettings"

			if [ -n "$ss_http_host" ] ; then
				json_add_array "host"

				local h
				for h in $ss_http_host ; do
					json_add_string "" "$h"
				done

				json_close_array # host
			fi

			test -n "$ss_http_path" && \
				json_add_string "path" "$ss_http_path"

			json_close_object # httpSettings
			;;
		"domainsocket")
			json_add_object "dsSettings"

			test -n "$ss_domainsocket_path" && \
				json_add_string "path" "$ss_domainsocket_path"

			json_close_object # dsSettings
			;;
		"quic")
			json_add_object "quicSettings"

			test -n "$ss_quic_security" && \
				json_add_string "security" "$ss_quic_security"
			test -n "$ss_quic_key" && \
				json_add_string "key" "$ss_quic_key"

			if [ -n "$ss_quic_header_type" ] ; then
				json_add_object "header"
				json_add_string "type" "$ss_quic_header_type"
				json_close_object # header
			fi

			json_close_object # quicSettings
			;;
	esac

	json_add_object "sockopt"

	if [ -n "$TRANSPARENT_PROXY_PORT" ] ; then
		json_add_int "mark" "255"
	else
		test -n "$ss_sockopt_mark" && \
			json_add_int "mark" "$ss_sockopt_mark"
	fi

	test -n "$ss_sockopt_tcp_fast_open" && \
		json_add_boolean "tcpFastOpen" "$ss_sockopt_tcp_fast_open"
	test -n "$ss_sockopt_mptcp" && \
		json_add_boolean "mptcp" "$ss_sockopt_mptcp"

	json_close_object # sockopt

	json_close_object # streamSettings

	test -n "$tag" && \
		json_add_string "tag" "$tag"

	if [ -n "$proxy_settings_tag" ] ; then
		json_add_object "proxySettings"
		json_add_string "tag" "$proxy_settings_tag"
		json_close_object # proxySettings
	fi

	if [ "x$mux_enabled" = "x1" ] ; then
		json_add_object "mux"
		json_add_boolean "enabled" "1"
		json_add_int "concurrency" "$mux_concurrency"
		json_close_object # mux
	fi

	json_close_object
}

init_transparent_proxy() {
	local tp_cfg="main_transparent_proxy"
	local redirect_port use_tproxy redirect_udp redirect_dns

	config_get redirect_port "$tp_cfg" "redirect_port"
	config_get_bool use_tproxy "$tp_cfg" "use_tproxy" "0"
	config_get_bool redirect_udp "$tp_cfg" "redirect_udp" "0"
	config_get_bool redirect_dns "$tp_cfg" "redirect_dns" "0"

	if [ -n "$redirect_port" ] && \
		! validate_data "port" "$redirect_port" 2>/dev/null ; then
		_err "Transparent proxy redirect port is invalid: $redirect_port"
		return 1
	fi

	TRANSPARENT_PROXY_PORT="$redirect_port"
	TRANSPARENT_PROXY_USE_TPROXY="$use_tproxy"

	if [ "x$redirect_udp" = "x1" ] && [ "$(uci -q get v2ray.omrout.protocol)" != "socks" ]; then
		TRANSPARENT_PROXY_ADDITION="udp"
	elif [ "x$redirect_dns" = "x1" ] ; then
		TRANSPARENT_PROXY_ADDITION="dns"
	else
		TRANSPARENT_PROXY_ADDITION=
	fi
}

setup_transparent_proxy() {
	if [ -z "$TRANSPARENT_PROXY_PORT" ] ; then
		#_info "Transparent proxy disabled."
		return 0
	fi

	if [ "x$TRANSPARENT_PROXY_EXPECTED" != "x1" ] ; then
		#_info "No v2ray instance enabled, skip transparent proxy."
		return 0
	fi

	_info "Setting transparent proxy on port: $TRANSPARENT_PROXY_PORT"

	local tp_cfg="main_transparent_proxy"
	local lan_ifaces only_privileged_ports proxy_mode direct_list_dns proxy_list_dns

	config_get lan_ifaces "$tp_cfg" "lan_ifaces"
	config_get_bool only_privileged_ports "$tp_cfg" "only_privileged_ports" "0"
	config_get proxy_mode "$tp_cfg" "proxy_mode"
	config_get direct_list_dns "$tp_cfg" "direct_list_dns"
	config_get proxy_list_dns "$tp_cfg" "proxy_list_dns"

	_info "Transparent proxy mode: $proxy_mode"

	#init_rules_for_listfile "$direct_list_dns" "$proxy_list_dns"

	local ext_args
	case "${proxy_mode:-default}" in
		"cn_direct")
			local chnroute="$(get_file_content "chnroute")"
			local chnroute6="$(get_file_content "chnroute6")"

			if [ -n "$chnroute" ] ; then
				echo "$chnroute" | _nft_set_populate "$IPSET_DST_DIRECT_V4" ipv4_addr
			fi

			if [ -n "$chnroute6" ] ; then
				echo "$chnroute6" | _nft_set_populate "$IPSET_DST_DIRECT_V6" ipv6_addr
			fi

			ext_args=
			;;
		"cn_proxy")
			local chnroute="$(get_file_content "chnroute")"
			local chnroute6="$(get_file_content "chnroute6")"

			if [ -n "$chnroute" ] ; then
				echo "$chnroute" | _nft_set_populate "$IPSET_DST_PROXY_V4" ipv4_addr
			fi

			if [ -n "$chnroute6" ] ; then
				echo "$chnroute6" | _nft_set_populate "$IPSET_DST_PROXY_V6" ipv6_addr
			fi

			ext_args="ip daddr @$IPSET_DST_PROXY_V4"
			;;
		"gfwlist_proxy")
			local gfwlist="$(get_file_content "gfwlist")"

			if [ -n "$gfwlist" ] ; then
				if [ -n "$proxy_list_dns" ] ; then
					echo "$gfwlist" | \
						sed "s|.*|server=/&/$proxy_list_dns\nnftset=/&/4#inet#fw4#$IPSET_DST_PROXY_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_PROXY_V6|" \
						>> "$FILE_V2RAY_DNSMASQ_CACHE"
				else
					echo "$gfwlist" | \
						sed "s|.*|nftset=/&/4#inet#fw4#$IPSET_DST_PROXY_V4\nnftset=/&/6#inet#fw4#$IPSET_DST_PROXY_V6|" \
						>> "$FILE_V2RAY_DNSMASQ_CACHE"
				fi
			fi

			ext_args="ip daddr @$IPSET_DST_PROXY_V4"
			;;
		*)
			ext_args=
			;;
	esac

	if [ "x$only_privileged_ports" = "x1" ] ; then
		ext_args="--dport 0:1023 $ext_args"
	fi

	if [ -f $FILE_V2RAY_DNSMASQ_CACHE ] && [ -n "$(cat "$FILE_V2RAY_DNSMASQ_CACHE" | grep -v "^$" | grep -v "^#")" ] ; then
		local dir="$(dirname "$FILE_V2RAY_DNSMASQ")"
		test -d "$dir" || mkdir -p "$dir"
		cat "$FILE_V2RAY_DNSMASQ_CACHE" >"$FILE_V2RAY_DNSMASQ" 2>/dev/null
		DNSMASQ_RESTART_EXPECTED=1
	fi

	rm -f "$FILE_V2RAY_DNSMASQ_CACHE"

	local lan_devices lan_ipaddrs

	if [ -n "$lan_ifaces" ] ; then
		. /lib/functions/network.sh

		local lan
		for lan in $lan_ifaces ; do
			local device ipaddrs ipaddr
			network_get_device device "$lan"
			network_get_ipaddrs ipaddrs "$lan"

			if [ -n "$device" ] ; then
				if [ -n "$lan_devices" ] ; then
					lan_devices="$lan_devices $device"
				else
					lan_devices="$device"
				fi
			fi

			if [ -n "$ipaddrs" ] ; then
				for ipaddr in $ipaddrs ; do
					if [ -n "$lan_ipaddrs" ] ; then
						lan_ipaddrs="$ipaddr"
					else
						lan_ipaddrs="$lan_ipaddrs $ipaddr"
					fi
				done
			fi
		done
	fi

	logger -t "v2ray" "add rules"
	add_v2ray_redirect_rules "$ext_args" "$lan_devices" "$lan_ipaddrs"
}

# The template adds "fwmark 1 lookup 100" (local delivery of what tproxy
# marks) when it renders a tproxy redirect. The other proxies' templates add
# the same rule: kept while one of their rule files has a tproxy redirect
v2ray_tproxy_route_del() {
	local f family
	for f in /etc/nftables.d/90-proxy-*.nft*; do
		case "$f" in
			"$v2rrules_nft"*) continue ;;
		esac
		grep -qs tproxy "$f" && return 0
	done
	for family in -4 -6; do
		while ip $family rule del fwmark 1 lookup 100 2>/dev/null; do :; done
		ip $family route flush table 100 2>/dev/null
	done
	return 0
}

clear_transparent_proxy() {
	if [ -f "${v2rrules_nft}" ] || [ -f "${v2rrules_nft}.down" ]; then
		rm -f "$v2rrules_nft"
		rm -f "${v2rrules_nft}.down"
		fw4 -q reload
	fi
	v2ray_tproxy_route_del

	#v2ray-rules -f

	if [ -s "$FILE_V2RAY_DNSMASQ" ] ; then
		rm -f "$FILE_V2RAY_DNSMASQ"
		DNSMASQ_RESTART_EXPECTED=1
	fi
}

start_instance() {
	local section="$1"

	if ! v2ray_section_validate "$section" ; then
		_err "Invalid config."
		return 1
	fi

	if [ "x$enabled" != "x1" ] ; then
		#_info "Service disabled: $section"
		return 0
	fi

	if ! _path_allowed binary "$v2ray_file" || [ ! -f "$v2ray_file" ] || [ ! -s "$v2ray_file" ] ; then
		_err "Invalid V2Ray file."
		return 1
	fi

	test -x "$v2ray_file" || [ -L "$v2ray_file" ] || chmod 755 "$v2ray_file"

	_path_allowed log "$access_log" || {
		_err "Access log $access_log refused, only /tmp or /var/log"
		access_log=/dev/null
	}
	_path_allowed log "$error_log" || {
		_err "Error log $error_log refused, only /tmp or /var/log"
		error_log=/dev/null
	}

	local temp_config

	if [ -n "$config_file" ] ; then
		if [ ! -s "$config_file" ] ; then
			_err "Config file not found: $config_file"
			return 1
		fi

		if ! "$v2ray_file" test -config "$config_file" >/dev/null 2>&1 ; then
			_err "Validate config file failed: $config_file"
			return 1
		fi

		local file_content="$(cat "$config_file")"
		local config_commands="$(get_commands_from_json "$file_content")"

		local addr

		for addr in $(echo "$config_commands" | sed -n "s/^json.*'address'[[:space:]]'\([^']*\)'.*/\1/p") ; do
			append_server_address "$addr"
		done

		temp_config="$config_file"
	else
		test -d "$CONFIG_FOLDER" || mkdir -p "$CONFIG_FOLDER"
		# ids, passwords and keys of the outbounds
		chmod 0700 "$CONFIG_FOLDER"

		temp_config="$CONFIG_FOLDER/v2ray.${section}.json"

		local old_ns
		json_set_namespace "$section" old_ns
		json_init

		json_add_object "log"

		test -n "$access_log" && \
			json_add_string "access" "$access_log"

		if [ -n "$loglevel" ] && [ "$loglevel" != "none" ] ; then
			json_add_string "loglevel" "$loglevel"
			json_add_string "error" "$error_log"
		fi

		json_close_object # log

		if [ "x$stats_enabled" = "x1" ] ; then
			json_add_object "stats"
			json_close_object # stats
		fi

		add_dns_settings "$section"
		add_routing_settings "$section"
		add_policy_settings "$section"
		add_reverse_settings "$section"

		if [ "x$transport_enabled" = "x1" ] ; then
			add_transport_settings
		fi

		if [ -n "$inbounds" ] ; then
			json_add_array "inbounds"

			local is
			for is in $inbounds ; do
				add_inbound_setting "$is"
			done

			json_close_array # inbounds
		fi

		if [ -n "$outbounds" ] ; then
			json_add_array "outbounds"

			local os
			for os in $outbounds ; do
				add_outbound_setting "$os"
			done

			json_close_array # outbounds
		fi

		json_dump -i >"$temp_config"

		json_cleanup
		json_set_namespace "$old_ns"

		if [ ! -s "$temp_config" ] ; then
			_err "Error when create config file: $temp_config"
			return 1
		fi
	fi
	
	PROG="$NAME.$section"

	TRANSPARENT_PROXY_EXPECTED=1
	procd_open_instance "$PROG"
	procd_set_param command "$v2ray_file"
	procd_append_param command run
	procd_append_param command -config "$temp_config"
	procd_set_param respawn

	if [ -n "$asset_location" ] && [ -d "$asset_location" ] ; then
		procd_set_param env V2RAY_LOCATION_ASSET="$asset_location"
	fi

	# cat /proc/PID/limits to see if limits works
	procd_set_param limits nofile="102400 102400"
	procd_append_param limits core="0 0"

	if [ "$mem_percentage" -gt "0" ] ; then
		local mem_total="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
		if [ -n "$mem_total" ] ; then
			local use_mem="$(expr $mem_total \* $mem_percentage \* 10)"
			procd_append_param limits as="$use_mem $use_mem"
		fi
	fi

	procd_set_param file "$temp_config"
	procd_set_param nice 10
	procd_set_param stderr 1 # forward stderr of the command to logd
	procd_set_param stdout 1
	procd_set_param pidfile "/var/run/${NAME}.${section}.pid"
	procd_close_instance
}

rules_exist() {
	#[ -n "$(iptables -w -t nat -L -n 2>/dev/null | grep v2r_)" ] && return 0
	if [ -f "$v2rrules_nft" ]; then
		return 0
	else
		return 1
	fi
}

rules_up() {
	# Don't self-heal missing rules by restarting a v2ray that isn't even
	# supposed to be running: v2ray.main.enabled is the only correct signal
	# for that (NOT network.omrvpn.disabled -- that's the VPN interface's own
	# flag and is unrelated to whether v2ray/xray, which can run standalone
	# with no VPN at all, should be up). Without this, whenever wait_proxy=0
	# and no instance is configured/enabled (disabled server, proxy switched
	# away, empty/broken outbound config, ...), the missing rules file makes
	# every caller of rules_up() -- procd, the omr-tracker-v2ray watchdog,
	# manual restart -- re-trigger
	# `/etc/init.d/v2ray restart &` forever, independent of procd's own
	# respawn handling (issue #4347).
	[ "$(uci -q get v2ray.main.enabled)" = "1" ] || return 0

	if [ -f "${v2rrules_nft}.down" ]; then
		mv -f "${v2rrules_nft}.down" "$v2rrules_nft"
		_fw4_restart
		conntrack -F 2>/dev/null || true
	elif [ ! -f "$v2rrules_nft" ]; then
		# The last start made no rules (transparent proxy off, a config
		# it refused, a missing binary): a restart makes none either,
		# and each one came back here, on every tracker check, or at once
		# from start_service with wait_proxy=0
		[ -f "$NORULES" ] && return 0
		# out of the caller's cgroup and stdio: the omr-tracker-v2ray
		# instance runs this, and procd kills what is left in its cgroup
		# when it stops (omr-tracker restart), half-way through the restart
		sh -c 'echo $$ >/sys/fs/cgroup/cgroup.procs 2>/dev/null; exec /etc/init.d/v2ray restart' >/dev/null 2>&1 1000>&- &
	elif ! nft list ruleset 2>/dev/null | grep -qE 'goto v2r_rules_pre_src_(tcp|udp)'; then
		# The include file itself is present and not parked in .down (so by
		# file-state alone the rules "should" already be live), but an
		# external full nftables rebuild/flush -- e.g. a firewall reload
		# triggered by an unrelated service restart or netifd interface
		# churn racing the same restart_all_async() cascade that (re)started
		# v2ray -- can drop the actually *live* redirect without ever
		# touching this file (issues #4337/#3251). File presence alone is
		# therefore not sufficient evidence the rules are live; the
		# omr-tracker-v2ray watchdog already checks this same live "goto"
		# marker (not bare chain existence, see its _v2ray_rules_missing())
		# for exactly this reason -- mirror that check here so rules_up()
		# actually repairs what it's asked to instead of silently no-op'ing.
		fw4 -q reload
		conntrack -F 2>/dev/null || true
	fi
}

rules_down() {
	if [ -f "${v2rrules_nft}" ]; then
		mv -f "$v2rrules_nft" "${v2rrules_nft}.down"
		_fw4_restart
	fi
}

start_service() {
	rm -f "$TLS_PIN_PENDING"
	rm -rf "$TLS_PIN_STARTED"
	TLS_PROBE_MEMO="/var/run/$NAME-tls-probe"
	rm -rf "$TLS_PROBE_MEMO"
	clear_transparent_proxy
	# until this start has made the rules (an early return keeps it)
	touch "$NORULES"

	config_load "$NAME"

	if ! init_transparent_proxy ; then
		gracefully_restart_dnsmasq
		return 1
	fi

	config_foreach start_instance "v2ray"

	setup_transparent_proxy
	gracefully_restart_dnsmasq
	if [ -f "$v2rrules_nft" ]; then
		rm -f "$NORULES"
	# no instance enabled (not the proxy in use): no rules are expected
	elif [ "x$TRANSPARENT_PROXY_EXPECTED" = "x1" ] && [ -n "$(uci -q get v2ray.main_transparent_proxy.redirect_port)" ]; then
		_err "No transparent proxy rules could be set up"
	fi
	local wait_proxy
	wait_proxy=$(uci -q get v2ray.main_transparent_proxy.wait_proxy 2>/dev/null)
	if [ "${wait_proxy:-1}" = "1" ]; then
		rules_down
	else
		rules_up
	fi
	_tls_retry_schedule
	rm -rf "$TLS_PROBE_MEMO"

	unset OUTBOUND_SERVERS_V4 \
		TLS_PROBE_MEMO \
		OUTBOUND_SERVERS_V6 \
		TRANSPARENT_PROXY_EXPECTED \
		TRANSPARENT_PROXY_PORT \
		TRANSPARENT_PROXY_ADDITION \
		DNSMASQ_RESTART_EXPECTED
}

stop_service() {
	if [ "x$action" = "xrestart" ] ; then
		# skip when restarting, start_service will do this
		return 0
	fi

	_tls_retry_stop
	clear_transparent_proxy
	rules_down
	gracefully_restart_dnsmasq
	test -d "$CONFIG_FOLDER" && rm -rf "$CONFIG_FOLDER"
}

restart_service() {
	stop
	start
}

reload_service() {
	stop
	start
}

service_triggers() {
	procd_add_reload_trigger "$NAME"
}
