#!/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" }