#!/bin/sh /etc/rc.common
# E-IX OpenWrt 服务（procd）
#
# 以 `eix-cli daemon --transparent` 常驻：mihomo TUN（auto-route + auto-redirect）
# 接管路由器自身与 LAN 转发流量，LAN 设备无需任何配置。
#
# 配置来自 /etc/config/eix（UCI）：
#   enabled     0/1        总开关（默认 0）
#   mode        rule|global|direct
#   tun_stack   system|gvisor|mixed   （经环境变量 EIX_TUN_STACK 传给 CLI，默认 system）
#   node        节点名，留空/auto = 自动选择（Fallback）
#   tags        0/1        节点名显示运营商标签（本地改名，reload 时热切换，不重启）
#   dns_mode    auto|custom|dnsmasq   内核 DNS 上游：auto 不动订阅；custom 用 dns_server 列表；
#                                     dnsmasq 以本机 dnsmasq(127.0.0.1:53) 为上游（--dns-mode）
#   dns_server  列表       custom 模式的上游（IPv4 / IPv4:端口 / IPv6 / [IPv6] / [IPv6]:端口 / tls:// / https:// / quic://），
#                          每项一个 --dns-server；语法不合法的条目丢弃并记日志（valid_dns_server，与 CLI 同一套规则）
#   dns_hijack  0/1        TUN 是否劫持 LAN 的 DNS 查询（默认 1；0 = --no-dns-hijack）
#   role        router|bypass   router = 本机就是主路由；bypass = 旁路由（LAN 设备把网关 + DNS 指向本机），
#                               旁路由模式额外生成 /etc/eix-bypass.nft：同网卡进出的转发流量做 masquerade，
#                               让主路由看到的是对称流量
#   redirect    auto|nftables|iptables   auto-redirect 用哪套防火墙（默认 auto：见 pick_redirect；另两项排障用）
#
# 安全约定（详见 openwrt/README.md）：
#   - 数据目录 /etc/eix 0700，内部文件 0600；每次启动强制校正
#   - 本脚本不读取、不打印任何凭据 / 订阅地址 / 控制器 secret，日志只含状态信息
#   - 控制器由 eix-cli 绑定 127.0.0.1:9689 + secret；本脚本不接触 secret
#   - 不在 /tmp 放任何配置；不设 pidfile（procd 自行跟踪实例）；/var/run 里只记一份"已应用的开关指纹"
#   - 不开放任何 LAN 侧监听端口（allow-lan 始终 false；内核 DNS 只监听 127.0.0.1:1053）；LAN 流量靠 TUN 转发

START=99
STOP=10
USE_PROCD=1

PROG=/usr/bin/eix-cli
# 内核：.ipk 把内核压缩存放在品牌专属目录 /usr/lib/eix/mihomo.gz（省约 17 MB 闪存，多个品牌互不冲突），
# 启动时解压到内存 CORE_RUN，停止时删掉释放内存；老版本 .ipk / install.sh 装的是未压缩的内核
CORE_GZ=/usr/lib/eix/mihomo.gz
CORE_RUN=/tmp/eix-core/mihomo
if [ -f "$CORE_GZ" ]; then
	CORE=$CORE_RUN
elif [ -x /usr/lib/eix/mihomo ]; then
	CORE=/usr/lib/eix/mihomo
else
	CORE=/usr/bin/mihomo
fi
DATA_DIR=/etc/eix
GEO_SRC=/usr/share/eix/geoip.metadb
FW_INCLUDE=/etc/eix-firewall.nft
# 旁路由 masquerade 链：由本脚本按 role 生成（router 角色 = 只有注释），stop 时清空
FW_BYPASS=/etc/eix-bypass.nft
CHECK_SCRIPT=/usr/share/eix/check_perms.sh
DIAG_SCRIPT=/usr/share/eix/diagnose.sh
# 国内域名清单（订阅里的 rule-provider cn-domain）的包内预置
RULESET_SRC=/usr/share/eix/cn-domain.mrs
# 已应用的开关指纹（非敏感：模式 / DNS / 角色等），reload 时据此判断是否需要 restart
APPLIED=/var/run/eix.applied
# 本次启动 auto-redirect 用的防火墙（"后端|原因|iptables 缺的软件包"，非敏感），诊断脚本与 LuCI 环境提示读取
REDIRECT_STATE=/var/run/eix.redirect
# iptables 模式下丢弃公网来源 DNS 查询的 mangle 链（见 dns_guard）
DNS_GUARD_CHAIN=eix_dns_guard
# mihomo 在 Linux 上的默认 TUN 网卡名（可能带数字后缀，规则里用通配 Meta*）
TUN_DEV=Meta

extra_command "check" "Run E-IX security self-check (permissions / ports / leaks)"
extra_command "diag" "Run E-IX diagnostics (sanitized report for support)"

log() {
	logger -t eix -p daemon.info -- "$*"
}

warn() {
	logger -t eix -p daemon.warn -- "$*"
	echo "eix: $*" >&2
}

