2025-07-18 00:58:03 +08:00

467 lines
18 KiB
Bash

#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1
. "$IPKG_INSTROOT/lib/functions/network.sh"
. "$IPKG_INSTROOT/etc/nikki/scripts/include.sh"
extra_command 'update_subscription' 'Update subscription by section id'
boot() {
# prepare files
prepare_files
# load config
config_load nikki
# start delay
local enabled start_delay
config_get_bool enabled "config" "enabled" 0
config_get start_delay "config" "start_delay" 0
if [ "$enabled" = 1 ] && [ "$start_delay" -gt 0 ]; then
log "App" "Start after $start_delay seconds."
sleep "$start_delay"
fi
# start
start
}
start_service() {
# prepare files
prepare_files
# load config
config_load nikki
# check if enabled
local enabled
config_get_bool enabled "config" "enabled" 0
if [ "$enabled" = 0 ]; then
log "App" "Disabled."
log "App" "Exit."
return
fi
# start
log "App" "Enabled."
log "App" "Start."
# get config
## app config
local scheduled_restart cron_expression profile test_profile fast_reload core_only
config_get_bool scheduled_restart "config" "scheduled_restart" 0
config_get cron_expression "config" "cron_expression"
config_get profile "config" "profile"
config_get_bool test_profile "config" "test_profile" 0
config_get_bool fast_reload "config" "fast_reload" 0
config_get_bool core_only "config" "core_only" 0
## mixin config
### overwrite
local overwrite_authentication overwrite_tun_dns_hijack overwrite_fake_ip_filter overwrite_hosts overwrite_dns_nameserver overwrite_dns_nameserver_policy overwrite_sniffer_sniff overwrite_sniffer_force_domain_name overwrite_sniffer_ignore_domain_name
config_get_bool overwrite_authentication "mixin" "authentication" 0
config_get_bool overwrite_tun_dns_hijack "mixin" "tun_dns_hijack" 0
config_get_bool overwrite_fake_ip_filter "mixin" "fake_ip_filter" 0
config_get_bool overwrite_hosts "mixin" "hosts" 0
config_get_bool overwrite_dns_nameserver "mixin" "dns_nameserver" 0
config_get_bool overwrite_dns_nameserver_policy "mixin" "dns_nameserver_policy" 0
config_get_bool overwrite_sniffer_force_domain_name "mixin" "sniffer_force_domain_name" 0
config_get_bool overwrite_sniffer_ignore_domain_name "mixin" "sniffer_ignore_domain_name" 0
config_get_bool overwrite_sniffer_sniff "mixin" "sniffer_sniff" 0
### mixin file content
local mixin_file_content
config_get_bool mixin_file_content "mixin" "mixin_file_content" 0
## environment variable
local safe_paths disable_loopback_detector disable_quic_go_gso disable_quic_go_ecn skip_system_ipv6_check
config_get safe_paths "env" "safe_paths"
config_get_bool disable_loopback_detector "env" "disable_loopback_detector" 0
config_get_bool disable_quic_go_gso "env" "disable_quic_go_gso" 0
config_get_bool disable_quic_go_ecn "env" "disable_quic_go_ecn" 0
config_get_bool skip_system_ipv6_check "env" "skip_system_ipv6_check" 0
# get profile
local profile_type; profile_type=$(echo "$profile" | cut -d ':' -f 1)
local profile_id; profile_id=$(echo "$profile" | cut -d ':' -f 2)
if [ "$profile_type" = "file" ]; then
local profile_name; profile_name="$profile_id"
local profile_file; profile_file="$PROFILES_DIR/$profile_name"
log "Profile" "Use file: $profile_name."
if [ ! -f "$profile_file" ]; then
log "Profile" "File not found."
log "App" "Exit."
return
fi
cp -f "$profile_file" "$RUN_PROFILE_PATH"
elif [ "$profile_type" = "subscription" ]; then
local subscription_section; subscription_section="$profile_id"
local subscription_name subscription_prefer
config_get subscription_name "$subscription_section" "name"
config_get subscription_prefer "$subscription_section" "prefer" "remote"
log "Profile" "Use subscription: $subscription_name."
local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
if [ "$subscription_prefer" = "remote" ] || [ ! -f "$subscription_file" ]; then
update_subscription "$subscription_section"
fi
if [ ! -f "$subscription_file" ]; then
log "Profile" "Subscription file not found."
log "App" "Exit."
return
fi
cp -f "$subscription_file" "$RUN_PROFILE_PATH"
else
log "Profile" "No profile/subscription selected."
log "App" "Exit."
return
fi
# mixin
if [ "$core_only" = 0 ]; then
log "Mixin" "Mixin config."
if [ "$overwrite_authentication" = 1 ]; then
yq -M -i 'del(.authentication)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_tun_dns_hijack" = 1 ]; then
yq -M -i 'del(.tun.dns-hijack)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_fake_ip_filter" = 1 ]; then
yq -M -i 'del(.dns.fake-ip-filter)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_hosts" = 1 ]; then
yq -M -i 'del(.hosts)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_dns_nameserver" = 1 ]; then
yq -M -i 'del(.dns.default-nameserver) | del(.dns.proxy-server-nameserver) | del(.dns.direct-nameserver) | del(.dns.nameserver) | del(.dns.fallback) ' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_dns_nameserver_policy" = 1 ]; then
yq -M -i 'del(.dns.nameserver-policy)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_sniffer_force_domain_name" = 1 ]; then
yq -M -i 'del(.sniffer.force-domain)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_sniffer_ignore_domain_name" = 1 ]; then
yq -M -i 'del(.sniffer.skip-domain)' "$RUN_PROFILE_PATH"
fi
if [ "$overwrite_sniffer_sniff" = 1 ]; then
yq -M -i 'del(.sniffer.sniff)' "$RUN_PROFILE_PATH"
fi
if [ "$mixin_file_content" = 0 ]; then
ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" -
elif [ "$mixin_file_content" = 1 ]; then
ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" "$MIXIN_FILE_PATH" -
fi
fi
# test profile
if [ "$test_profile" = 1 ]; then
log "Profile" "Testing..."
if ($PROG -d "$RUN_DIR" -t >> "$CORE_LOG_PATH" 2>&1); then
log "Profile" "Test passed."
else
log "Profile" "Test failed."
log "Profile" "Please check the core log to find out the problem."
log "App" "Exit."
return
fi
fi
# start core
log "Core" "Start."
procd_open_instance nikki
procd_set_param command /bin/sh -c "$PROG -d $RUN_DIR >> $CORE_LOG_PATH 2>&1"
procd_set_param file "$RUN_PROFILE_PATH"
procd_append_param env SAFE_PATHS="$safe_paths"
procd_append_param env DISABLE_LOOPBACK_DETECTOR="$disable_loopback_detector"
procd_append_param env QUIC_GO_DISABLE_GSO="$disable_quic_go_gso"
procd_append_param env QUIC_GO_DISABLE_ECN="$disable_quic_go_ecn"
procd_append_param env SKIP_SYSTEM_IPV6_CHECK="$skip_system_ipv6_check"
if [ "$fast_reload" = 1 ]; then
procd_set_param reload_signal HUP
fi
procd_set_param pidfile "$PID_FILE_PATH"
procd_set_param respawn
procd_append_param limits core="unlimited"
procd_append_param limits nofile="1048576 1048576"
procd_close_instance
# cron
if [ "$scheduled_restart" = 1 ] && [ -n "$cron_expression" ]; then
log "App" "Set scheduled restart."
echo "$cron_expression /etc/init.d/nikki restart #nikki" >> "/etc/crontabs/root"
/etc/init.d/cron restart
fi
# set started flag
touch "$STARTED_FLAG_PATH"
}
service_started() {
# check if started
if [ ! -f "$STARTED_FLAG_PATH" ]; then
return
fi
# load config
config_load nikki
# check if proxy enabled
local enabled
config_get_bool enabled "proxy" "enabled" 0
if [ "$enabled" = 0 ]; then
log "Proxy" "Disabled."
return
fi
# get config
## app config
local core_only
config_get_bool core_only "config" "core_only" 0
## mixin
### tun
local tun_device
config_get tun_device "mixin" "tun_device" "nikki"
## proxy config
### general
local tcp_mode udp_mode ipv4_proxy ipv6_proxy tun_timeout tun_interval
config_get tcp_mode "proxy" "tcp_mode"
config_get udp_mode "proxy" "udp_mode"
config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0
config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0
config_get tun_timeout "proxy" "tun_timeout" 30
config_get tun_interval "proxy" "tun_interval" 1
## routing config
local tproxy_fw_mark tun_fw_mark tproxy_rule_pref tun_rule_pref tproxy_route_table tun_route_table cgroup_id cgroup_name
config_get tproxy_fw_mark "routing" "tproxy_fw_mark" "0x80"
config_get tun_fw_mark "routing" "tun_fw_mark" "0x81"
config_get tproxy_rule_pref "routing" "tproxy_rule_pref" "1024"
config_get tun_rule_pref "routing" "tun_rule_pref" "1025"
config_get tproxy_route_table "routing" "tproxy_route_table" "80"
config_get tun_route_table "routing" "tun_route_table" "81"
config_get cgroup_id "routing" "cgroup_id" "0x12061206"
config_get cgroup_name "routing" "cgroup_name" "nikki"
# prepare config
local tproxy_enable; tproxy_enable=0
if [ "$tcp_mode" = "tproxy" ] || [ "$udp_mode" = "tproxy" ]; then
tproxy_enable=1
fi
local tun_enable; tun_enable=0
if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then
tun_enable=1
fi
if [ "$core_only" = 0 ]; then
# proxy
log "Proxy" "Enabled."
# wait for tun device online
if [ "$tun_enable" = 1 ]; then
log "Proxy" "Waiting for tun device online within $tun_timeout seconds..."
while [ "$tun_timeout" -gt 0 ]; do
if (ip link show dev "$tun_device" > /dev/null 2>&1); then
if [ "$(ip -json addr show dev "$tun_device" | tun_device="$tun_device" yq -M '.[] | select(.ifname = strenv(tun_device)) | .addr_info | length')" -gt 0 ]; then
log "Proxy" "TUN device is online."
break
fi
fi
tun_timeout=$((tun_timeout - tun_interval))
sleep "$tun_interval"
done
if [ "$tun_timeout" -le 0 ]; then
log "Proxy" "Timeout, TUN device is not online."
log "App" "Exit."
return
fi
fi
# fix compatible with dockerd
## cgroupfs-mount
### when cgroupfs-mount is installed, cgroupv1 will mounted instead of cgroupv2, we need to create cgroup manually
if (mount | grep -q -w "^cgroup"); then
mkdir -p "/sys/fs/cgroup/net_cls/$cgroup_name"
echo "$cgroup_id" > "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid"
cat "$PID_FILE_PATH" > "/sys/fs/cgroup/net_cls/$cgroup_name/cgroup.procs"
fi
## kmod-br-netfilter
### when kmod-br-netfilter is loaded, bridge-nf-call-iptables and bridge-nf-call-ip6tables are set to 1, we need to set them to 0 if tproxy is enabled
if [ "$tproxy_enable" = 1 ] && (lsmod | grep -q br_netfilter); then
if [ "$ipv4_proxy" = 1 ]; then
local bridge_nf_call_iptables; bridge_nf_call_iptables=$(sysctl -e -n net.bridge.bridge-nf-call-iptables)
if [ "$bridge_nf_call_iptables" = 1 ]; then
touch "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH"
sysctl -q -w net.bridge.bridge-nf-call-iptables=0
fi
fi
if [ "$ipv6_proxy" = 1 ]; then
local bridge_nf_call_ip6tables; bridge_nf_call_ip6tables=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables)
if [ "$bridge_nf_call_ip6tables" = 1 ]; then
touch "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH"
sysctl -q -w net.bridge.bridge-nf-call-ip6tables=0
fi
fi
fi
# ip route and rule
if [ "$tproxy_enable" = 1 ]; then
if [ "$ipv4_proxy" = 1 ]; then
ip -4 route add local default dev lo table "$tproxy_route_table"
ip -4 rule add pref "$tproxy_rule_pref" fwmark "$tproxy_fw_mark" table "$tproxy_route_table"
fi
if [ "$ipv6_proxy" = 1 ]; then
ip -6 route add local default dev lo table "$tproxy_route_table"
ip -6 rule add pref "$tproxy_rule_pref" fwmark "$tproxy_fw_mark" table "$tproxy_route_table"
fi
fi
if [ "$tun_enable" = 1 ]; then
if [ "$ipv4_proxy" = 1 ]; then
ip -4 route add unicast default dev "$tun_device" table "$tun_route_table"
ip -4 rule add pref "$tun_rule_pref" fwmark "$tun_fw_mark" table "$tun_route_table"
fi
if [ "$ipv6_proxy" = 1 ]; then
ip -6 route add unicast default dev "$tun_device" table "$tun_route_table"
ip -6 rule add pref "$tun_rule_pref" fwmark "$tun_fw_mark" table "$tun_route_table"
fi
$FIREWALL_INCLUDE_SH
fi
# hijack
utpl -S "$HIJACK_UT" | nft -f -
# check hijack
if (nft list tables | grep -q nikki); then
log "Proxy" "Hijack successful."
else
log "Proxy" "Hijack failed."
log "App" "Exit."
fi
fi
}
service_stopped() {
cleanup
}
reload_service() {
cleanup
start
}
service_triggers() {
procd_add_reload_trigger "nikki"
}
cleanup() {
# clear log
clear_log
# load config
config_load nikki
# get config
## routing config
local tproxy_route_table tun_route_table
config_get tproxy_route_table "routing" "tproxy_route_table" "80"
config_get tun_route_table "routing" "tun_route_table" "81"
# delete routing policy
ip -4 rule del table "$tproxy_route_table" > /dev/null 2>&1
ip -4 rule del table "$tun_route_table" > /dev/null 2>&1
ip -6 rule del table "$tproxy_route_table" > /dev/null 2>&1
ip -6 rule del table "$tun_route_table" > /dev/null 2>&1
# delete routing table
ip -4 route flush table "$tproxy_route_table" > /dev/null 2>&1
ip -4 route flush table "$tun_route_table" > /dev/null 2>&1
ip -6 route flush table "$tproxy_route_table" > /dev/null 2>&1
ip -6 route flush table "$tun_route_table" > /dev/null 2>&1
# delete hijack
nft delete table inet nikki > /dev/null 2>&1
local handles handle
handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "input" and .comment == "nikki") | .handle')
for handle in $handles; do
nft delete rule inet fw4 input handle "$handle"
done
handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "forward" and .comment == "nikki") | .handle')
for handle in $handles; do
nft delete rule inet fw4 forward handle "$handle"
done
# delete started flag
rm "$STARTED_FLAG_PATH" > /dev/null 2>&1
# revert fix compatible with dockerd
## kmod-br-netfilter
if (rm "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" > /dev/null 2>&1); then
sysctl -q -w net.bridge.bridge-nf-call-iptables=1
fi
if (rm "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" > /dev/null 2>&1); then
sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1
fi
# delete cron
sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1
/etc/init.d/cron restart
}
update_subscription() {
local subscription_section; subscription_section="$1"
if [ -z "$subscription_section" ]; then
return
fi
# load config
config_load nikki
# get subscription config
local subscription_name subscription_url subscription_user_agent
config_get subscription_name "$subscription_section" "name"
config_get subscription_url "$subscription_section" "url"
config_get subscription_user_agent "$subscription_section" "user_agent"
# reset subscription info
uci_remove "nikki" "$subscription_section" "expire" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "upload" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "download" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "total" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "used" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "avaliable" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "update" > /dev/null 2>&1
uci_remove "nikki" "$subscription_section" "success" > /dev/null 2>&1
# update subscription
log "Profile" "Update subscription: $subscription_name."
local success
local subscription_header_tmpfile; subscription_header_tmpfile="$TEMP_DIR/$subscription_section.header"
local subscription_tmpfile; subscription_tmpfile="$TEMP_DIR/$subscription_section.yaml"
local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
if (curl -s -f -m 120 --connect-timeout 15 --retry 3 -L -X GET -A "$subscription_user_agent" -D "$subscription_header_tmpfile" -o "$subscription_tmpfile" "$subscription_url"); then
log "Profile" "Subscription download successful."
if (yq -p yaml -o yaml -e 'has("proxies") or has("proxy-providers")' "$subscription_tmpfile" > /dev/null 2>&1); then
log "Profile" "Subscription is valid."
success=1
else
log "Profile" "Subscription is not valid."
success=0
fi
else
log "Profile" "Subscription download failed."
success=0
fi
# check if success
if [ "$success" = 1 ]; then
log "Profile" "Subscription update successful."
local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable
subscription_expire=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
subscription_total=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "total=[[:digit:]]+" | cut -d '=' -f 2)
if [ -n "$subscription_upload" ] && [ -n "$subscription_download" ]; then
subscription_used=$((subscription_upload + subscription_download))
if [ -n "$subscription_total" ]; then
subscription_avaliable=$((subscription_total - subscription_used))
fi
fi
# update subscription info
if [ -n "$subscription_expire" ]; then
uci_set "nikki" "$subscription_section" "expire" "$(date "+%Y-%m-%d %H:%M:%S" -d "@$subscription_expire")"
fi
if [ -n "$subscription_upload" ]; then
uci_set "nikki" "$subscription_section" "upload" "$(format_filesize "$subscription_upload")"
fi
if [ -n "$subscription_download" ]; then
uci_set "nikki" "$subscription_section" "download" "$(format_filesize "$subscription_download")"
fi
if [ -n "$subscription_total" ]; then
uci_set "nikki" "$subscription_section" "total" "$(format_filesize "$subscription_total")"
fi
if [ -n "$subscription_used" ]; then
uci_set "nikki" "$subscription_section" "used" "$(format_filesize "$subscription_used")"
fi
if [ -n "$subscription_avaliable" ]; then
uci_set "nikki" "$subscription_section" "avaliable" "$(format_filesize "$subscription_avaliable")"
fi
uci_set "nikki" "$subscription_section" "update" "$(date "+%Y-%m-%d %H:%M:%S")"
uci_set "nikki" "$subscription_section" "success" "1"
# update subscription file
rm -f "$subscription_header_tmpfile"
mv -f "$subscription_tmpfile" "$subscription_file"
elif [ "$success" = 0 ]; then
log "Profile" "Subscription update failed."
# update subscription info
uci_set "nikki" "$subscription_section" "success" "0"
# remove tmpfile
rm -f "$subscription_header_tmpfile"
rm -f "$subscription_tmpfile"
fi
uci_commit "nikki"
}