Files

544 lines
18 KiB
Bash

#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=90 # 确保在防火墙重载前停止
NAME="${NAME:-UAmask}"
PROG="${PROG:-/usr/bin/$NAME}"
PID_FILE="/var/run/$NAME.pid"
CORE_CONFIG_DIR="${CORE_CONFIG_DIR:-/var/run/$NAME}"
CORE_CONFIG_PATH="$CORE_CONFIG_DIR/config.json"
JSHN_LIB="${JSHN_LIB:-/usr/share/libubox/jshn.sh}"
# --- NFT (fw4) 变量 ---
FW_CONFIG_NAME="UAmask" # 防火墙 UCI 规则的名称
NFT_PATH="/tmp/UAmask_rules.nft" # .nft 文件的路径 (使用 /tmp/)
# --- IPT (fw3) 变量 ---
IPT="iptables"
CHAIN_PREROUTING="UAmask_prerouting"
CHAIN_OUTPUT="UAmask_output"
IPSET_NAME="UAmask_bypass_set"
# --- 防火墙检测 ---
FW_TYPE=""
detect_firewall() {
if command -v fw4 >/dev/null 2>&1; then
FW_TYPE="nft"
else
FW_TYPE="ipt"
fi
}
# 脚本加载时执行防火墙检测
detect_firewall
# NFTABLES (fw4) 函数
# 仅清理规则和 uci,不重载 (NFT helper)
unset_firewall_rules_nft() {
rm -f "${NFT_PATH}" # 清理生成的规则文件
uci -q delete firewall.${FW_CONFIG_NAME}
uci commit firewall
}
# 清理防火墙规则并重载 (NFT)
unset_firewall_nft() {
logger -t "$NAME" "Removing firewall rules (nft)..."
unset_firewall_rules_nft
nft delete chain inet fw4 UAmask_prerouting_before 2>/dev/null || true
nft delete chain inet fw4 UAmask_output_after 2>/dev/null || true
nft delete set inet fw4 ${IPSET_NAME} 2>/dev/null || true
fw4 reload >/dev/null 2>&1
logger -t "$NAME" "Firewall rules removed (nft)."
}
# 自动管理防火墙规则 (NFT)
set_firewall_nft() {
# 1. 确保旧规则被彻底清除
unset_firewall_rules_nft
# 2. 从 UCI 读取配置
config_load "$NAME"
local port
local iface_list
local bypass_gid
local bypass_ports_list
local bypass_ips_list
local force_replace
local proxy_host
local enable_firewall_set
config_get port "main" "port" "12032"
config_get iface_list "main" "iface" "br-lan"
config_get bypass_gid "main" "bypass_gid" "65533"
config_get bypass_ports_list "main" "bypass_ports" ""
config_get bypass_ips_list "main" "bypass_ips" ""
config_get_bool proxy_host "main" "proxy_host" ""
config_get_bool enable_firewall_set "main" "enable_firewall_set" "0"
# 3. 格式化接口
local nft_ifaces
set -- $iface_list
if [ "$#" -eq 1 ]; then
nft_ifaces="\"$1\"" # 单个接口
else
nft_ifaces="{ $(echo "$iface_list" | sed -e 's/ /", "/g' -e 's/^/"/' -e 's/$/"/') }" # 多个接口
fi
# 4. 格式化豁免 IPs
local nft_ips_rule
if [ -n "$bypass_ips_list" ]; then
local nft_ips
nft_ips="{ $(echo "$bypass_ips_list" | sed -e 's/ /, /g') }"
nft_ips_rule="ip daddr != $nft_ips \\"
else
nft_ips_rule="# No bypass IPs configured"
fi
# 5. 格式化豁免 Ports
local nft_ports_rule
if [ -n "$bypass_ports_list" ]; then
local nft_ports
nft_ports="{ $(echo "$bypass_ports_list" | sed -e 's/ /, /g') }"
nft_ports_rule="tcp dport != $nft_ports \\"
else
nft_ports_rule="# No bypass ports configured"
fi
> "${NFT_PATH}"
if [ "$enable_firewall_set" = "1" ] && ! command -v nft >/dev/null 2>&1; then
logger -t "$NAME" "Error: 'nft' command (nftables) is not installed. Domain bypass disabled."
enable_firewall_set="0"
fi
if [ "$enable_firewall_set" = "1" ]; then
logger -t "$NAME" "Ensuring nftables set '${IPSET_NAME}' exists..."
nft add set inet fw4 ${IPSET_NAME} "{ type ipv4_addr . inet_service ; timeout 10m ;}"
if [ $? -ne 0 ]; then
logger -t "$NAME" "Error: Failed to create nftables set '${IPSET_NAME}'. Domain bypass disabled."
enable_firewall_set="0"
fi
fi
# 6. 动态写入 .nft 文件
cat > "${NFT_PATH}" << EOF
chain UAmask_prerouting_before {
type nat hook prerouting priority dstnat - 1;
$( [ "$enable_firewall_set" = "1" ] && echo "iifname $nft_ifaces ip protocol tcp ip daddr . tcp dport @$IPSET_NAME return" )
iifname $nft_ifaces ip protocol tcp \\
$nft_ips_rule
$nft_ports_rule
redirect to :$port
}
EOF
if [ "$proxy_host" = "1" ]; then
cat >> "${NFT_PATH}" << EOF
chain UAmask_output_after {
type nat hook output priority -100;
$( [ "$enable_firewall_set" = "1" ] && echo "ip protocol tcp ip daddr . tcp dport @$IPSET_NAME return" )
ip protocol tcp \\
# 豁免局域网、环回、保留地址等
$nft_ips_rule
# 豁免端口
$nft_ports_rule
# 豁免 UAmask 自己的流量
# 同时豁免 OpenClash 的流量 (GID 65534),防止循环
meta skgid != { $bypass_gid, 65534 } \\
redirect to :$port
}
EOF
logger -t "$NAME" "proxy_host is enabled. Added UAmask_output_after chain."
else
logger -t "$NAME" "proxy_host is disabled. Skipping UAmask_output_after chain."
fi
logger -t "$NAME" "Generated nftables rules at ${NFT_PATH}"
# 7. 注册防火墙规则
uci set firewall.${FW_CONFIG_NAME}="include"
uci set firewall.${FW_CONFIG_NAME}.type="nftables"
uci set firewall.${FW_CONFIG_NAME}.path="${NFT_PATH}"
uci set firewall.${FW_CONFIG_NAME}.enabled="1"
uci commit firewall
nft delete chain inet fw4 UAmask_prerouting_before 2>/dev/null || true
nft delete chain inet fw4 UAmask_output_after 2>/dev/null || true
fw4 reload >/dev/null 2>&1
logger -t "$NAME" "Firewall rules applied (nft)."
}
# IPTABLES (fw3) 函数
# 清理防火墙规则并重载 (IPT)
unset_firewall_ipt() {
logger -t "$NAME" "Removing firewall rules (iptables)..."
# 循环尝试删除跳转规则,直到成功或失败
while $IPT -t nat -D PREROUTING -j $CHAIN_PREROUTING 2>/dev/null; do :; done
while $IPT -t nat -D OUTPUT -j $CHAIN_OUTPUT 2>/dev/null; do :; done
$IPT -t nat -F $CHAIN_PREROUTING 2>/dev/null || true
$IPT -t nat -X $CHAIN_PREROUTING 2>/dev/null || true
$IPT -t nat -F $CHAIN_OUTPUT 2>/dev/null || true
$IPT -t nat -X $CHAIN_OUTPUT 2>/dev/null || true
ipset destroy "$IPSET_NAME" 2>/dev/null || true
/etc/init.d/firewall reload >/dev/null 2>&1
logger -t "$NAME" "Firewall rules removed (iptables)."
}
# 自动管理防火墙规则 (IPT)
set_firewall_ipt() {
# 1. 确保旧规则被彻底清除
unset_firewall_ipt
# 2. 从 UCI 读取配置 (此部分不变)
config_load "$NAME"
local port
local iface_list
local bypass_gid
local bypass_ports_list
local bypass_ips_list
local proxy_host
local enable_firewall_set
config_get port "main" "port" "12032"
config_get iface_list "main" "iface" "br-lan"
config_get bypass_gid "main" "bypass_gid" "65533"
config_get bypass_ports_list "main" "bypass_ports" ""
config_get bypass_ips_list "main" "bypass_ips" ""
config_get_bool proxy_host "main" "proxy_host" ""
config_get_bool enable_firewall_set "main" "enable_firewall_set" "0"
if [ "$enable_firewall_set" = "1" ]; then
if ! command -v ipset >/dev/null 2>&1; then
logger -t "$NAME" "Error: 'ipset' package is not installed. Domain bypass disabled."
enable_firewall_set="0"
else
logger -t "$NAME" "Creating ipset '$IPSET_NAME' for domain bypass..."
ipset create "$IPSET_NAME" hash:ip,port timeout 600 -exist
fi
fi
# --- IPTABLES 规则设置 ---
# 3. 创建自定义链
$IPT -t nat -N $CHAIN_PREROUTING
$IPT -t nat -N $CHAIN_OUTPUT
if [ "$enable_firewall_set" = "1" ]; then
$IPT -t nat -A $CHAIN_PREROUTING -m set --match-set "$IPSET_NAME" dst,dst -j RETURN
fi
# 4. 填充 PREROUTING 链
for iface in $iface_list; do
for ip in $bypass_ips_list; do
$IPT -t nat -A $CHAIN_PREROUTING -i "$iface" -p tcp -d "$ip" -j RETURN
done
if [ -n "$bypass_ports_list" ]; then
local ipt_ports=$(echo "$bypass_ports_list" | sed 's/ /,/g')
$IPT -t nat -A $CHAIN_PREROUTING -i "$iface" -p tcp -m multiport --dports "$ipt_ports" -j RETURN
fi
# 将剩余的 TCP 流量重定向到代理端口
$IPT -t nat -A $CHAIN_PREROUTING -i "$iface" -p tcp -j REDIRECT --to-port "$port"
done
# 5. OUTPUT 链处理本机流量
if [ "$proxy_host" = "1" ]; then
# 域名绕过规则
if [ "$enable_firewall_set" = "1" ]; then
$IPT -t nat -A $CHAIN_OUTPUT -m set --match-set "$IPSET_NAME" dst -j RETURN
fi
# 豁免 UAmask 自己的流量 (通过 GID)
$IPT -t nat -A $CHAIN_OUTPUT -p tcp -m owner --gid-owner "$bypass_gid" -j RETURN
# 同时豁免 OpenClash 的流量 (GID 65534),防止循环
$IPT -t nat -A $CHAIN_OUTPUT -p tcp -m owner --gid-owner "65534" -j RETURN
# 豁免指定的目标 IP
for ip in $bypass_ips_list; do
$IPT -t nat -A $CHAIN_OUTPUT -p tcp -d "$ip" -j RETURN
done
# 豁免指定的端口
if [ -n "$bypass_ports_list" ]; then
local ipt_ports=$(echo "$bypass_ports_list" | sed 's/ /,/g')
$IPT -t nat -A $CHAIN_OUTPUT -p tcp -m multiport --dports "$ipt_ports" -j RETURN
fi
# 将剩余的 TCP 流量重定向到代理端口
$IPT -t nat -A $CHAIN_OUTPUT -p tcp -j REDIRECT --to-port "$port"
# 将自定义 OUTPUT 链挂载到内置的 OUTPUT 链
$IPT -t nat -I OUTPUT 1 -j $CHAIN_OUTPUT
logger -t "$NAME" "proxy_host is enabled. Added iptables rules for OUTPUT chain."
else
logger -t "$NAME" "proxy_host is disabled. Skipping OUTPUT chain rules."
fi
# 6. 将自定义 PREROUTING 链挂载到内置的 PREROUTING 链
$IPT -t nat -I PREROUTING 1 -j $CHAIN_PREROUTING
logger -t "$NAME" "Firewall rules (iptables) applied."
}
# 通用封装函数
set_firewall() {
if [ "$FW_TYPE" = "nft" ]; then
set_firewall_nft
else
set_firewall_ipt
fi
}
unset_firewall() {
if [ "$FW_TYPE" = "nft" ]; then
unset_firewall_nft
else
unset_firewall_ipt
fi
}
# 通用服务函数 (启动、停止、组设置)
# 检查并创建专用组
setup_group() {
config_load "$NAME"
local bypass_gid
config_get bypass_gid "main" "bypass_gid" "65533"
if ! grep -q ":$bypass_gid:" /etc/group; then
echo "UAmask:x:$bypass_gid:" >> /etc/group
fi
}
trim_config_value() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
json_add_csv_array() {
local name="$1"
local remaining="$2"
local item
json_add_array "$name"
while [ -n "$remaining" ]; do
case "$remaining" in
*,*)
item="${remaining%%,*}"
remaining="${remaining#*,}"
;;
*)
item="$remaining"
remaining=""
;;
esac
item="$(trim_config_value "$item")"
[ -n "$item" ] && json_add_string "" "$item"
done
json_close_array
}
generate_core_config() {
local tmp_config="$CORE_CONFIG_PATH.tmp.$$"
local validation_output
local port ua log_level log_file whitelist
local match_mode keywords ua_regex replace_method partial_replace
local enable_firewall_set firewall_ua_whitelist firewall_ua_bypass firewall_drop_on_match
local firewall_advanced_settings firewall_nonhttp_threshold firewall_timeout firewall_decision_delay
local operating_profile cache_size buffer_size pool_size gogc_value
if [ ! -r "$JSHN_LIB" ]; then
logger -t "$NAME" "Cannot generate core config: jshn.sh is unavailable."
return 1
fi
. "$JSHN_LIB"
config_get port "main" "port" "12032"
config_get ua "main" "ua" "FFF"
config_get log_level "main" "log_level" "info"
config_get log_file "main" "log_file" "/tmp/UAmask/UAmask.log"
config_get whitelist "main" "whitelist" ""
config_get match_mode "main" "match_mode" "keywords"
config_get keywords "main" "keywords" "iPhone,iPad,Android,Macintosh,Windows"
config_get ua_regex "main" "ua_regex" "(iPhone|iPad|Android|Macintosh|Windows|Linux)"
config_get replace_method "main" "replace_method" "full"
partial_replace=0
[ "$replace_method" = "partial" ] && partial_replace=1
config_get_bool enable_firewall_set "main" "enable_firewall_set" "0"
config_get firewall_ua_whitelist "main" "Firewall_ua_whitelist" ""
config_get_bool firewall_ua_bypass "main" "Firewall_ua_bypass" "0"
config_get_bool firewall_drop_on_match "main" "Firewall_drop_on_match" "0"
config_get_bool firewall_advanced_settings "main" "firewall_advanced_settings" "0"
config_get firewall_nonhttp_threshold "main" "firewall_nonhttp_threshold" "5"
config_get firewall_timeout "main" "firewall_timeout" "28800"
config_get firewall_decision_delay "main" "firewall_decision_delay" "60"
if [ "$enable_firewall_set" != "1" ]; then
firewall_ua_whitelist=""
firewall_ua_bypass=0
firewall_drop_on_match=0
fi
if [ "$firewall_advanced_settings" != "1" ]; then
firewall_nonhttp_threshold=5
firewall_timeout=28800
firewall_decision_delay=60
else
# Preserve the safety floors used by the legacy flag-based launcher.
[ "$firewall_nonhttp_threshold" -ge 1 ] 2>/dev/null || firewall_nonhttp_threshold=5
[ "$firewall_timeout" -ge 60 ] 2>/dev/null || firewall_timeout=28800
[ "$firewall_decision_delay" -ge 10 ] 2>/dev/null || firewall_decision_delay=60
fi
config_get operating_profile "main" "operating_profile" "Medium"
config_get cache_size "main" "cache_size" "2000"
config_get buffer_size "main" "buffer_size" "8192"
config_get pool_size "main" "pool_size" "0"
config_get gogc_value "main" "gogc_value" "100"
umask 077
mkdir -p "$CORE_CONFIG_DIR" || return 1
mkdir -p "$(dirname "$log_file")" || return 1
json_init
json_add_int "schema_version" 1
json_add_object "listen"
json_add_int "port" "$port"
json_add_string "dial_timeout" "30s"
json_add_string "client_keep_alive" "3m"
json_add_string "upstream_keep_alive" "3m"
json_close_object
json_add_object "rewrite"
json_add_string "user_agent" "$ua"
json_add_string "mode" "$match_mode"
json_add_csv_array "keywords" "$keywords"
json_add_csv_array "whitelist" "$whitelist"
json_add_string "pattern" "$ua_regex"
json_add_boolean "partial_replace" "$partial_replace"
json_close_object
json_add_object "firewall"
json_add_string "backend" "$FW_TYPE"
json_add_string "set_name" "$IPSET_NAME"
json_add_csv_array "ua_whitelist" "$firewall_ua_whitelist"
json_add_boolean "drop_on_match" "$firewall_drop_on_match"
json_add_string "whitelist_timeout" "24h"
json_add_object "non_http"
json_add_boolean "enabled" "$firewall_ua_bypass"
json_add_int "threshold" "$firewall_nonhttp_threshold"
json_add_string "decision_delay" "${firewall_decision_delay}s"
json_add_string "http_cooldown" "1h"
json_add_string "timeout" "${firewall_timeout}s"
json_add_string "cleanup_interval" "10m"
json_close_object
json_close_object
json_add_object "observe"
json_add_string "log_level" "$log_level"
json_add_string "log_file" "$log_file"
json_add_string "stats_file" "/tmp/UAmask.stats"
json_add_string "stats_interval" "5s"
json_close_object
json_add_object "performance"
json_add_string "profile" "$operating_profile"
json_add_int "cache_size" "$cache_size"
json_add_int "buffer_size" "$buffer_size"
json_add_int "pool_size" "$pool_size"
json_add_int "gc_percent" "$gogc_value"
json_close_object
if ! json_dump > "$tmp_config"; then
logger -t "$NAME" "Failed to serialize core configuration."
rm -f "$tmp_config"
return 1
fi
validation_output="$("$PROG" -check-config -config "$tmp_config" 2>&1)"
if [ $? -ne 0 ]; then
logger -t "$NAME" "Core configuration validation failed: $validation_output"
rm -f "$tmp_config"
return 1
fi
if ! chmod 600 "$tmp_config"; then
logger -t "$NAME" "Failed to secure generated core configuration."
rm -f "$tmp_config"
return 1
fi
if ! mv -f "$tmp_config" "$CORE_CONFIG_PATH"; then
logger -t "$NAME" "Failed to install core configuration."
rm -f "$tmp_config"
return 1
fi
return 0
}
start_service() {
logger -t "$NAME" "Starting $NAME with firewall $FW_TYPE..."
config_load "$NAME"
local enabled proxy_host
config_get_bool enabled "enabled" "enabled" "0"
[ "$enabled" -eq 1 ] || return 1
config_get_bool proxy_host "main" "proxy_host" "0"
if [ "$proxy_host" = "1" ]; then
setup_group
fi
if ! generate_core_config; then
logger -t "$NAME" "Refusing to start with invalid core configuration."
return 1
fi
procd_open_instance "$NAME"
procd_set_param command "$PROG" -config "$CORE_CONFIG_PATH"
procd_set_param file "$CORE_CONFIG_PATH"
procd_set_param limits nofile="65536 65536"
if [ "$proxy_host" = "1" ]; then
procd_set_param group "UAmask"
logger -t "$NAME" "Running as group 'UAmask' for host proxying."
else
logger -t "$NAME" "Running as default user (proxy_host is disabled)."
fi
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
set_firewall
}
stop_service() {
logger -t "$NAME" "Stopping $NAME..."
# 停止服务时自动清理防火墙
unset_firewall
# procd 会自动使用 pidfile 停止 start-stop-daemon
}
service_triggers() {
procd_add_reload_trigger "$NAME"
}