# 数据目录与文件权限：目录 0700、普通文件 0600、全部 root 所有。
# 只处理普通文件与子目录，不跟随符号链接（geoip.metadb 可能是指向 /usr/share 的链接）。
ensure_perms() {
	[ -d "$DATA_DIR" ] || mkdir -p "$DATA_DIR"
	chown root:root "$DATA_DIR" 2>/dev/null
	chmod 0700 "$DATA_DIR"
	find "$DATA_DIR" -type d -exec chmod 0700 {} \; 2>/dev/null
	find "$DATA_DIR" -type f -exec chmod 0600 {} \; 2>/dev/null
	find "$DATA_DIR" -type f -exec chown root:root {} \; 2>/dev/null
}

# 内核需要数据目录里有 geoip.metadb（GEOIP,CN 规则）。
# 包内数据库装在 /usr/share/eix，这里用符号链接接入，省下 overlay 空间；链接失败则拷贝。
ensure_geo() {
	local dst="$DATA_DIR/geoip.metadb"
	# 悬空链接（例如包被卸载后重装）先清掉
	if [ -L "$dst" ] && [ ! -e "$dst" ]; then
		rm -f "$dst"
	fi
	[ -e "$dst" ] && return 0
	if [ -f "$GEO_SRC" ]; then
		ln -s "$GEO_SRC" "$dst" 2>/dev/null || cp "$GEO_SRC" "$dst"
		[ -L "$dst" ] || chmod 0600 "$dst"
	else
		warn "缺少 $GEO_SRC，GEOIP 规则将无法工作（内核会尝试联网下载，通常失败）"
	fi
}

# 国内域名清单：订阅的 rule-provider cn-domain 指向数据目录的 ruleset/cn-domain.mrs。本地没有时内核要先联网下载完
# 才开放控制器，国内网络慢时会超过启动等待时间（controller did not become ready in time）；有本地文件就直接用，
# 更新在后台进行。所以数据目录没有时从包内预置拷一份（0600）。
seed_ruleset() {
	local dir="$DATA_DIR/ruleset" dst="$DATA_DIR/ruleset/cn-domain.mrs"
	[ -s "$dst" ] && return 0
	[ -f "$RULESET_SRC" ] || return 0
	mkdir -p "$dir" && chmod 0700 "$dir"
	if cp -f "$RULESET_SRC" "$dst.tmp" && chmod 0600 "$dst.tmp" && mv -f "$dst.tmp" "$dst"; then
		log "已预置国内域名清单（首次启动不必联网下载）"
	else
		rm -f "$dst.tmp"
	fi
}

# 是否已登录：凭据文件由 `eix-cli login` 创建（0600）。只判断存在，不读取内容；
# 文件名不符时退回 `eix-cli status --json` 的 loggedIn 字段（该输出按约定不含任何敏感信息）。
logged_in() {
	[ -s "$DATA_DIR/credentials.enc" ] && return 0
	"$PROG" status --json --data-dir "$DATA_DIR" 2>/dev/null | grep -q '"loggedIn": *true'
}

# 内核是否在运行（同样只看 status --json 的 running 字段）
core_running() {
	"$PROG" status --json --data-dir "$DATA_DIR" 2>/dev/null | grep -q '"running": *true'
}

valid_mode() {
	case "$1" in
		rule|global|direct) echo "$1" ;;
		*) echo rule ;;
	esac
}

valid_stack() {
	case "$1" in
		system|gvisor|mixed) echo "$1" ;;
		*) echo system ;;
	esac
}

valid_dns_mode() {
	case "$1" in
		auto|custom|dnsmasq) echo "$1" ;;
		*) echo auto ;;
	esac
}

valid_role() {
	case "$1" in
		router|bypass) echo "$1" ;;
		*) echo router ;;
	esac
}

valid_redirect() {
	case "$1" in
		nftables|iptables) echo "$1" ;;
		*) echo auto ;;
	esac
}

# LAN 桥接网卡名（旁路由 masquerade 规则用），取不到时退回 br-lan
lan_device() {
	local dev=""
	if [ -f /lib/functions/network.sh ]; then
		. /lib/functions/network.sh
		network_get_device dev lan 2>/dev/null
	fi
	[ -n "$dev" ] || dev=br-lan
	echo "$dev"
}

# 生成旁路由 include（fw4 table-post：可定义整条链）。每次 start 都重写（幂等）：
#   bypass：postrouting srcnat 链，只对"从 LAN 网卡进、又从同一 LAN 网卡出"的转发流量做 masquerade
#           （LAN 设备 → 本机作网关 → 转回主路由），主路由看到的是本机发起的对称流量
#   router：只留注释（无规则）
# 网卡名来自 network_get_device，不含用户输入；写入前仍只接受 [A-Za-z0-9_.-]。
write_bypass_include() {
	local role="$1" lan
	local tmp="$FW_BYPASS.tmp"
	{
		echo "# E-IX：旁路由 masquerade（由 /etc/init.d/eix 按 UCI eix.main.role 自动生成，请勿手改）"
		echo "# fw4 include：position=table-post（位于 table inet fw4 内部，可定义链）"
		if [ "$role" = bypass ]; then
			lan="$(lan_device)"
			case "$lan" in
				*[!A-Za-z0-9_.-]*|"") lan=br-lan ;;
			esac
			echo "chain eix_bypass_srcnat {"
			echo "	type nat hook postrouting priority srcnat; policy accept;"
			echo "	iifname \"$lan\" oifname \"$lan\" counter masquerade comment \"eix: bypass gateway, keep flows symmetric for the main router\""
			echo "}"
		else
			echo "# role=router：无规则"
		fi
	} > "$tmp" && chmod 0644 "$tmp" && mv -f "$tmp" "$FW_BYPASS"
}

