#!/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 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 ## 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="${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 log "Mixin" "Mixin config." 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 pidfile "$PID_FILE_PATH" procd_set_param respawn 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_PATH" } service_started() { # check if started if [ ! -f "$STARTED_FLAG_PATH" ]; 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_proxy ipv6_proxy config_get tcp_transparent_proxy_mode "proxy" "tcp_transparent_proxy_mode" config_get udp_transparent_proxy_mode "proxy" "udp_transparent_proxy_mode" config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0 config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0 # prepare config 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 # 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 local cgroup_v1_path; cgroup_v1_path="/sys/fs/cgroup/net_cls/$CGROUP_NAME" mkdir -p "$cgroup_v1_path" echo "$CGROUP_ID" > "$cgroup_v1_path/net_cls.classid" cat "$PID_FILE_PATH" > "$cgroup_v1_path/cgroup.procs" # local bypass_cgroup; config_get bypass_cgroup "proxy" "bypass_cgroup" # if [ -n "$bypass_cgroup" ]; then # local cgroup # for cgroup in $bypass_cgroup; do # ubus call service list "{\"name\": \"$cgroup\"}" | jsonfilter -e "$.$cgroup.instances.*.pid" >> "$cgroup_v1_path/cgroup.procs" # done # fi 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 # transparent proxy log "Transparent Proxy" "Enabled." # wait for tun device online if [ "$tun_enable" == 1 ]; then log "Transparent Proxy" "Waiting for tun device online..." local tun_timeout; tun_timeout=15 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 # 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 -D cgroup_name="$CGROUP_NAME" -D cgroup_id="$CGROUP_ID" -D tproxy_fw_mark="$TPROXY_FW_MARK" -D tun_fw_mark="$TUN_FW_MARK" -S "$HIJACK_UT" | nft -f - # check hijack if (nft list tables | grep -q nikki); then log "Transparent Proxy" "Hijack successful." else log "Transparent Proxy" "Hijack failed." log "App" "Exit." 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_PATH" # revert fix compatible between tproxy and dockerd (kmod-br-netfilter) if [ -f "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" ]; then rm -f "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" sysctl -q -w net.bridge.bridge-nf-call-iptables=1 fi if [ -f "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" ]; then rm -f "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" 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="$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 --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 -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2) subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2) subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "download=[[:digit:]]+" | cut -d '=' -f 2) subscription_total=$(grep -i "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" }