436 lines
17 KiB
Bash
436 lines
17 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 mixin test_profile fast_reload
|
|
config_get_bool scheduled_restart "config" "scheduled_restart" 0
|
|
config_get cron_expression "config" "cron_expression"
|
|
config_get profile "config" "profile"
|
|
config_get_bool mixin "config" "mixin" 0
|
|
config_get_bool test_profile "config" "test_profile" 0
|
|
config_get_bool fast_reload "config" "fast_reload" 0
|
|
## mixin config
|
|
### mixin file content
|
|
local mixin_file_content
|
|
config_get_bool mixin_file_content "mixin" "mixin_file_content" 0
|
|
## environment variable
|
|
local disable_safe_path_check disable_loopback_detector disable_quic_go_gso disable_quic_go_ecn
|
|
config_get_bool disable_safe_path_check "env" "disable_safe_path_check" 0
|
|
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
|
|
# get profile
|
|
if [[ "$profile" == "file:"* ]]; then
|
|
local profile_name; profile_name=$(basename "${profile/file:/}")
|
|
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" == "subscription:"* ]]; then
|
|
local subscription_section; subscription_section="${profile/subscription:/}"
|
|
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" ] || [[ "$subscription_prefer" == "local" && ! -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 [ "$mixin" == 0 ]; then
|
|
log "Mixin" "Disabled."
|
|
log "Mixin" "Mixin neccesary config."
|
|
else
|
|
log "Mixin" "Enabled."
|
|
log "Mixin" "Mixin all config."
|
|
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
|
|
# 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_set_param env SKIP_SAFE_PATH_CHECK="$disable_safe_path_check" DISABLE_LOOPBACK_DETECTOR="$disable_loopback_detector" QUIC_GO_DISABLE_GSO="$disable_quic_go_gso" QUIC_GO_DISABLE_ECN="$disable_quic_go_ecn"
|
|
if [ "$fast_reload" == 1 ]; then
|
|
procd_set_param reload_signal HUP
|
|
fi
|
|
procd_set_param respawn
|
|
procd_set_param user "$NIKKI_USER"
|
|
procd_set_param group "$NIKKI_GROUP"
|
|
|
|
procd_set_param limits core="unlimited" 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"
|
|
}
|
|
|
|
service_started() {
|
|
# check if started
|
|
if [ ! -f "$STARTED_FLAG" ]; then
|
|
return
|
|
fi
|
|
# load config
|
|
config_load nikki
|
|
# check if transparent proxy enabled
|
|
local transparent_proxy
|
|
config_get_bool transparent_proxy "proxy" "transparent_proxy" 0
|
|
if [ "$transparent_proxy" == 0 ]; then
|
|
log "Transparent Proxy" "Disabled."
|
|
return
|
|
fi
|
|
# get config
|
|
## mixin
|
|
### tun
|
|
local tun_device
|
|
config_get tun_device "mixin" "tun_device" "nikki"
|
|
## proxy config
|
|
### transparent proxy
|
|
local tcp_transparent_proxy_mode udp_transparent_proxy_mode ipv4_dns_hijack ipv6_dns_hijack ipv4_proxy ipv6_proxy router_proxy lan_proxy
|
|
config_get tcp_transparent_proxy_mode "proxy" "tcp_transparent_proxy_mode" "redirect"
|
|
config_get udp_transparent_proxy_mode "proxy" "udp_transparent_proxy_mode" "tun"
|
|
config_get_bool ipv4_dns_hijack "proxy" "ipv4_dns_hijack" 0
|
|
config_get_bool ipv6_dns_hijack "proxy" "ipv6_dns_hijack" 0
|
|
config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0
|
|
config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0
|
|
config_get_bool router_proxy "proxy" "router_proxy" 0
|
|
config_get_bool lan_proxy "proxy" "lan_proxy" 0
|
|
### access control
|
|
local access_control_mode
|
|
config_get access_control_mode "proxy" "access_control_mode"
|
|
### bypass
|
|
local bypass_user bypass_group bypass_china_mainland_ip proxy_tcp_dport proxy_udp_dport bypass_dscp
|
|
config_get bypass_user "proxy" "bypass_user"
|
|
config_get bypass_group "proxy" "bypass_group"
|
|
config_get_bool bypass_china_mainland_ip "proxy" "bypass_china_mainland_ip" 0
|
|
config_get proxy_tcp_dport "proxy" "proxy_tcp_dport" "0-65535"
|
|
config_get proxy_udp_dport "proxy" "proxy_udp_dport" "0-65535"
|
|
config_get bypass_dscp "proxy" "bypass_dscp"
|
|
# prepare
|
|
local tproxy_enable; tproxy_enable=0
|
|
if [[ "$tcp_transparent_proxy_mode" == "tproxy" || "$udp_transparent_proxy_mode" == "tproxy" ]]; then
|
|
tproxy_enable=1
|
|
fi
|
|
local tun_enable; tun_enable=0
|
|
if [[ "$tcp_transparent_proxy_mode" == "tun" || "$udp_transparent_proxy_mode" == "tun" ]]; then
|
|
tun_enable=1
|
|
fi
|
|
# transparent proxy
|
|
log "Transparent Proxy" "Enabled."
|
|
log "Transparent Proxy" "TCP Mode: $tcp_transparent_proxy_mode."
|
|
log "Transparent Proxy" "UDP Mode: $udp_transparent_proxy_mode."
|
|
# wait for tun device online
|
|
if [ "$tun_enable" == 1 ]; then
|
|
log "Transparent Proxy" "Waiting for tun device online..."
|
|
local tun_timeout; tun_timeout=60
|
|
local tun_interval; tun_interval=1
|
|
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 "Transparent 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 "Transparent Proxy" "Waiting timeout, tun device is not online."
|
|
log "App" "Exit."
|
|
return
|
|
fi
|
|
fi
|
|
# prepare
|
|
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
|
|
utpl -D nikki_group="$NIKKI_GROUP" -D tproxy_fw_mark="$TPROXY_FW_MARK" -D tun_fw_mark="$TUN_FW_MARK" -S "$HIJACK_UT" | nft -f -
|
|
# dns hijack
|
|
if [ "$ipv4_dns_hijack" == 1 ]; then
|
|
log "Transparent Proxy" "Hijack IPv4 dns request."
|
|
fi
|
|
if [ "$ipv6_dns_hijack" == 1 ]; then
|
|
log "Transparent Proxy" "Hijack IPv6 dns request."
|
|
fi
|
|
# proxy
|
|
if [ "$ipv4_proxy" == 1 ]; then
|
|
log "Transparent Proxy" "Proxy IPv4 traffic."
|
|
fi
|
|
if [ "$ipv6_proxy" == 1 ]; then
|
|
log "Transparent Proxy" "Proxy IPv6 traffic."
|
|
fi
|
|
# bypass
|
|
if [ -n "$bypass_user" ]; then
|
|
log "Transparent Proxy" "Bypass user: $bypass_user."
|
|
fi
|
|
if [ -n "$bypass_group" ]; then
|
|
log "Transparent Proxy" "Bypass group: $bypass_group."
|
|
fi
|
|
if [ "$bypass_china_mainland_ip" == 1 ]; then
|
|
log "Transparent Proxy" "Bypass china mainland ip."
|
|
fi
|
|
log "Transparent Proxy" "Destination TCP Port to Proxy: $proxy_tcp_dport."
|
|
log "Transparent Proxy" "Destination UDP Port to Proxy: $proxy_udp_dport."
|
|
if [ -n "$bypass_dscp" ]; then
|
|
log "Transparent Proxy" "Bypass DSCP: $bypass_dscp."
|
|
fi
|
|
# router proxy
|
|
if [ "$router_proxy" == 1 ]; then
|
|
log "Transparent Proxy" "Set proxy for router."
|
|
fi
|
|
# lan proxy
|
|
if [ "$lan_proxy" == 1 ]; then
|
|
log "Transparent Proxy" "Set proxy for lan."
|
|
# access control
|
|
if [ "$access_control_mode" == "all" ]; then
|
|
log "Transparent Proxy" "Access Control is using all mode, set proxy for all client."
|
|
elif [ "$access_control_mode" == "allow" ]; then
|
|
log "Transparent Proxy" "Access Control is using allow mode, set proxy for client which is in acl."
|
|
elif [ "$access_control_mode" == "block" ]; then
|
|
log "Transparent Proxy" "Access Control is using block mode, set proxy for client which is not in acl."
|
|
fi
|
|
fi
|
|
# fix compatible between tproxy and dockerd (kmod-br-netfilter)
|
|
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"
|
|
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"
|
|
sysctl -q -w net.bridge.bridge-nf-call-ip6tables=0
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
service_stopped() {
|
|
cleanup
|
|
}
|
|
|
|
reload_service() {
|
|
cleanup
|
|
start
|
|
}
|
|
|
|
service_triggers() {
|
|
procd_add_reload_trigger "nikki"
|
|
}
|
|
|
|
cleanup() {
|
|
# clear log
|
|
clear_log
|
|
# 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 -f "$STARTED_FLAG"
|
|
# revert fix compatible between tproxy and dockerd (kmod-br-netfilter)
|
|
if [ -f "$BRIDGE_NF_CALL_IPTABLES_FLAG" ]; then
|
|
rm -f "$BRIDGE_NF_CALL_IPTABLES_FLAG"
|
|
sysctl -q -w net.bridge.bridge-nf-call-iptables=1
|
|
fi
|
|
if [ -f "$BRIDGE_NF_CALL_IP6TABLES_FLAG" ]; then
|
|
rm -f "$BRIDGE_NF_CALL_IP6TABLES_FLAG"
|
|
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"
|
|
uci_remove "nikki" "$subscription_section" "upload"
|
|
uci_remove "nikki" "$subscription_section" "download"
|
|
uci_remove "nikki" "$subscription_section" "total"
|
|
uci_remove "nikki" "$subscription_section" "used"
|
|
uci_remove "nikki" "$subscription_section" "avaliable"
|
|
uci_remove "nikki" "$subscription_section" "update"
|
|
uci_remove "nikki" "$subscription_section" "success"
|
|
# update subscription
|
|
log "Profile" "Update subscription: $subscription_name."
|
|
local subscription_header_tmpfile; subscription_header_tmpfile="/tmp/$subscription_section.header"
|
|
local subscription_tmpfile; subscription_tmpfile="/tmp/$subscription_section.yaml"
|
|
local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
|
|
if (curl -s -f --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 update successful."
|
|
local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable
|
|
subscription_expire=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
|
|
subscription_upload=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
|
|
subscription_download=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
|
|
subscription_total=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -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_upload - subscription_download))
|
|
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"
|
|
else
|
|
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"
|
|
}
|