# 是否需要 reload fw4：include 内容变了才 reload（避免每次 start 都打断连接跟踪）
bypass_include_changed() {
	local role="$1"
	[ -f "$FW_BYPASS" ] || return 0
	if [ "$role" = bypass ]; then
		grep -q 'chain eix_bypass_srcnat' "$FW_BYPASS" || return 0
		grep -q "iifname \"$(lan_device)\" oifname \"$(lan_device)\"" "$FW_BYPASS" || return 0
		return 1
	fi
	grep -q 'chain eix_bypass_srcnat' "$FW_BYPASS" && return 0
	return 1
}

# 放行 LAN -> TUN 的转发。TUN 网卡不属于任何防火墙 zone，fw4 默认 forward 策略会拒绝，
# 所以要把 /etc/eix-firewall.nft 以 include（chain-pre / forward）挂进 fw4；
# 旁路由 masquerade 链另以 table-post include（/etc/eix-bypass.nft）挂进 fw4。
# 旧版 fw3/iptables 系统直接插 FORWARD 规则（stop 时移除；旁路由 masquerade 用 iptables MASQUERADE）。
# 注意：这里只放行"转发"，不放行到路由器自身的 input，不开放任何 LAN 侧端口。
ensure_firewall() {
	local role="$1" changed=0 lan lan_cidr
	if command -v fw4 >/dev/null 2>&1; then
		[ -f "$FW_INCLUDE" ] || {
			warn "缺少 $FW_INCLUDE，LAN 设备可能无法通过路由器代理"
		}
		if [ -f "$FW_INCLUDE" ] && [ "$(uci -q get firewall.eix.path)" != "$FW_INCLUDE" ]; then
			uci -q batch <<-EOF
				delete firewall.eix
				set firewall.eix=include
				set firewall.eix.type='nftables'
				set firewall.eix.path='$FW_INCLUDE'
				set firewall.eix.position='chain-pre'
				set firewall.eix.chain='forward'
				commit firewall
			EOF
			log "已注册 fw4 include（放行 lan -> $TUN_DEV 转发）"
			changed=1
		fi
		bypass_include_changed "$role" && changed=1
		write_bypass_include "$role"
		if [ "$(uci -q get firewall.eix_bypass.path)" != "$FW_BYPASS" ]; then
			uci -q batch <<-EOF
				delete firewall.eix_bypass
				set firewall.eix_bypass=include
				set firewall.eix_bypass.type='nftables'
				set firewall.eix_bypass.path='$FW_BYPASS'
				set firewall.eix_bypass.position='table-post'
				commit firewall
			EOF
			log "已注册 fw4 include（旁路由 masquerade 链，role=$role）"
			changed=1
		fi
		[ "$changed" = 1 ] && /etc/init.d/firewall reload >/dev/null 2>&1
		# fw4 reload 只 flush 规则不删链：从旁路由切回主路由时把空链也删掉
		[ "$role" != bypass ] && nft delete chain inet fw4 eix_bypass_srcnat >/dev/null 2>&1
	elif command -v iptables >/dev/null 2>&1; then
		iptables -C FORWARD -o "${TUN_DEV}+" -j ACCEPT 2>/dev/null || \
			iptables -I FORWARD -o "${TUN_DEV}+" -j ACCEPT
		iptables -C FORWARD -i "${TUN_DEV}+" -j ACCEPT 2>/dev/null || \
			iptables -I FORWARD -i "${TUN_DEV}+" -j ACCEPT
		if command -v ip6tables >/dev/null 2>&1; then
			ip6tables -C FORWARD -o "${TUN_DEV}+" -j ACCEPT 2>/dev/null || \
				ip6tables -I FORWARD -o "${TUN_DEV}+" -j ACCEPT
			ip6tables -C FORWARD -i "${TUN_DEV}+" -j ACCEPT 2>/dev/null || \
				ip6tables -I FORWARD -i "${TUN_DEV}+" -j ACCEPT
		fi
		remove_bypass_legacy
		if [ "$role" = bypass ]; then
			# nat POSTROUTING 里不能用 -i（入口网卡此时已不可知）：改为"出口是 LAN 网卡 且 源地址在 LAN 网段"，
			# 与 fw4 的 iifname==oifname 语义等价（只有 LAN 设备发来的、又送回 LAN 的转发包会命中）
			lan="$(lan_device)"
			lan_cidr="$(lan_cidr "$lan")"
			if [ -n "$lan_cidr" ]; then
				iptables -t nat -C POSTROUTING -o "$lan" -s "$lan_cidr" -m comment --comment eix-bypass -j MASQUERADE 2>/dev/null || \
					iptables -t nat -I POSTROUTING -o "$lan" -s "$lan_cidr" -m comment --comment eix-bypass -j MASQUERADE
			else
				warn "无法确定 $lan 的 IPv4 网段，跳过旁路由 masquerade 规则（主路由可能看到不对称流量）"
			fi
		fi
	fi
}

# LAN 网卡的第一个 IPv4 网段（如 192.168.1.1/24 → 192.168.1.0/24 由 iptables 自行归一）；取不到返回空
lan_cidr() {
	local cidr
	cidr="$(ip -o -4 addr show dev "$1" 2>/dev/null | awk '{print $4; exit}')"
	case "$cidr" in
		*[!0-9./]*|"") echo "" ;;
		*/*) echo "$cidr" ;;
		*) echo "" ;;
	esac
}

# 旁路由 include 清空（stop 时）：留一个只含注释的文件，fw4 include 路径仍有效
clear_bypass_include() {
	if command -v fw4 >/dev/null 2>&1 && [ -f "$FW_BYPASS" ] && grep -q 'chain eix_bypass_srcnat' "$FW_BYPASS"; then
		write_bypass_include router
		/etc/init.d/firewall reload >/dev/null 2>&1
		nft delete chain inet fw4 eix_bypass_srcnat >/dev/null 2>&1
	fi
}

# 删除全部带 eix-bypass 注释的 nat POSTROUTING 规则：按当前规则文本删（-S 输出的 -A 改成 -D），
# 不依赖此刻的网卡名 / 网段——LAN 网段变了也能清干净
remove_bypass_legacy() {
	command -v iptables >/dev/null 2>&1 || return 0
	local rule
	iptables -t nat -S POSTROUTING 2>/dev/null | grep -- '--comment eix-bypass' | sed 's/^-A /-D /' | \
	while read -r rule; do
		# shellcheck disable=SC2086  # 规则由 iptables 自己生成，按空白切分即为原参数
		iptables -t nat $rule 2>/dev/null
	done
}

remove_firewall_legacy() {
	command -v fw4 >/dev/null 2>&1 && return 0
	command -v iptables >/dev/null 2>&1 || return 0
	while iptables -C FORWARD -o "${TUN_DEV}+" -j ACCEPT 2>/dev/null; do
		iptables -D FORWARD -o "${TUN_DEV}+" -j ACCEPT
	done
	while iptables -C FORWARD -i "${TUN_DEV}+" -j ACCEPT 2>/dev/null; do
		iptables -D FORWARD -i "${TUN_DEV}+" -j ACCEPT
	done
	if command -v ip6tables >/dev/null 2>&1; then
		while ip6tables -C FORWARD -o "${TUN_DEV}+" -j ACCEPT 2>/dev/null; do
			ip6tables -D FORWARD -o "${TUN_DEV}+" -j ACCEPT
		done
		while ip6tables -C FORWARD -i "${TUN_DEV}+" -j ACCEPT 2>/dev/null; do
			ip6tables -D FORWARD -i "${TUN_DEV}+" -j ACCEPT
		done
	fi
	remove_bypass_legacy
}

# ---- auto-redirect 用哪套防火墙 ----
# 局域网透明代理靠内核（sing-tun）的 auto-redirect 往防火墙里写 nat 规则。它自己的选法是「内核能列出 nftables 的表
# 就用 nftables」：老的 fw3 固件（QWRT、Lean LEDE 等）内核常带 nftables 核心却没有 nat 模块（kmod-nft-nat），
# 建 nat 链时报 netlink receive: no such file or directory，TUN 整个起不来。所以先探测：fw4 固件照旧用 nftables；
# 其它固件 nftables 的 nat 用不了、iptables 的 nat 能用时，给内核设 DISABLE_NFTABLES=1（mihomo 读取）改走 iptables。

# nftables 能否建 auto-redirect 要用的 nat 链 + redirect / dnat
nft_nat_ok() {
	local m d
	if command -v nft >/dev/null 2>&1; then
		# 真建一张临时表（只匹配 9 号端口），能建成即可用；nft -f 是原子的，失败时什么都不会留下
		nft delete table inet eix_probe >/dev/null 2>&1
		nft -f - >/dev/null 2>&1 <<-EOF || return 1
			table inet eix_probe {
				chain p {
					type nat hook prerouting priority -100; policy accept;
					meta nfproto ipv4 tcp dport 9 redirect to :9
					meta nfproto ipv4 udp dport 9 dnat ip to 127.0.0.1:9
				}
			}
		EOF
		nft delete table inet eix_probe >/dev/null 2>&1
		return 0
	fi
	# 没有 nft 命令：看内核模块（OpenWrt 的 kmod 平铺在 /lib/modules/<版本>/，已加载的在 /proc/modules）
	d="/lib/modules/$(uname -r)"
	for m in nft_chain_nat nft_nat nft_redir; do
		[ -e "$d/$m.ko" ] || grep -q "^$m " /proc/modules 2>/dev/null || return 1
	done
	return 0
}

# iptables 能否建 auto-redirect 要用的 nat 规则（addrtype / REDIRECT / DNAT）：输出缺的软件包，空 = 能用。
# 在一条不挂进任何内置链的临时链里试，不影响现有流量
ipt_nat_missing() {
	local c=eix_probe miss=""
	command -v iptables >/dev/null 2>&1 || { echo "iptables kmod-ipt-nat"; return 0; }
	iptables -w -t nat -F "$c" 2>/dev/null
	iptables -w -t nat -X "$c" 2>/dev/null
	if iptables -w -t nat -N "$c" 2>/dev/null; then
		iptables -w -t nat -A "$c" -m addrtype --dst-type LOCAL -j RETURN 2>/dev/null ||
			miss="iptables-mod-extra kmod-ipt-extra"
		{ iptables -w -t nat -A "$c" -p tcp -j REDIRECT --to-ports 9 2>/dev/null &&
			iptables -w -t nat -A "$c" -p udp --dport 53 -j DNAT --to-destination 127.0.0.1 2>/dev/null; } ||
			miss="$miss kmod-ipt-nat"
		iptables -w -t nat -F "$c" 2>/dev/null
		iptables -w -t nat -X "$c" 2>/dev/null
	else
		miss="kmod-ipt-nat"
	fi
	echo "${miss# }"
}

# 选定后端放进 REDIRECT_BACKEND，并写 REDIRECT_STATE。$1 = UCI redirect（auto / nftables / iptables）
REDIRECT_BACKEND=nftables
pick_redirect() {
	local miss
	REDIRECT_BACKEND=nftables
	mkdir -p "${REDIRECT_STATE%/*}"
	case "$1" in
		nftables|iptables)
			REDIRECT_BACKEND="$1"
			echo "$1|uci|" > "$REDIRECT_STATE"
			log "透明代理按设置使用 $1（eix.main.redirect）"
			return 0
			;;
	esac
	if command -v fw4 >/dev/null 2>&1; then
		echo "nftables|fw4|" > "$REDIRECT_STATE"
		return 0
	fi
	if nft_nat_ok; then
		echo "nftables|nft_nat|" > "$REDIRECT_STATE"
		return 0
	fi
	miss="$(ipt_nat_missing)"
	if [ -z "$miss" ]; then
		REDIRECT_BACKEND=iptables
		echo "iptables|no_nft_nat|" > "$REDIRECT_STATE"
		log "内核没有 nftables 的 nat 功能（kmod-nft-nat），透明代理改用 iptables"
		return 0
	fi
	# 两边都不行：照旧交给内核自己选（多半起不来），提示写进 REDIRECT_STATE，由 env_warnings / LuCI 显示
	echo "none|no_nat|$miss" > "$REDIRECT_STATE"
}

# iptables 模式的 DNS 劫持（sing-tun 在 nat PREROUTING 把所有 UDP 53 都 DNAT 进内核）不像 nftables 版那样只接管
# 本机直连网段发来的查询：公网发到 WAN 口 53 端口的查询也会被转进内核的 DNS，等于开了一个公网 DNS 解析器。
# mangle 表先于 nat：在这里丢掉来源既不是私网地址、也不在本机直连网段的 UDP 53 查询（局域网设备不受影响；
# 路由器自己发出的查询走 OUTPUT，不经过这里）。$1 = on / off
dns_guard() {
	local c="$DNS_GUARD_CHAIN" n ok=1
	command -v iptables >/dev/null 2>&1 || return 0
	# fw4 固件只有本次按设置用了 iptables 时才碰 iptables（iptables-nft 会顺手建出多余的表）
	if command -v fw4 >/dev/null 2>&1 && [ "$1" != on ] && [ "$(cut -d'|' -f1 "$REDIRECT_STATE" 2>/dev/null)" != iptables ]; then
		return 0
	fi
	while iptables -w -t mangle -D PREROUTING -p udp --dport 53 -j "$c" 2>/dev/null; do :; done
	iptables -w -t mangle -F "$c" 2>/dev/null
	iptables -w -t mangle -X "$c" 2>/dev/null
	[ "$1" = on ] || return 0
	if iptables -w -t mangle -N "$c" 2>/dev/null; then
		for n in 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16 $(local_nets); do
			iptables -w -t mangle -A "$c" -s "$n" -j RETURN 2>/dev/null || ok=0
		done
		[ "$ok" = 1 ] && iptables -w -t mangle -A "$c" -j DROP 2>/dev/null &&
			iptables -w -t mangle -I PREROUTING -p udp --dport 53 -j "$c" 2>/dev/null && return 0
	fi
	dns_guard off
	warn "无法添加 WAN 口 DNS 防护规则（iptables mangle 表），公网可能访问到本机代理的 DNS"
}

# 本机各网卡的 IPv4 网段（如 192.168.1.1/24，iptables 会自行归一成网络地址），只保留合法字符
local_nets() {
	ip -o -4 addr show scope global 2>/dev/null | awk '{ print $4 }' | grep -E '^[0-9.]+/[0-9]+$'
}

# ---- dns_server 语法校验：与 CLI 的 patch.ValidDNSServer 同一套规则（改动需同步）----
#   IPv4 | IPv4:port | IPv6 | [IPv6] | [IPv6]:port | tls://host[:port] | https://host/path | quic://…
valid_port() {
	case "$1" in
		""|*[!0-9]*) return 1 ;;
	esac
	[ ${#1} -le 5 ] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}

valid_ipv4() {
	local n c=0 IFS=.
	case "$1" in
		""|*[!0-9.]*|.*|*.|*..*) return 1 ;;
	esac
	for n in $1; do
		[ ${#n} -le 3 ] && [ "$n" -le 255 ] || return 1
		c=$((c + 1))
	done
	[ "$c" -eq 4 ]
}

# 宽松的 IPv6 形态检查：只允许十六进制 / 冒号 / 点（IPv4 映射），至少两个冒号，不含 ":::"
valid_ipv6() {
	case "$1" in
		""|*[!0-9a-fA-F:.]*|*:::*) return 1 ;;
		*:*:*) return 0 ;;
	esac
	return 1
}

valid_dns_server() {
	local s="$1" host port
	case "$s" in
		""|*[[:space:],]*) return 1 ;;
		tls://?*|https://?*|quic://?*) return 0 ;;
		\[*\]:*)
			host="${s#\[}"; host="${host%%\]*}"; port="${s##*\]:}"
			valid_ipv6 "$host" && valid_port "$port"; return ;;
		\[*\])
			host="${s#\[}"; host="${host%\]}"
			valid_ipv6 "$host"; return ;;
		*:*:*) valid_ipv6 "$s"; return ;;
		*:*)
			host="${s%%:*}"; port="${s#*:}"
			valid_ipv4 "$host" && valid_port "$port"; return ;;
	esac
	valid_ipv4 "$s"
}

# 读取 UCI 并规范化；结果放在全局变量（config_load 只能在同一 shell 里用）
DNS_SERVERS=""
add_dns_server() {
	# 逐项校验语法；不合法的丢弃并记日志（CLI 端遇到坏条目也会自行退回 auto，这里先拦一道）
	if valid_dns_server "$1"; then
		DNS_SERVERS="$DNS_SERVERS $1"
	else
		warn "忽略无效的 dns_server 条目（应为 IP / IP:端口 / [IPv6] / tls:// / https:// / quic://）"
	fi
}

load_settings() {
	config_load eix
	config_get_bool enabled main enabled 0
	config_get mode main mode rule
	config_get tun_stack main tun_stack system
	config_get node main node ""
	config_get_bool tags main tags 0
	config_get dns_mode main dns_mode auto
	config_get_bool dns_hijack main dns_hijack 1
	config_get role main role router
	config_get redirect main redirect auto
	DNS_SERVERS=""
	config_list_foreach main dns_server add_dns_server
	mode="$(valid_mode "$mode")"
	tun_stack="$(valid_stack "$tun_stack")"
	dns_mode="$(valid_dns_mode "$dns_mode")"
	role="$(valid_role "$role")"
	redirect="$(valid_redirect "$redirect")"
	# 回退放在这里而不是 start_service：start 与 reload 看到的是同一份"生效设置"，
	# 指纹（settings_fingerprint）才不会因为回退而每次 reload 都判定有变化并 restart
	if [ "$dns_mode" = custom ] && [ -z "$DNS_SERVERS" ]; then
		warn "dns_mode=custom 但没有有效的 list dns_server，改用 auto"
		dns_mode=auto
	fi
}

# 需要 restart 才能生效的开关指纹（不含节点名 / 模式 / 运营商标签：这几项 reload 热切）。
# 取的是 load_settings 回退之后的生效值。
settings_fingerprint() {
	echo "stack=$tun_stack dns_mode=$dns_mode dns_server=$DNS_SERVERS dns_hijack=$dns_hijack role=$role redirect=$redirect"
}

# 局域网发往本机的 IPv6 DNS：内核只接管 IPv4（订阅 ipv6: false），TUN 的 DNS 劫持也只覆盖 IPv4。
# 设备走 IPv6 问本机 DNS 时，dnsmasq 若转给本机的 SmartDNS / AdGuardHome 等（不经 TUN），会拿到
# 未经内核的解析结果（被污染的 IP / 直连的 IPv6 地址），表现为「服务在运行但上不了外网」。
# 代理运行期间拒绝这类查询，设备会立即改用 IPv4 DNS（被劫持进内核）。停止服务时移除。
V6DNS_TABLE=eix_v6dns
V6DNS_CHAIN=eix_v6dns
ipv6_dns_guard() {
	# 先把两种实现都清掉，再加一种：nft 能用就用独立的 nft 表；老的 fw3 固件（QWRT、Lean LEDE 等）
	# 没有 nft 命令或加不上时，用 ip6tables 自定义链实现同样的规则。fw4 固件上不碰 ip6tables（iptables-nft 会顺手建出多余的表）
	command -v nft >/dev/null 2>&1 && nft delete table inet "$V6DNS_TABLE" >/dev/null 2>&1
	command -v fw4 >/dev/null 2>&1 || ipv6_dns_guard_legacy off
	[ "$1" = on ] || return 0
	if command -v nft >/dev/null 2>&1 && nft -f - 2>/dev/null <<-EOF
		table inet $V6DNS_TABLE {
			chain input {
				type filter hook input priority -10; policy accept;
				meta nfproto ipv6 iifname != "lo" meta l4proto { tcp, udp } th dport 53 counter reject comment "eix: LAN IPv6 DNS -> use IPv4 (hijacked)"
			}
		}
	EOF
	then
		return 0
	fi
	if command -v fw4 >/dev/null 2>&1; then
		warn "无法添加 IPv6 DNS 规则（nft），IPv6 设备的 DNS 可能不经代理"
		return 0
	fi
	ipv6_dns_guard_legacy on
}

ipv6_dns_guard_legacy() {
	command -v ip6tables >/dev/null 2>&1 || return 0
	while ip6tables -w -D INPUT -j "$V6DNS_CHAIN" 2>/dev/null; do :; done
	ip6tables -w -F "$V6DNS_CHAIN" 2>/dev/null
	ip6tables -w -X "$V6DNS_CHAIN" 2>/dev/null
	[ "$1" = on ] || return 0
	if ip6tables -w -N "$V6DNS_CHAIN" 2>/dev/null &&
		ip6tables -w -A "$V6DNS_CHAIN" ! -i lo -p udp --dport 53 -j REJECT 2>/dev/null &&
		ip6tables -w -A "$V6DNS_CHAIN" ! -i lo -p tcp --dport 53 -j REJECT --reject-with tcp-reset 2>/dev/null &&
		ip6tables -w -I INPUT -j "$V6DNS_CHAIN" 2>/dev/null; then
		return 0
	fi
	ipv6_dns_guard_legacy off
	warn "无法添加 IPv6 DNS 规则（ip6tables），IPv6 设备的 DNS 可能不经代理"
}

# 把压缩存放的内核解压到内存；内存里已有同一版本（校验和一致）就直接用
ensure_core() {
	[ -f "$CORE_GZ" ] || return 0
	local sum dir="${CORE_RUN%/*}"
	sum="$(md5sum "$CORE_GZ" 2>/dev/null | cut -d' ' -f1)"
	if [ -x "$CORE_RUN" ] && [ -n "$sum" ] && [ "$(cat "$CORE_RUN.md5" 2>/dev/null)" = "$sum" ]; then
		return 0
	fi
	mkdir -p "$dir" && chmod 0700 "$dir"
	if gzip -dc "$CORE_GZ" > "$CORE_RUN.tmp" 2>/dev/null && chmod 0755 "$CORE_RUN.tmp" && mv -f "$CORE_RUN.tmp" "$CORE_RUN"; then
		echo "$sum" > "$CORE_RUN.md5"
		return 0
	fi
	rm -f "$CORE_RUN.tmp"
	warn "解压内核到 $dir 失败（内存不足？约需 30 MB 可用内存）"
	return 1
}

# 停止后删掉解压出来的内核，释放约 30 MB 内存（下次启动再解压，只要一两秒）
drop_core() {
	[ -f "$CORE_GZ" ] && rm -rf "${CORE_RUN%/*}"
}

# 常见的「运行中但上不了网」环境问题（其它代理、流量分载、时间、运行角色…）：检查逻辑在诊断脚本里，
# 与 LuCI 概览页的提示是同一份。这里只写进日志，不自动改用户设置
env_warnings() {
	[ -x "$DIAG_SCRIPT" ] || return 0
	"$DIAG_SCRIPT" warnings 2>/dev/null | while IFS='|' read -r code detail text; do
		[ -n "$text" ] && warn "$text"
	done
}

start_service() {
	local enabled mode tun_stack node tags dns_mode dns_hijack role redirect s nft_env=""
	load_settings

	if [ "$enabled" != 1 ]; then
		warn "服务未启用：uci set eix.main.enabled=1 && uci commit eix && /etc/init.d/eix start"
		return 0
	fi

	ensure_perms
	ensure_geo
	seed_ruleset

	if [ ! -x "$PROG" ]; then
		warn "找不到 $PROG"
		return 1
	fi
	# 先确认已登录，再解压内核（未登录时不白占约 30 MB 内存）
	if ! logged_in; then
		warn "尚未登录：请先执行 eix-cli login（数据目录 $DATA_DIR），再 /etc/init.d/eix start"
		return 1
	fi
	ensure_core || return 1
	if [ ! -x "$CORE" ]; then
		warn "找不到内核 $CORE"
		return 1
	fi
	if [ ! -c /dev/net/tun ]; then
		modprobe tun 2>/dev/null
		[ -c /dev/net/tun ] || {
			warn "缺少 /dev/net/tun，请安装 kmod-tun"
			return 1
		}
	fi

	ensure_firewall "$role"
	pick_redirect "$redirect"
	if [ "$REDIRECT_BACKEND" = iptables ]; then
		nft_env="DISABLE_NFTABLES=1"
		dns_guard on
	else
		dns_guard off
	fi
	env_warnings
	if [ "$dns_hijack" = 1 ]; then ipv6_dns_guard on; else ipv6_dns_guard off; fi

	procd_open_instance eix
	procd_set_param command "$PROG" daemon
	procd_append_param command --data-dir "$DATA_DIR" --core "$CORE"
	procd_append_param command --transparent --mode "$mode"
	if [ "$tags" = 1 ]; then
		procd_append_param command --tags
	else
		procd_append_param command --no-tags
	fi
	[ -n "$node" ] && procd_append_param command --node "$node"
	procd_append_param command --dns-mode "$dns_mode"
	for s in $DNS_SERVERS; do
		procd_append_param command --dns-server "$s"
	done
	if [ "$dns_hijack" = 1 ]; then
		procd_append_param command --dns-hijack
	else
		procd_append_param command --no-dns-hijack
	fi
	# 只传路径 / 栈类型 / 防火墙后端，不传任何凭据（CLI 启动内核时环境变量原样继承）
	# shellcheck disable=SC2086  # nft_env 为空时不占参数
	procd_set_param env HOME=/root \
		EIX_DATA_DIR="$DATA_DIR" \
		EIX_CORE="$CORE" \
		EIX_TUN_STACK="$tun_stack" $nft_env
	# 异常退出自动拉起：1 小时内最多 5 次，每次间隔 5 秒（未登录等硬错误不会无限循环）
	procd_set_param respawn 3600 5 5
	# CLI 约定：任何输出都不含订阅地址 / token / secret / 配置内容，可以安全交给 logd
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_set_param term_timeout 20
	procd_set_param limits nofile="65535 65535"
	procd_close_instance

	mkdir -p "$(dirname "$APPLIED")"
	settings_fingerprint > "$APPLIED"

	log "已启动（mode=$mode stack=$tun_stack node=${node:-auto} dns=$dns_mode hijack=$dns_hijack role=$role redirect=$REDIRECT_BACKEND）"
	if [ "$role" = bypass ]; then
		log "旁路由模式：请把 LAN 设备（或主路由 DHCP 下发）的网关与 DNS 指向本机 LAN 地址；同网卡转发流量已做 masquerade"
	fi
	if [ "$dns_mode" = dnsmasq ] && [ "$dns_hijack" = 1 ]; then
		log "提示：dns_mode=dnsmasq 且 dns_hijack=1 时，本机 dnsmasq 的上游查询也会被 TUN 劫持回内核；通常应把 dns_hijack 设为 0"
	fi
}

# 正常停止的顺序由 rc.common 决定：先 stop_service，再 procd_kill（向 daemon 发 SIGTERM，daemon 自己
# 停内核、还原路由 / nft、抹 tmpfs）。这里不要自己再 procd_kill / 杀 daemon：重复 delete 会让 rc.common 的
# 那次 procd_kill 报 "ubus call service delete … (Not found)"，而外部 SIGTERM 又会触发 procd respawn。
# 只有 procd 里没有实例（daemon 早已退出、内核残留）时才由 CLI 兜底 down。
stop_service() {
	if ! procd_has_instance; then
		[ -x "$PROG" ] && "$PROG" down --data-dir "$DATA_DIR" --core "$CORE" >/dev/null 2>&1
	fi
	clear_bypass_include
	remove_firewall_legacy
	ipv6_dns_guard off
	dns_guard off
	drop_core
	rm -f "$APPLIED" "$REDIRECT_STATE"
	log "已停止"
}

# procd 里是否还有本服务的实例
procd_has_instance() {
	ubus -S call service list "{\"name\":\"$(basename "$initscript")\"}" 2>/dev/null | grep -q '"instances"'
}

service_triggers() {
	procd_add_reload_trigger "eix"
}

# uci commit eix 后 reload：mode / node / tags 热切换（tags 用缓存订阅本地改名后热重载）；
# enabled 变化则启停；tun_stack / dns_* / role / redirect 变化需要重建配置，自动 restart（对比启动时记录的指纹）。
reload_service() {
	local enabled mode tun_stack node tags dns_mode dns_hijack role redirect s
	load_settings

	if [ "$enabled" != 1 ]; then
		stop
		return 0
	fi
	if ! core_running; then
		start
		return 0
	fi
	if [ ! -f "$APPLIED" ] || [ "$(cat "$APPLIED" 2>/dev/null)" != "$(settings_fingerprint)" ]; then
		log "DNS / 角色 / 协议栈 / 防火墙设置有变化，重启服务以重建配置"
		restart
		return 0
	fi
	if [ "$tags" = 1 ]; then s=on; else s=off; fi
	"$PROG" tags "$s" --data-dir "$DATA_DIR" >/dev/null 2>&1 || warn "切换运营商标签失败（tags=$tags）"
	"$PROG" mode "$mode" --data-dir "$DATA_DIR" >/dev/null 2>&1 || warn "切换模式失败（mode=$mode）"
	"$PROG" node "${node:-auto}" --data-dir "$DATA_DIR" >/dev/null 2>&1 || warn "切换节点失败（node=${node:-auto}）"
	log "已热更新（mode=$mode node=${node:-auto}）"
}

check() {
	if [ -x "$CHECK_SCRIPT" ]; then
		"$CHECK_SCRIPT"
	else
		echo "eix: 找不到 $CHECK_SCRIPT" >&2
		return 1
	fi
}

# 一键诊断（与 LuCI「日志」页同一份报告，已脱敏，可直接发给客服）
diag() {
	if [ -x "$DIAG_SCRIPT" ]; then
		"$DIAG_SCRIPT" run
	else
		echo "eix: 找不到 $DIAG_SCRIPT" >&2
		return 1
	fi
}
