├── root ├── etc │ ├── homeproxy │ │ ├── resources │ │ │ ├── gfw_list.ver │ │ │ ├── china_ip4.ver │ │ │ ├── china_ip6.ver │ │ │ ├── china_list.ver │ │ │ └── china_ip6.txt │ │ └── scripts │ │ │ ├── update_crond.sh │ │ │ ├── clean_log.sh │ │ │ ├── firewall_pre.uc │ │ │ ├── update_resources.sh │ │ │ ├── homeproxy.uc │ │ │ ├── generate_server.uc │ │ │ ├── migrate_config.uc │ │ │ ├── update_subscriptions.uc │ │ │ ├── firewall_post.ut │ │ │ └── generate_client.uc │ ├── uci-defaults │ │ ├── luci-homeproxy-migration │ │ └── luci-homeproxy │ ├── capabilities │ │ └── homeproxy.json │ ├── config │ │ └── homeproxy │ └── init.d │ │ └── homeproxy └── usr │ └── share │ ├── rpcd │ ├── acl.d │ │ └── luci-app-homeproxy.json │ └── ucode │ │ └── luci.homeproxy │ └── luci │ └── menu.d │ └── luci-app-homeproxy.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── bug-report.yml ├── rescan-translation.sh ├── update-geodata.sh ├── workflows │ └── build-ipk.yml └── build-ipk.sh ├── README ├── Makefile ├── htdocs └── luci-static │ └── resources │ ├── view │ └── homeproxy │ │ └── status.js │ └── homeproxy.js └── LICENSE /root/etc/homeproxy/resources/gfw_list.ver: -------------------------------------------------------------------------------- 1 | 202512132213 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip4.ver: -------------------------------------------------------------------------------- 1 | 20251213034233 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip6.ver: -------------------------------------------------------------------------------- 1 | 20251214035524 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_list.ver: -------------------------------------------------------------------------------- 1 | 202512132213 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | -------------------------------------------------------------------------------- /root/etc/uci-defaults/luci-homeproxy-migration: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ucode "/etc/homeproxy/scripts/migrate_config.uc" 4 | 5 | exit 0 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | TODO: 2 | - Subscription page slow response with a large number of nodes 3 | - Refactor nft rules 4 | - Move ACL settings to a dedicated page 5 | - Any other improvements 6 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/update_crond.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2023 ImmortalWrt.org 5 | 6 | SCRIPTS_DIR="/etc/homeproxy/scripts" 7 | 8 | for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do 9 | "$SCRIPTS_DIR"/update_resources.sh "$i" 10 | done 11 | 12 | "$SCRIPTS_DIR"/update_subscriptions.uc 13 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/clean_log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2022-2023 ImmortalWrt.org 5 | 6 | NAME="homeproxy" 7 | 8 | log_max_size="50" #KB 9 | main_log_file="/var/run/$NAME/$NAME.log" 10 | singc_log_file="/var/run/$NAME/sing-box-c.log" 11 | sings_log_file="/var/run/$NAME/sing-box-s.log" 12 | 13 | while true; do 14 | sleep 180 15 | for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do 16 | [ -s "$i" ] || continue 17 | [ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i" 18 | done 19 | done 20 | -------------------------------------------------------------------------------- /.github/rescan-translation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR="$(cd "$(dirname $0)"; pwd)" 4 | LUCI_DIR="$BASE_DIR/../../luci" 5 | 6 | if [ -d "$LUCI_DIR" ]; then 7 | perl "$LUCI_DIR/build/i18n-scan.pl" . > "$BASE_DIR/../po/templates/homeproxy.pot" 8 | perl "$LUCI_DIR/build/i18n-update.pl" "$BASE_DIR/../po" 9 | else 10 | LUCI_URL="https://raw.githubusercontent.com/openwrt/luci/691574263356689912c5bd31984bb1b96417a847" 11 | perl <(curl -fs "$LUCI_URL/build/i18n-scan.pl") . > "$BASE_DIR/../po/templates/homeproxy.pot" 12 | perl <(curl -fs "$LUCI_URL/build/i18n-update.pl") "$BASE_DIR/../po" 13 | fi 14 | find "$BASE_DIR/../po" -name '*.po~' -delete; 15 | -------------------------------------------------------------------------------- /root/etc/capabilities/homeproxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "bounding": [ 3 | "CAP_NET_ADMIN", 4 | "CAP_NET_BIND_SERVICE", 5 | "CAP_NET_RAW", 6 | "CAP_SYS_PTRACE" 7 | ], 8 | "effective": [ 9 | "CAP_NET_ADMIN", 10 | "CAP_NET_BIND_SERVICE", 11 | "CAP_NET_RAW", 12 | "CAP_SYS_PTRACE" 13 | ], 14 | "ambient": [ 15 | "CAP_NET_ADMIN", 16 | "CAP_NET_BIND_SERVICE", 17 | "CAP_NET_RAW", 18 | "CAP_SYS_PTRACE" 19 | ], 20 | "permitted": [ 21 | "CAP_NET_ADMIN", 22 | "CAP_NET_BIND_SERVICE", 23 | "CAP_NET_RAW", 24 | "CAP_SYS_PTRACE" 25 | ], 26 | "inheritable": [ 27 | "CAP_NET_ADMIN", 28 | "CAP_NET_BIND_SERVICE", 29 | "CAP_NET_RAW", 30 | "CAP_SYS_PTRACE" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/acl.d/luci-app-homeproxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-homeproxy": { 3 | "description": "Grant access to homeproxy configuration", 4 | "read": { 5 | "file": { 6 | "/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ], 7 | "/var/run/homeproxy/homeproxy.log": [ "read" ], 8 | "/var/run/homeproxy/sing-box-c.log": [ "read" ], 9 | "/var/run/homeproxy/sing-box-s.log": [ "read" ] 10 | }, 11 | "ubus": { 12 | "service": [ "list" ], 13 | "luci.homeproxy": [ "*" ] 14 | }, 15 | "uci": [ "homeproxy" ] 16 | }, 17 | "write": { 18 | "file": { 19 | "/tmp/homeproxy_certificate.tmp": [ "write" ] 20 | }, 21 | "uci": [ "homeproxy" ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-only 2 | # 3 | # Copyright (C) 2022-2023 ImmortalWrt.org 4 | 5 | include $(TOPDIR)/rules.mk 6 | 7 | LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64 8 | LUCI_PKGARCH:=all 9 | LUCI_DEPENDS:= \ 10 | +sing-box \ 11 | +firewall4 \ 12 | +kmod-nft-tproxy \ 13 | +ucode-mod-digest 14 | 15 | PKG_NAME:=luci-app-homeproxy 16 | 17 | define Package/luci-app-homeproxy/conffiles 18 | /etc/config/homeproxy 19 | /etc/homeproxy/certs/ 20 | /etc/homeproxy/ruleset/ 21 | /etc/homeproxy/resources/direct_list.txt 22 | /etc/homeproxy/resources/proxy_list.txt 23 | endef 24 | 25 | include $(TOPDIR)/feeds/luci/luci.mk 26 | 27 | # call BuildPackage - OpenWrt buildroot signature 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new feature or enhancement. 3 | labels: 4 | - enhancement 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Feature description 10 | description: A clear and concise description of the request. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: expected 15 | attributes: 16 | label: Expected behavior 17 | description: A clear and concise description of what you expected to happen. 18 | - type: textarea 19 | id: additional 20 | attributes: 21 | label: Additional information 22 | description: Add any additional info you think might be helfpul. 23 | -------------------------------------------------------------------------------- /root/usr/share/luci/menu.d/luci-app-homeproxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/homeproxy": { 3 | "title": "HomeProxy", 4 | "order": 10, 5 | "action": { 6 | "type": "firstchild" 7 | }, 8 | "depends": { 9 | "acl": [ "luci-app-homeproxy" ], 10 | "uci": { "homeproxy": true } 11 | } 12 | }, 13 | "admin/services/homeproxy/client": { 14 | "title": "Client Settings", 15 | "order": 10, 16 | "action": { 17 | "type": "view", 18 | "path": "homeproxy/client" 19 | } 20 | }, 21 | "admin/services/homeproxy/node": { 22 | "title": "Node Settings", 23 | "order": 15, 24 | "action": { 25 | "type": "view", 26 | "path": "homeproxy/node" 27 | } 28 | }, 29 | "admin/services/homeproxy/server": { 30 | "title": "Server Settings", 31 | "order": 20, 32 | "action": { 33 | "type": "view", 34 | "path": "homeproxy/server" 35 | } 36 | }, 37 | "admin/services/homeproxy/status": { 38 | "title": "Service Status", 39 | "order": 30, 40 | "action": { 41 | "type": "view", 42 | "path": "homeproxy/status" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /root/etc/uci-defaults/luci-homeproxy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -f "/www/luci-static/resources/icons/loading.gif" ] && \ 4 | sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy/status.js" 5 | 6 | uci -q batch <<-EOF >"/dev/null" 7 | delete firewall.homeproxy_pre 8 | 9 | delete firewall.homeproxy_forward 10 | set firewall.homeproxy_forward=include 11 | set firewall.homeproxy_forward.type=nftables 12 | set firewall.homeproxy_forward.path="/var/run/homeproxy/fw4_forward.nft" 13 | set firewall.homeproxy_forward.position="chain-pre" 14 | set firewall.homeproxy_forward.chain="forward" 15 | 16 | delete firewall.homeproxy_input 17 | set firewall.homeproxy_input=include 18 | set firewall.homeproxy_input.type=nftables 19 | set firewall.homeproxy_input.path="/var/run/homeproxy/fw4_input.nft" 20 | set firewall.homeproxy_input.position="chain-pre" 21 | set firewall.homeproxy_input.chain="input" 22 | 23 | delete firewall.homeproxy_post 24 | set firewall.homeproxy_post=include 25 | set firewall.homeproxy_post.type=nftables 26 | set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft" 27 | set firewall.homeproxy_post.position="table-post" 28 | commit firewall 29 | EOF 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/firewall_pre.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | 3 | 'use strict'; 4 | 5 | import { writefile } from 'fs'; 6 | import { cursor } from 'uci'; 7 | import { isEmpty, RUN_DIR } from 'homeproxy'; 8 | 9 | const cfgname = 'homeproxy'; 10 | const uci = cursor(); 11 | uci.load(cfgname); 12 | 13 | const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china', 14 | proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy'; 15 | 16 | let outbound_node, tun_name; 17 | if (match(proxy_mode, /tun/)) { 18 | if (routing_mode === 'custom') 19 | outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil'; 20 | else 21 | outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil'; 22 | 23 | if (outbound_node !== 'nil') 24 | tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0'; 25 | } 26 | 27 | const server_enabled = uci.get(cfgname, 'server', 'enabled'); 28 | 29 | let forward = [], 30 | input = []; 31 | 32 | if (tun_name) { 33 | push(forward, `oifname ${tun_name} counter accept comment "!${cfgname}: accept tun forward"`); 34 | push(input ,`iifname ${tun_name} counter accept comment "!${cfgname}: accept tun input"`); 35 | } 36 | 37 | if (server_enabled === '1') { 38 | uci.foreach(cfgname, 'server', (s) => { 39 | if (s.enabled !== '1' || s.firewall !== '1') 40 | return; 41 | 42 | let proto = s.network || '{ tcp, udp }'; 43 | push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`); 44 | }); 45 | } 46 | 47 | if (!isEmpty(forward)) 48 | writefile(RUN_DIR + '/fw4_forward.nft', join('\n', forward) + '\n'); 49 | 50 | if (!isEmpty(input)) 51 | writefile(RUN_DIR + '/fw4_input.nft', join('\n', input) + '\n'); 52 | -------------------------------------------------------------------------------- /.github/update-geodata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR="$(cd "$(dirname $0)"; pwd)" 4 | RESOURCES_DIR="$BASE_DIR/../root/etc/homeproxy/resources" 5 | 6 | TEMP_DIR="$(mktemp -d -p $BASE_DIR)" 7 | 8 | check_list_update() { 9 | local listtype="$1" 10 | local listrepo="$2" 11 | local listref="$3" 12 | local listname="$4" 13 | 14 | local list_info="$(gh api "repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")" 15 | local list_sha="$(echo -e "$list_info" | jq -r ".[].sha")" 16 | local list_ver="$(echo -e "$list_info" | jq -r ".[].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')" 17 | if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then 18 | echo -e "[${listtype^^}] Failed to get the latest version, please retry later." 19 | return 1 20 | fi 21 | 22 | local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")" 23 | if [ "$local_list_ver" = "$list_ver" ]; then 24 | echo -e "[${listtype^^}] Current version: $list_ver." 25 | echo -e "[${listtype^^}] You're already at the latest version." 26 | return 3 27 | else 28 | echo -e "[${listtype^^}] Local version: $local_list_ver, latest version: $list_ver." 29 | fi 30 | 31 | if ! curl -fsSL "https://raw.githubusercontent.com/$listrepo/$list_sha/$listname" -o "$TEMP_DIR/$listname" || [ ! -s "$TEMP_DIR/$listname" ]; then 32 | rm -f "$TEMP_DIR/$listname" 33 | echo -e "[${listtype^^}] Update failed." 34 | return 1 35 | fi 36 | 37 | mv -f "$TEMP_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}" 38 | echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver" 39 | echo -e "[${listtype^^}] Successfully updated." 40 | 41 | return 0 42 | } 43 | 44 | check_list_update "china_ip4" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt" 45 | check_list_update "china_ip6" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt" 46 | check_list_update "gfw_list" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt" 47 | check_list_update "china_list" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \ 48 | sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt" 49 | 50 | rm -rf "$TEMP_DIR" 51 | -------------------------------------------------------------------------------- /root/etc/config/homeproxy: -------------------------------------------------------------------------------- 1 | 2 | config homeproxy 'infra' 3 | option __warning 'DO NOT EDIT THIS SECTION, OR YOU ARE ON YOUR OWN!' 4 | option common_port '22,53,80,143,443,465,587,853,873,993,995,5222,8080,8443,9418' 5 | option mixed_port '5330' 6 | option redirect_port '5331' 7 | option tproxy_port '5332' 8 | option dns_port '5333' 9 | option ntp_server 'nil' 10 | option sniff_override '1' 11 | option udp_timeout '' 12 | option tun_name 'singtun0' 13 | option tun_addr4 '172.19.0.1/30' 14 | option tun_addr6 'fdfe:dcba:9876::1/126' 15 | option tun_mtu '9000' 16 | option table_mark '100' 17 | option self_mark '100' 18 | option tproxy_mark '101' 19 | option tun_mark '102' 20 | 21 | config homeproxy 'migration' 22 | option crontab '1' 23 | 24 | config homeproxy 'config' 25 | option main_node 'nil' 26 | option main_udp_node 'same' 27 | option dns_server '8.8.8.8' 28 | option china_dns_server '223.5.5.5' 29 | option routing_mode 'bypass_mainland_china' 30 | option routing_port 'common' 31 | option proxy_mode 'redirect_tproxy' 32 | option ipv6_support '1' 33 | option github_token '' 34 | option log_level 'warn' 35 | 36 | config homeproxy 'control' 37 | option lan_proxy_mode 'disabled' 38 | list wan_proxy_ipv4_ips '91.105.192.0/23' 39 | list wan_proxy_ipv4_ips '91.108.4.0/22' 40 | list wan_proxy_ipv4_ips '91.108.8.0/22' 41 | list wan_proxy_ipv4_ips '91.108.16.0/22' 42 | list wan_proxy_ipv4_ips '91.108.12.0/22' 43 | list wan_proxy_ipv4_ips '91.108.20.0/22' 44 | list wan_proxy_ipv4_ips '91.108.56.0/22' 45 | list wan_proxy_ipv4_ips '149.154.160.0/20' 46 | list wan_proxy_ipv4_ips '185.76.151.0/24' 47 | list wan_proxy_ipv4_ips '203.208.50.66/32' 48 | list wan_proxy_ipv6_ips '2001:67c:4e8::/48' 49 | list wan_proxy_ipv6_ips '2001:b28:f23c::/48' 50 | list wan_proxy_ipv6_ips '2001:b28:f23d::/48' 51 | list wan_proxy_ipv6_ips '2001:b28:f23f::/48' 52 | list wan_proxy_ipv6_ips '2a0a:f280::/32' 53 | 54 | config homeproxy 'routing' 55 | option sniff_override '1' 56 | option default_outbound 'direct-out' 57 | option default_outbound_dns 'default-dns' 58 | 59 | config homeproxy 'dns' 60 | option dns_strategy 'prefer_ipv4' 61 | option default_server 'local-dns' 62 | option disable_cache '0' 63 | option disable_cache_expire '0' 64 | 65 | config homeproxy 'subscription' 66 | option auto_update '0' 67 | option allow_insecure '0' 68 | option packet_encoding 'xudp' 69 | option update_via_proxy '0' 70 | option filter_nodes 'blacklist' 71 | list filter_keywords '重置|到期|过期|剩余|套餐' 72 | list filter_keywords 'Expiration|Remaining' 73 | 74 | config homeproxy 'server' 75 | option enabled '0' 76 | option log_level 'warn' 77 | 78 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/update_resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2022-2025 ImmortalWrt.org 5 | 6 | NAME="homeproxy" 7 | 8 | RESOURCES_DIR="/etc/$NAME/resources" 9 | mkdir -p "$RESOURCES_DIR" 10 | 11 | RUN_DIR="/var/run/$NAME" 12 | LOG_PATH="$RUN_DIR/$NAME.log" 13 | mkdir -p "$RUN_DIR" 14 | 15 | log() { 16 | echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH" 17 | } 18 | 19 | to_upper() { 20 | echo -e "$1" | tr "[a-z]" "[A-Z]" 21 | } 22 | 23 | check_list_update() { 24 | local listtype="$1" 25 | local listrepo="$2" 26 | local listref="$3" 27 | local listname="$4" 28 | local lock="$RUN_DIR/update_resources-$listtype.lock" 29 | local github_token="$(uci -q get homeproxy.config.github_token)" 30 | local wget="wget --timeout=10 -q" 31 | 32 | exec 200>"$lock" 33 | if ! flock -n 200 &> "/dev/null"; then 34 | log "[$(to_upper "$listtype")] A task is already running." 35 | return 2 36 | fi 37 | 38 | [ -z "$github_token" ] || github_token="--header=Authorization: Bearer $github_token" 39 | local list_info="$($wget "${github_token:--q}" -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")" 40 | local list_sha="$(echo -e "$list_info" | jsonfilter -qe "@[0].sha")" 41 | local list_ver="$(echo -e "$list_info" | jsonfilter -qe "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')" 42 | if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then 43 | log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later." 44 | return 1 45 | fi 46 | 47 | local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")" 48 | if [ "$local_list_ver" = "$list_ver" ]; then 49 | log "[$(to_upper "$listtype")] Current version: $list_ver." 50 | log "[$(to_upper "$listtype")] You're already at the latest version." 51 | return 3 52 | else 53 | log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver." 54 | fi 55 | 56 | if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then 57 | rm -f "$RUN_DIR/$listname" 58 | log "[$(to_upper "$listtype")] Update failed." 59 | return 1 60 | fi 61 | 62 | mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}" 63 | echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver" 64 | log "[$(to_upper "$listtype")] Successfully updated." 65 | 66 | return 0 67 | } 68 | 69 | case "$1" in 70 | "china_ip4") 71 | check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt" 72 | ;; 73 | "china_ip6") 74 | check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt" 75 | ;; 76 | "gfw_list") 77 | check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt" 78 | ;; 79 | "china_list") 80 | check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \ 81 | sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt" 82 | ;; 83 | *) 84 | echo -e "Usage: $0 " 85 | exit 1 86 | ;; 87 | esac 88 | -------------------------------------------------------------------------------- /.github/workflows/build-ipk.yml: -------------------------------------------------------------------------------- 1 | name: Build ipk for HomeProxy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'dev' 8 | paths: 9 | - 'htdocs/**' 10 | - 'po/**' 11 | - 'root/**' 12 | - 'Makefile' 13 | - '.github/**' 14 | - '!.github/ISSUE_TEMPLATE/**' 15 | 16 | pull_request: 17 | branches: 18 | - 'master' 19 | - 'dev' 20 | types: 21 | - opened 22 | - synchronize 23 | - reopened 24 | paths: 25 | - 'htdocs/**' 26 | - 'root/**' 27 | - 'Makefile' 28 | - '.github/**' 29 | - '!.github/ISSUE_TEMPLATE/**' 30 | 31 | release: 32 | types: 33 | - published 34 | 35 | jobs: 36 | build: 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - name: Checkout source tree 41 | uses: actions/checkout@v5 42 | 43 | - name: Checkout apk 44 | uses: actions/checkout@v5 45 | with: 46 | repository: 'alpinelinux/apk-tools' 47 | path: 'apk-tools' 48 | fetch-depth: 0 49 | ref: 'd093f7c198a64bff0cd58afeaf638909fda24ca8' 50 | 51 | - name: Checkout po2lmo 52 | uses: actions/checkout@v5 53 | with: 54 | repository: 'openwrt/luci' 55 | path: 'po2lmo' 56 | sparse-checkout: 'modules/luci-base/src' 57 | 58 | - name: Setup dependencies 59 | env: 60 | DEBIAN_FRONTEND: noninteractive 61 | run: | 62 | sudo -E rm -rf /var/lib/man-db/auto-update 63 | sudo -E apt-get update -qq 64 | sudo -E apt-get install -y build-essential curl fakeroot libssl-dev lua5.1 meson ninja-build 65 | 66 | pushd apk-tools 67 | meson setup build \ 68 | -Db_lto=true \ 69 | -Dcompressed-help=false \ 70 | -Ddocs=disabled \ 71 | -Dhelp=enabled \ 72 | -Dlua_version=5.1 \ 73 | -Ddefault_library=static \ 74 | -Durl_backend=wget \ 75 | -Dzstd=false \ 76 | -Dpython=disabled \ 77 | -Dtests=disabled \ 78 | -Dcrypto_backend=openssl 79 | ninja -C build 80 | sudo -E meson install -C build --strip 81 | popd 82 | 83 | pushd po2lmo/modules/luci-base/src 84 | make po2lmo 85 | sudo -E install -m0755 -s po2lmo /usr/local/bin/po2lmo 86 | popd 87 | 88 | curl -fLO "https://raw.githubusercontent.com/openwrt/openwrt/master/scripts/ipkg-build" 89 | sudo install -m0755 ipkg-build /usr/local/bin/ipkg-build 90 | 91 | - name: Build ipk file 92 | run: | 93 | pushd .github 94 | fakeroot bash build-ipk.sh apk "${{ github.event_name }}" 95 | fakeroot bash build-ipk.sh ipk "${{ github.event_name }}" 96 | echo "APK_ASSET_NAME=$(ls *.apk)" >> $GITHUB_ENV 97 | echo "IPK_ASSET_NAME=$(ls *.ipk)" >> $GITHUB_ENV 98 | popd 99 | 100 | - name: Publishing APK files to GitHub Artifacts 101 | uses: actions/upload-artifact@v4 102 | if: github.event_name != 'release' 103 | with: 104 | name: ${{ env.APK_ASSET_NAME }} 105 | path: .github/${{ env.APK_ASSET_NAME }} 106 | 107 | - name: Publishing IPK files to GitHub Artifacts 108 | uses: actions/upload-artifact@v4 109 | if: github.event_name != 'release' 110 | with: 111 | name: ${{ env.IPK_ASSET_NAME }} 112 | path: .github/${{ env.IPK_ASSET_NAME }} 113 | 114 | - name: Publishing to GitHub Releases 115 | uses: svenstaro/upload-release-action@v2 116 | if: github.event_name == 'release' 117 | with: 118 | repo_token: ${{ github.token }} 119 | file: .github/luci-app-homeproxy_*.* 120 | file_glob: true 121 | tag: ${{ github.ref }} 122 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report to help us improve. 3 | labels: 4 | - bug 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Bug description 10 | description: A clear and concise description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: behaviour 15 | attributes: 16 | label: Actual behavior 17 | description: A clear and concise description of what actually happens. 18 | - type: textarea 19 | id: expected 20 | attributes: 21 | label: Expected behavior 22 | description: A clear and concise description of what you expected to happen. 23 | - type: textarea 24 | id: reproduce 25 | attributes: 26 | label: Steps to reproduce 27 | description: Steps to reproduce the reported behaviour. 28 | - type: textarea 29 | id: luciconfig 30 | attributes: 31 | label: HomeProxy configuration 32 | description: | 33 | The HomeProxy configuration file where the error occurred (use command below). 34 | ```cat /etc/config/homeproxy``` 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: coreconfig 39 | attributes: 40 | label: sing-box configuration 41 | description: | 42 | The sing-box configuration file where the error occurred (use command below). 43 | For client: ```cat /var/run/homeproxy/sing-box-c.json``` 44 | For server: ```cat /var/run/homeproxy/sing-box-s.json``` 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: lucilog 49 | attributes: 50 | label: HomeProxy log 51 | description: | 52 | The HomeProxy log file where the error occurred (use command below). 53 | ```cat /var/run/homeproxy/homeproxy.log``` 54 | validations: 55 | required: false 56 | - type: textarea 57 | id: corelog 58 | attributes: 59 | label: sing-box log 60 | description: | 61 | The sing-box log file where the error occurred (use command below). 62 | For client: ```cat /var/run/homeproxy/sing-box-c.log``` 63 | For server: ```cat /var/run/homeproxy/sing-box-s.log``` 64 | validations: 65 | required: true 66 | 67 | - type: textarea 68 | id: syslog 69 | attributes: 70 | label: System log 71 | description: | 72 | Paste system log if the daemon cannot start (use command below). 73 | ```logread -e sing-box``` 74 | 75 | - type: input 76 | id: osrelease 77 | attributes: 78 | label: OpenWrt/ImmortalWrt release 79 | description: | 80 | The OpenWrt/ImmortalWrt release or commit hash where this bug occurs (use command below). 81 | ```. /etc/openwrt_release && echo $DISTRIB_RELEASE $DISTRIB_REVISION``` 82 | validations: 83 | required: true 84 | - type: input 85 | id: device 86 | attributes: 87 | label: Device 88 | description: | 89 | The device exhibiting this bug (if unsure, use command below). 90 | ```cat /tmp/sysinfo/model``` 91 | validations: 92 | required: true 93 | - type: textarea 94 | id: additional 95 | attributes: 96 | label: Additional information 97 | description: Add any additional info you think might be helfpul. 98 | - type: checkboxes 99 | id: terms 100 | attributes: 101 | label: Terms 102 | description: Please check the following before submitting the issue. 103 | options: 104 | - label: I confirm that the HomeProxy I installed is from official source, like GitHub artifacts or official ImmortalWrt opkg feeds. 105 | required: true 106 | - label: I confirm that the Homeproxy I installed does not contain Clash mode support. 107 | required: true 108 | - label: I confirm that the version of OpenWrt/ImmortalWrt I installed is >= 24.10. 109 | required: true 110 | - label: I confirm that I have installed the latest version of HomeProxy and sing-box. 111 | required: true 112 | - label: I confirm that I have read the sing-box documentation, understand the meaning of all the configuration items I added. 113 | required: true 114 | - label: I confirm that I have not mixed iptables and nftables rules. 115 | required: true 116 | - label: I confirm that I have not modified system DNS settings, or enabled any other DNS servers like MosDNS and SmartDNS. 117 | required: true 118 | - label: I confirm that I have cleared the browser cache. 119 | required: true 120 | - label: I confirm that I have selected all terms **blindly**. 121 | required: false 122 | -------------------------------------------------------------------------------- /.github/build-ipk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2023 Tianling Shen 5 | 6 | set -o errexit 7 | set -o pipefail 8 | 9 | PKG_MGR="${1:-apk}" 10 | RELEASE_TYPE="${2:-snapshot}" 11 | 12 | export PKG_SOURCE_DATE_EPOCH="$(date "+%s")" 13 | export SOURCE_DATE_EPOCH="$PKG_SOURCE_DATE_EPOCH" 14 | 15 | BASE_DIR="$(cd "$(dirname $0)"; pwd)" 16 | PKG_DIR="$BASE_DIR/.." 17 | 18 | function get_mk_value() { 19 | awk -F "$1:=" '{print $2}' "$PKG_DIR/Makefile" | xargs 20 | } 21 | 22 | PKG_NAME="$(get_mk_value "PKG_NAME")" 23 | if [ "$RELEASE_TYPE" == "release" ]; then 24 | PKG_VERSION="$(get_mk_value "PKG_VERSION")" 25 | else 26 | PKG_VERSION="$PKG_SOURCE_DATE_EPOCH~$(git rev-parse --short HEAD)" 27 | fi 28 | 29 | TEMP_DIR="$(mktemp -d -p $BASE_DIR)" 30 | TEMP_PKG_DIR="$TEMP_DIR/$PKG_NAME" 31 | mkdir -p "$TEMP_PKG_DIR/lib/upgrade/keep.d/" 32 | mkdir -p "$TEMP_PKG_DIR/usr/lib/lua/luci/i18n/" 33 | mkdir -p "$TEMP_PKG_DIR/www/" 34 | if [ "$PKG_MGR" == "apk" ]; then 35 | mkdir -p "$TEMP_PKG_DIR/lib/apk/packages/" 36 | else 37 | mkdir -p "$TEMP_PKG_DIR/CONTROL/" 38 | fi 39 | 40 | cp -fpR "$PKG_DIR/htdocs"/* "$TEMP_PKG_DIR/www/" 41 | cp -fpR "$PKG_DIR/root"/* "$TEMP_PKG_DIR/" 42 | 43 | cat > "$TEMP_PKG_DIR/lib/upgrade/keep.d/$PKG_NAME" <<-EOF 44 | /etc/homeproxy/certs/ 45 | /etc/homeproxy/ruleset/ 46 | /etc/homeproxy/resources/direct_list.txt 47 | /etc/homeproxy/resources/proxy_list.txt 48 | EOF 49 | 50 | po2lmo "$PKG_DIR/po/zh_Hans/homeproxy.po" "$TEMP_PKG_DIR/usr/lib/lua/luci/i18n/homeproxy.zh-cn.lmo" 51 | 52 | if [ "$PKG_MGR" == "apk" ]; then 53 | find "$TEMP_PKG_DIR" -type f,l -printf '/%P\n' | sort > "$TEMP_PKG_DIR/lib/apk/packages/$PKG_NAME.list" 54 | echo "/etc/config/homeproxy" >> "$TEMP_PKG_DIR/lib/apk/packages/$PKG_NAME.conffiles" 55 | cat "$TEMP_PKG_DIR/lib/apk/packages/$PKG_NAME.conffiles" | while IFS= read -r file; do 56 | [ -f "$TEMP_PKG_DIR/$file" ] || continue 57 | sha256sum "$TEMP_PKG_DIR/$file" | sed "s,$TEMP_PKG_DIR/,," >> "$TEMP_PKG_DIR/lib/apk/packages/$PKG_NAME.conffiles_static" 58 | done 59 | 60 | echo -e '#!/bin/sh 61 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 62 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 63 | . ${IPKG_INSTROOT}/lib/functions.sh 64 | export root="${IPKG_INSTROOT}" 65 | export pkgname="'"$PKG_NAME"'" 66 | add_group_and_user 67 | default_postinst 68 | [ -n "${IPKG_INSTROOT}" ] || { rm -f /tmp/luci-indexcache.* 69 | rm -rf /tmp/luci-modulecache/ 70 | killall -HUP rpcd 2>/dev/null 71 | exit 0 72 | }' > "$TEMP_DIR/post-install" 73 | 74 | echo -e '#!/bin/sh 75 | export PKG_UPGRADE=1 76 | #!/bin/sh 77 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 78 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 79 | . ${IPKG_INSTROOT}/lib/functions.sh 80 | export root="${IPKG_INSTROOT}" 81 | export pkgname="'"$PKG_NAME"'" 82 | add_group_and_user 83 | default_postinst 84 | [ -n "${IPKG_INSTROOT}" ] || { rm -f /tmp/luci-indexcache.* 85 | rm -rf /tmp/luci-modulecache/ 86 | killall -HUP rpcd 2>/dev/null 87 | exit 0 88 | }' > "$TEMP_DIR/post-upgrade" 89 | 90 | echo -e '#!/bin/sh 91 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 92 | . ${IPKG_INSTROOT}/lib/functions.sh 93 | export root="${IPKG_INSTROOT}" 94 | export pkgname="'"$PKG_NAME"'" 95 | default_prerm' > "$TEMP_DIR/pre-deinstall" 96 | 97 | apk mkpkg \ 98 | --info "name:$PKG_NAME" \ 99 | --info "version:$PKG_VERSION" \ 100 | --info "description:The modern ImmortalWrt proxy platform for ARM64/AMD64" \ 101 | --info "arch:all" \ 102 | --info "origin:https://github.com/immortalwrt/homeproxy" \ 103 | --info "url:" \ 104 | --info "maintainer:Tianling Shen " \ 105 | --info "provides:" \ 106 | --script "post-install:$TEMP_DIR/post-install" \ 107 | --script "post-upgrade:$TEMP_DIR/post-upgrade" \ 108 | --script "pre-deinstall:$TEMP_DIR/pre-deinstall" \ 109 | --info "depends:libc sing-box firewall4 kmod-nft-tproxy ucode-mod-digest" \ 110 | --files "$TEMP_PKG_DIR" \ 111 | --output "$TEMP_DIR/${PKG_NAME}_${PKG_VERSION}.apk" 112 | 113 | mv "$TEMP_DIR/${PKG_NAME}_${PKG_VERSION}.apk" "$BASE_DIR/${PKG_NAME}_${PKG_VERSION}_all.apk" 114 | else 115 | mkdir -p "$TEMP_PKG_DIR/CONTROL/" 116 | 117 | cat > "$TEMP_PKG_DIR/CONTROL/control" <<-EOF 118 | Package: $PKG_NAME 119 | Version: $PKG_VERSION 120 | Depends: libc, sing-box, firewall4, kmod-nft-tproxy, ucode-mod-digest 121 | Source: https://github.com/immortalwrt/homeproxy 122 | SourceName: $PKG_NAME 123 | Section: luci 124 | SourceDateEpoch: $PKG_SOURCE_DATE_EPOCH 125 | Maintainer: Tianling Shen 126 | Architecture: all 127 | Installed-Size: TO-BE-FILLED-BY-IPKG-BUILD 128 | Description: The modern ImmortalWrt proxy platform for ARM64/AMD64 129 | EOF 130 | chmod 0644 "$TEMP_PKG_DIR/CONTROL/control" 131 | 132 | echo -e "/etc/config/homeproxy" > "$TEMP_PKG_DIR/CONTROL/conffiles" 133 | 134 | echo -e '#!/bin/sh 135 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 136 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 137 | . ${IPKG_INSTROOT}/lib/functions.sh 138 | default_postinst $0 $@' > "$TEMP_PKG_DIR/CONTROL/postinst" 139 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/postinst" 140 | 141 | echo -e "[ -n "\${IPKG_INSTROOT}" ] || { 142 | (. /etc/uci-defaults/$PKG_NAME) && rm -f /etc/uci-defaults/$PKG_NAME 143 | rm -f /tmp/luci-indexcache 144 | rm -rf /tmp/luci-modulecache/ 145 | exit 0 146 | }" > "$TEMP_PKG_DIR/CONTROL/postinst-pkg" 147 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/postinst-pkg" 148 | 149 | echo -e '#!/bin/sh 150 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 151 | . ${IPKG_INSTROOT}/lib/functions.sh 152 | default_prerm $0 $@' > "$TEMP_PKG_DIR/CONTROL/prerm" 153 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/prerm" 154 | 155 | ipkg-build -m "" "$TEMP_PKG_DIR" "$TEMP_DIR" 156 | 157 | mv "$TEMP_DIR/${PKG_NAME}_${PKG_VERSION}_all.ipk" "$BASE_DIR/${PKG_NAME}_${PKG_VERSION}_all.ipk" 158 | fi 159 | 160 | rm -rf "$TEMP_DIR" 161 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/homeproxy.uc: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-2.0-only 3 | * 4 | * Copyright (C) 2023 ImmortalWrt.org 5 | */ 6 | 7 | import { mkstemp } from 'fs'; 8 | import { urldecode_params } from 'luci.http'; 9 | 10 | /* Global variables start */ 11 | export const HP_DIR = '/etc/homeproxy'; 12 | export const RUN_DIR = '/var/run/homeproxy'; 13 | /* Global variables end */ 14 | 15 | /* Utilities start */ 16 | /* Kanged from luci-app-commands */ 17 | export function shellQuote(s) { 18 | return `'${replace(s, "'", "'\\''")}'`; 19 | }; 20 | 21 | export function isBinary(str) { 22 | for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off)) 23 | if (byte <= 8 || (byte >= 14 && byte <= 31)) 24 | return true; 25 | 26 | return false; 27 | }; 28 | 29 | export function executeCommand(...args) { 30 | let outfd = mkstemp(); 31 | let errfd = mkstemp(); 32 | 33 | const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`); 34 | 35 | outfd.seek(0); 36 | errfd.seek(0); 37 | 38 | const stdout = outfd.read(1024 * 512) ?? ''; 39 | const stderr = errfd.read(1024 * 512) ?? ''; 40 | 41 | outfd.close(); 42 | errfd.close(); 43 | 44 | const binary = isBinary(stdout); 45 | 46 | return { 47 | command: join(' ', args), 48 | stdout: binary ? null : stdout, 49 | stderr, 50 | exitcode, 51 | binary 52 | }; 53 | }; 54 | 55 | export function getTime(epoch) { 56 | const local_time = localtime(epoch); 57 | return replace(replace(sprintf( 58 | '%d-%2d-%2d@%2d:%2d:%2d', 59 | local_time.year, 60 | local_time.mon, 61 | local_time.mday, 62 | local_time.hour, 63 | local_time.min, 64 | local_time.sec 65 | ), ' ', '0'), '@', ' '); 66 | 67 | }; 68 | 69 | export function wGET(url, ua) { 70 | if (!url || type(url) !== 'string') 71 | return null; 72 | 73 | if (!ua) 74 | ua = 'Wget/1.21 (HomeProxy, like v2rayN)'; 75 | 76 | const output = executeCommand(`/usr/bin/wget -qO- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {}; 77 | return trim(output.stdout); 78 | }; 79 | /* Utilities end */ 80 | 81 | /* String helper start */ 82 | export function isEmpty(res) { 83 | return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0); 84 | }; 85 | 86 | export function strToBool(str) { 87 | return (str === '1') || null; 88 | }; 89 | 90 | export function strToInt(str) { 91 | return !isEmpty(str) ? (int(str) || null) : null; 92 | }; 93 | 94 | export function strToTime(str) { 95 | return !isEmpty(str) ? (str + 's') : null; 96 | }; 97 | 98 | export function removeBlankAttrs(res) { 99 | let content; 100 | 101 | if (type(res) === 'object') { 102 | content = {}; 103 | map(keys(res), (k) => { 104 | if (type(res[k]) in ['array', 'object']) 105 | content[k] = removeBlankAttrs(res[k]); 106 | else if (res[k] !== null && res[k] !== '') 107 | content[k] = res[k]; 108 | }); 109 | } else if (type(res) === 'array') { 110 | content = []; 111 | map(res, (k, i) => { 112 | if (type(k) in ['array', 'object']) 113 | push(content, removeBlankAttrs(k)); 114 | else if (k !== null && k !== '') 115 | push(content, k); 116 | }); 117 | } else 118 | return res; 119 | 120 | return content; 121 | }; 122 | 123 | export function validateHostname(hostname) { 124 | return (match(hostname, /^[a-zA-Z0-9_]+$/) != null || 125 | (match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) && 126 | match(hostname, /[^0-9.]/))); 127 | }; 128 | 129 | export function validation(datatype, data) { 130 | if (!datatype || !data) 131 | return null; 132 | 133 | const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`); 134 | return (ret === 0); 135 | }; 136 | /* String helper end */ 137 | 138 | /* String parser start */ 139 | export function decodeBase64Str(str) { 140 | if (isEmpty(str)) 141 | return null; 142 | 143 | str = trim(str); 144 | str = replace(str, '_', '/'); 145 | str = replace(str, '-', '+'); 146 | 147 | const padding = length(str) % 4; 148 | if (padding) 149 | str = str + substr('====', padding); 150 | 151 | return b64dec(str); 152 | }; 153 | 154 | export function parseURL(url) { 155 | if (type(url) !== 'string') 156 | return null; 157 | 158 | const services = { 159 | http: '80', 160 | https: '443' 161 | }; 162 | 163 | const objurl = {}; 164 | 165 | objurl.href = url; 166 | 167 | url = replace(url, /#(.+)$/, (_, val) => { 168 | objurl.hash = val; 169 | return ''; 170 | }); 171 | 172 | url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => { 173 | objurl.protocol = val; 174 | return ''; 175 | }); 176 | 177 | url = replace(url, /\?(.+)/, (_, val) => { 178 | objurl.search = val; 179 | objurl.searchParams = urldecode_params(val); 180 | return ''; 181 | }); 182 | 183 | url = replace(url, /^\/\/([^\/]+)/, (_, val) => { 184 | val = replace(val, /^([^@]+)@/, (_, val) => { 185 | objurl.userinfo = val; 186 | return ''; 187 | }); 188 | 189 | val = replace(val, /:(\d+)$/, (_, val) => { 190 | objurl.port = val; 191 | return ''; 192 | }); 193 | 194 | if (validation('ip4addr', val) || 195 | validation('ip6addr', replace(val, /\[|\]/g, '')) || 196 | validation('hostname', val)) 197 | objurl.hostname = val; 198 | 199 | return ''; 200 | }); 201 | 202 | objurl.pathname = url || '/'; 203 | 204 | if (!objurl.protocol || !objurl.hostname) 205 | return null; 206 | 207 | if (objurl.userinfo) { 208 | objurl.userinfo = replace(objurl.userinfo, /:(.+)$/, (_, val) => { 209 | objurl.password = val; 210 | return ''; 211 | }); 212 | 213 | if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) { 214 | objurl.username = objurl.userinfo; 215 | delete objurl.userinfo; 216 | } else { 217 | delete objurl.userinfo; 218 | delete objurl.password; 219 | } 220 | }; 221 | 222 | if (!objurl.port) 223 | objurl.port = services[objurl.protocol]; 224 | 225 | objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : ''); 226 | objurl.origin = `${objurl.protocol}://${objurl.host}`; 227 | 228 | return objurl; 229 | }; 230 | /* String parser end */ 231 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/generate_server.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | /* 3 | * SPDX-License-Identifier: GPL-2.0-only 4 | * 5 | * Copyright (C) 2023 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { writefile } from 'fs'; 11 | import { cursor } from 'uci'; 12 | 13 | import { 14 | isEmpty, strToBool, strToInt, strToTime, 15 | removeBlankAttrs, HP_DIR, RUN_DIR 16 | } from 'homeproxy'; 17 | 18 | /* UCI config start */ 19 | const uci = cursor(); 20 | 21 | const uciconfig = 'homeproxy'; 22 | uci.load(uciconfig); 23 | 24 | const uciserver = 'server'; 25 | 26 | const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn'; 27 | /* UCI config end */ 28 | 29 | const config = {}; 30 | 31 | /* Log */ 32 | config.log = { 33 | disabled: false, 34 | level: log_level, 35 | output: RUN_DIR + '/sing-box-s.log', 36 | timestamp: true 37 | }; 38 | 39 | config.inbounds = []; 40 | 41 | uci.foreach(uciconfig, uciserver, (cfg) => { 42 | if (cfg.enabled !== '1') 43 | return; 44 | 45 | push(config.inbounds, { 46 | type: cfg.type, 47 | tag: 'cfg-' + cfg['.name'] + '-in', 48 | 49 | listen: cfg.address || '::', 50 | listen_port: strToInt(cfg.port), 51 | bind_interface: cfg.bind_interface, 52 | reuse_addr: strToBool(cfg.reuse_addr), 53 | tcp_fast_open: strToBool(cfg.tcp_fast_open), 54 | tcp_multi_path: strToBool(cfg.tcp_multi_path), 55 | udp_fragment: strToBool(cfg.udp_fragment), 56 | udp_timeout: strToTime(cfg.udp_timeout), 57 | network: cfg.network, 58 | 59 | /* AnyTLS */ 60 | padding_scheme: cfg.anytls_padding_scheme, 61 | 62 | /* Hysteria */ 63 | up_mbps: strToInt(cfg.hysteria_up_mbps), 64 | down_mbps: strToInt(cfg.hysteria_down_mbps), 65 | obfs: cfg.hysteria_obfs_type ? { 66 | type: cfg.hysteria_obfs_type, 67 | password: cfg.hysteria_obfs_password 68 | } : cfg.hysteria_obfs_password, 69 | recv_window_conn: strToInt(cfg.hysteria_recv_window_conn), 70 | recv_window_client: strToInt(cfg.hysteria_revc_window_client), 71 | max_conn_client: strToInt(cfg.hysteria_max_conn_client), 72 | disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery), 73 | ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth), 74 | masquerade: cfg.hysteria_masquerade, 75 | 76 | /* Shadowsocks */ 77 | method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null, 78 | password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null, 79 | 80 | /* Tuic */ 81 | congestion_control: cfg.tuic_congestion_control, 82 | auth_timeout: strToTime(cfg.tuic_auth_timeout), 83 | zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt), 84 | heartbeat: strToTime(cfg.tuic_heartbeat), 85 | 86 | /* AnyTLS / HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */ 87 | users: (cfg.type !== 'shadowsocks') ? [ 88 | { 89 | name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null, 90 | username: cfg.username, 91 | password: cfg.password, 92 | 93 | /* Hysteria */ 94 | auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null, 95 | auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null, 96 | 97 | /* Tuic */ 98 | uuid: cfg.uuid, 99 | 100 | /* VLESS / VMess */ 101 | flow: cfg.vless_flow, 102 | alterId: strToInt(cfg.vmess_alterid) 103 | } 104 | ] : null, 105 | 106 | multiplex: (cfg.multiplex === '1') ? { 107 | enabled: true, 108 | padding: strToBool(cfg.multiplex_padding), 109 | brutal: (cfg.multiplex_brutal === '1') ? { 110 | enabled: true, 111 | up_mbps: strToInt(cfg.multiplex_brutal_up), 112 | down_mbps: strToInt(cfg.multiplex_brutal_down) 113 | } : null 114 | } : null, 115 | 116 | tls: (cfg.tls === '1') ? { 117 | enabled: true, 118 | server_name: cfg.tls_sni, 119 | alpn: cfg.tls_alpn, 120 | min_version: cfg.tls_min_version, 121 | max_version: cfg.tls_max_version, 122 | cipher_suites: cfg.tls_cipher_suites, 123 | certificate_path: cfg.tls_cert_path, 124 | key_path: cfg.tls_key_path, 125 | acme: (cfg.tls_acme === '1') ? { 126 | domain: cfg.tls_acme_domain, 127 | data_directory: HP_DIR + '/certs', 128 | default_server_name: cfg.tls_acme_dsn, 129 | email: cfg.tls_acme_email, 130 | provider: cfg.tls_acme_provider, 131 | disable_http_challenge: strToBool(cfg.tls_acme_dhc), 132 | disable_tls_alpn_challenge: (cfg.tls_acme_dtac), 133 | alternative_http_port: strToInt(cfg.tls_acme_ahp), 134 | alternative_tls_port: strToInt(cfg.tls_acme_atp), 135 | external_account: (cfg.tls_acme_external_account === '1') ? { 136 | key_id: cfg.tls_acme_ea_keyid, 137 | mac_key: cfg.tls_acme_ea_mackey 138 | } : null, 139 | dns01_challenge: (cfg.tls_dns01_challenge === '1') ? { 140 | provider: cfg.tls_dns01_provider, 141 | access_key_id: cfg.tls_dns01_ali_akid, 142 | access_key_secret: cfg.tls_dns01_ali_aksec, 143 | region_id: cfg.tls_dns01_ali_rid, 144 | api_token: cfg.tls_dns01_cf_api_token 145 | } : null 146 | } : null, 147 | ech: (cfg.tls_ech_key) ? { 148 | enabled: true, 149 | key: split(cfg.tls_ech_key, '\n'), 150 | // config: split(cfg.tls_ech_config, '\n') 151 | } : null, 152 | reality: (cfg.tls_reality === '1') ? { 153 | enabled: true, 154 | private_key: cfg.tls_reality_private_key, 155 | short_id: cfg.tls_reality_short_id, 156 | max_time_difference: strToTime(cfg.tls_reality_max_time_difference), 157 | handshake: { 158 | server: cfg.tls_reality_server_addr, 159 | server_port: strToInt(cfg.tls_reality_server_port) 160 | } 161 | } : null 162 | } : null, 163 | 164 | transport: !isEmpty(cfg.transport) ? { 165 | type: cfg.transport, 166 | host: cfg.http_host || cfg.httpupgrade_host, 167 | path: cfg.http_path || cfg.ws_path, 168 | headers: cfg.ws_host ? { 169 | Host: cfg.ws_host 170 | } : null, 171 | method: cfg.http_method, 172 | max_early_data: strToInt(cfg.websocket_early_data), 173 | early_data_header_name: cfg.websocket_early_data_header, 174 | service_name: cfg.grpc_servicename, 175 | idle_timeout: strToTime(cfg.http_idle_timeout), 176 | ping_timeout: strToTime(cfg.http_ping_timeout) 177 | } : null 178 | }); 179 | }); 180 | 181 | if (length(config.inbounds) === 0) 182 | exit(1); 183 | 184 | system('mkdir -p ' + RUN_DIR); 185 | writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config))); 186 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/ucode/luci.homeproxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | /* 3 | * SPDX-License-Identifier: GPL-2.0-only 4 | * 5 | * Copyright (C) 2023-2024 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { access, error, lstat, popen, readfile, writefile } from 'fs'; 11 | 12 | /* Kanged from ucode/luci */ 13 | function shellquote(s) { 14 | return `'${replace(s, "'", "'\\''")}'`; 15 | } 16 | 17 | function hasKernelModule(kmod) { 18 | return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0); 19 | } 20 | 21 | const HP_DIR = '/etc/homeproxy'; 22 | const RUN_DIR = '/var/run/homeproxy'; 23 | 24 | const methods = { 25 | acllist_read: { 26 | args: { type: 'type' }, 27 | call: function(req) { 28 | if (index(['direct_list', 'proxy_list'], req.args?.type) === -1) 29 | return { content: null, error: 'illegal type' }; 30 | 31 | const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`); 32 | return { content: filecontent }; 33 | } 34 | }, 35 | acllist_write: { 36 | args: { type: 'type', content: 'content' }, 37 | call: function(req) { 38 | if (index(['direct_list', 'proxy_list'], req.args?.type) === -1) 39 | return { result: false, error: 'illegal type' }; 40 | 41 | const file = `${HP_DIR}/resources/${req.args?.type}.txt`; 42 | let content = req.args?.content; 43 | 44 | /* Sanitize content */ 45 | if (content) { 46 | content = trim(content); 47 | content = replace(content, /\r\n?/g, '\n'); 48 | if (!match(content, /\n$/)) 49 | content += '\n'; 50 | } 51 | 52 | system(`mkdir -p ${HP_DIR}/resources`); 53 | writefile(file, content); 54 | 55 | return { result: true }; 56 | } 57 | }, 58 | 59 | certificate_write: { 60 | args: { filename: 'filename' }, 61 | call: function(req) { 62 | const writeCertificate = (filename, priv) => { 63 | const tmpcert = '/tmp/homeproxy_certificate.tmp'; 64 | const filestat = lstat(tmpcert); 65 | 66 | if (!filestat || filestat.type !== 'file' || filestat.size <= 0) { 67 | system(`rm -f ${tmpcert}`); 68 | return { result: false, error: 'empty certificate file' }; 69 | } 70 | 71 | let filecontent = readfile(tmpcert); 72 | if (is_binary(filecontent)) { 73 | system(`rm -f ${tmpcert}`); 74 | return { result: false, error: 'illegal file type: binary' }; 75 | } 76 | 77 | /* Kanged from luci-proto-openconnect */ 78 | const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/, 79 | end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/, 80 | lines = split(trim(filecontent), /[\r\n]/); 81 | let start = false, i; 82 | 83 | for (i = 0; i < length(lines); i++) { 84 | if (match(lines[i], beg)) 85 | start = true; 86 | else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64) 87 | break; 88 | } 89 | 90 | if (!start || i < length(lines) - 1 || !match(lines[i], end)) { 91 | system(`rm -f ${tmpcert}`); 92 | return { result: false, error: 'this does not look like a correct PEM file' }; 93 | } 94 | 95 | /* Sanitize certificate */ 96 | filecontent = trim(filecontent); 97 | filecontent = replace(filecontent, /\r\n?/g, '\n'); 98 | if (!match(filecontent, /\n$/)) 99 | filecontent += '\n'; 100 | 101 | system(`mkdir -p ${HP_DIR}/certs`); 102 | writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent); 103 | system(`rm -f ${tmpcert}`); 104 | 105 | return { result: true }; 106 | }; 107 | 108 | const filename = req.args?.filename; 109 | switch (filename) { 110 | case 'client_ca': 111 | case 'server_publickey': 112 | return writeCertificate(filename, false); 113 | break; 114 | case 'server_privatekey': 115 | return writeCertificate(filename, true); 116 | break; 117 | default: 118 | return { result: false, error: 'illegal cerificate filename' }; 119 | break; 120 | } 121 | } 122 | }, 123 | 124 | connection_check: { 125 | args: { site: 'site' }, 126 | call: function(req) { 127 | let url; 128 | switch(req.args?.site) { 129 | case 'baidu': 130 | url = 'https://www.baidu.com'; 131 | break; 132 | case 'google': 133 | url = 'https://www.google.com'; 134 | break; 135 | default: 136 | return { result: false, error: 'illegal site' }; 137 | break; 138 | } 139 | 140 | return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) }; 141 | } 142 | }, 143 | 144 | log_clean: { 145 | args: { type: 'type' }, 146 | call: function(req) { 147 | if (!(req.args?.type in ['homeproxy', 'sing-box-c', 'sing-box-s'])) 148 | return { result: false, error: 'illegal type' }; 149 | 150 | const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`); 151 | if (filestat) 152 | writefile(`${RUN_DIR}/${req.args?.type}.log`, ''); 153 | return { result: true }; 154 | } 155 | }, 156 | 157 | singbox_generator: { 158 | args: { type: 'type', params: 'params' }, 159 | call: function(req) { 160 | if (!(req.args?.type in ['ech-keypair', 'uuid', 'reality-keypair', 'vapid-keypair', 'wg-keypair'])) 161 | return { result: false, error: 'illegal type' }; 162 | 163 | const type = req.args?.type; 164 | let result = {}; 165 | 166 | const fd = popen('/usr/bin/sing-box generate ' + type + ` ${req.args?.params || ''}`); 167 | if (fd) { 168 | let ech_cfg_set = false; 169 | let ech_key_set = false; 170 | 171 | for (let line = fd.read('line'); length(line); line = fd.read('line')) { 172 | if (type === 'uuid') 173 | result.uuid = trim(line); 174 | else if (type in ['reality-keypair', 'vapid-keypair', 'wg-keypair']) { 175 | let priv = match(trim(line), /PrivateKey: (.*)/); 176 | if (priv) 177 | result.private_key = priv[1]; 178 | let pub = match(trim(line), /PublicKey: (.*)/); 179 | if (pub) 180 | result.public_key = pub[1]; 181 | } else if (type in ['ech-keypair']) { 182 | if (trim(line) === '-----BEGIN ECH CONFIGS-----') 183 | ech_cfg_set = true; 184 | else if (trim(line) === '-----BEGIN ECH KEYS-----') 185 | ech_key_set = true; 186 | 187 | if (ech_cfg_set) 188 | result.ech_cfg = result.ech_cfg ? result.ech_cfg + '\n' + trim(line) : trim(line) ; 189 | if (ech_key_set) 190 | result.ech_key = result.ech_key ? result.ech_key + '\n' + trim(line) : trim(line) ; 191 | 192 | if (trim(line) === '-----END ECH CONFIGS-----') 193 | ech_cfg_set = false; 194 | else if (trim(line) === '-----END ECH KEYS-----') 195 | ech_key_set = false; 196 | } 197 | } 198 | 199 | fd.close(); 200 | } 201 | 202 | return { result }; 203 | } 204 | }, 205 | 206 | singbox_get_features: { 207 | call: function() { 208 | let features = {}; 209 | 210 | const fd = popen('/usr/bin/sing-box version'); 211 | if (fd) { 212 | for (let line = fd.read('line'); length(line); line = fd.read('line')) { 213 | if (match(trim(line), /^sing-box version (.*)/)) 214 | features.version = match(trim(line), /^sing-box version (.*)/)[1]; 215 | 216 | let tags = match(trim(line), /^Tags: (.*)/); 217 | if (tags) 218 | for (let i in split(tags[1], ',')) 219 | features[i] = true; 220 | } 221 | 222 | fd.close(); 223 | } 224 | 225 | features.hp_has_ip_full = access('/usr/libexec/ip-full'); 226 | features.hp_has_tcp_brutal = hasKernelModule('brutal.ko'); 227 | features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy'); 228 | features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun'); 229 | 230 | return features; 231 | } 232 | }, 233 | 234 | resources_get_version: { 235 | args: { type: 'type' }, 236 | call: function(req) { 237 | const version = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`)); 238 | return { version: version, error: error() }; 239 | } 240 | }, 241 | resources_update: { 242 | args: { type: 'type' }, 243 | call: function(req) { 244 | if (req.args?.type) { 245 | const type = shellquote(req.args?.type); 246 | const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type}`); 247 | return { status: exit_code }; 248 | } else 249 | return { status: 255, error: 'illegal type' }; 250 | } 251 | } 252 | }; 253 | 254 | return { 'luci.homeproxy': methods }; 255 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/homeproxy/status.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-2.0-only 3 | * 4 | * Copyright (C) 2022-2025 ImmortalWrt.org 5 | */ 6 | 7 | 'use strict'; 8 | 'require dom'; 9 | 'require form'; 10 | 'require fs'; 11 | 'require poll'; 12 | 'require rpc'; 13 | 'require uci'; 14 | 'require ui'; 15 | 'require view'; 16 | 17 | /* Thanks to luci-app-aria2 */ 18 | const css = ' \ 19 | #log_textarea { \ 20 | padding: 10px; \ 21 | text-align: left; \ 22 | } \ 23 | #log_textarea pre { \ 24 | padding: .5rem; \ 25 | word-break: break-all; \ 26 | margin: 0; \ 27 | } \ 28 | .description { \ 29 | background-color: #33ccff; \ 30 | }'; 31 | 32 | const hp_dir = '/var/run/homeproxy'; 33 | 34 | function getConnStat(o, site) { 35 | const callConnStat = rpc.declare({ 36 | object: 'luci.homeproxy', 37 | method: 'connection_check', 38 | params: ['site'], 39 | expect: { '': {} } 40 | }); 41 | 42 | o.default = E('div', { 'style': 'cbi-value-field' }, [ 43 | E('button', { 44 | 'class': 'btn cbi-button cbi-button-action', 45 | 'click': ui.createHandlerFn(this, () => { 46 | return L.resolveDefault(callConnStat(site), {}).then((ret) => { 47 | let ele = o.default.firstElementChild.nextElementSibling; 48 | if (ret.result) { 49 | ele.style.setProperty('color', 'green'); 50 | ele.innerHTML = _('passed'); 51 | } else { 52 | ele.style.setProperty('color', 'red'); 53 | ele.innerHTML = _('failed'); 54 | } 55 | }); 56 | }) 57 | }, [ _('Check') ]), 58 | ' ', 59 | E('strong', { 'style': 'color:gray' }, _('unchecked')), 60 | ]); 61 | } 62 | 63 | function getResVersion(o, type) { 64 | const callResVersion = rpc.declare({ 65 | object: 'luci.homeproxy', 66 | method: 'resources_get_version', 67 | params: ['type'], 68 | expect: { '': {} } 69 | }); 70 | 71 | const callResUpdate = rpc.declare({ 72 | object: 'luci.homeproxy', 73 | method: 'resources_update', 74 | params: ['type'], 75 | expect: { '': {} } 76 | }); 77 | 78 | return L.resolveDefault(callResVersion(type), {}).then((res) => { 79 | let spanTemp = E('div', { 'style': 'cbi-value-field' }, [ 80 | E('button', { 81 | 'class': 'btn cbi-button cbi-button-action', 82 | 'click': ui.createHandlerFn(this, () => { 83 | return L.resolveDefault(callResUpdate(type), {}).then((res) => { 84 | switch (res.status) { 85 | case 0: 86 | o.description = _('Successfully updated.'); 87 | break; 88 | case 1: 89 | o.description = _('Update failed.'); 90 | break; 91 | case 2: 92 | o.description = _('Already in updating.'); 93 | break; 94 | case 3: 95 | o.description = _('Already at the latest version.'); 96 | break; 97 | default: 98 | o.description = _('Unknown error.'); 99 | break; 100 | } 101 | 102 | return o.map.reset(); 103 | }); 104 | }) 105 | }, [ _('Check update') ]), 106 | ' ', 107 | E('strong', { 'style': (res.error ? 'color:red' : 'color:green') }, 108 | [ res.error ? 'not found' : res.version ] 109 | ), 110 | ]); 111 | 112 | o.default = spanTemp; 113 | }); 114 | } 115 | 116 | function getRuntimeLog(o, name, _option_index, section_id, _in_table) { 117 | const filename = o.option.split('_')[1]; 118 | 119 | let section, log_level_el; 120 | switch (filename) { 121 | case 'homeproxy': 122 | section = null; 123 | break; 124 | case 'sing-box-c': 125 | section = 'config'; 126 | break; 127 | case 'sing-box-s': 128 | section = 'server'; 129 | break; 130 | } 131 | 132 | if (section) { 133 | const selected = uci.get('homeproxy', section, 'log_level') || 'warn'; 134 | const choices = { 135 | trace: _('Trace'), 136 | debug: _('Debug'), 137 | info: _('Info'), 138 | warn: _('Warn'), 139 | error: _('Error'), 140 | fatal: _('Fatal'), 141 | panic: _('Panic') 142 | }; 143 | 144 | log_level_el = E('select', { 145 | 'id': o.cbid(section_id), 146 | 'class': 'cbi-input-select', 147 | 'style': 'margin-left: 4px; width: 6em;', 148 | 'change': ui.createHandlerFn(this, (ev) => { 149 | uci.set('homeproxy', section, 'log_level', ev.target.value); 150 | return o.map.save(null, true).then(() => { 151 | ui.changes.apply(true); 152 | }); 153 | }) 154 | }); 155 | 156 | Object.keys(choices).forEach((v) => { 157 | log_level_el.appendChild(E('option', { 158 | 'value': v, 159 | 'selected': (v === selected) ? '' : null 160 | }, [ choices[v] ])); 161 | }); 162 | } 163 | 164 | const callLogClean = rpc.declare({ 165 | object: 'luci.homeproxy', 166 | method: 'log_clean', 167 | params: ['type'], 168 | expect: { '': {} } 169 | }); 170 | 171 | const log_textarea = E('div', { 'id': 'log_textarea' }, 172 | E('img', { 173 | 'src': L.resource('icons/loading.svg'), 174 | 'alt': _('Loading'), 175 | 'style': 'vertical-align:middle' 176 | }, _('Collecting data...')) 177 | ); 178 | 179 | let log; 180 | poll.add(L.bind(() => { 181 | return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text') 182 | .then((res) => { 183 | log = E('pre', { 'wrap': 'pre' }, [ 184 | res.trim() || _('Log is empty.') 185 | ]); 186 | 187 | dom.content(log_textarea, log); 188 | }).catch((err) => { 189 | if (err.toString().includes('NotFoundError')) 190 | log = E('pre', { 'wrap': 'pre' }, [ 191 | _('Log file does not exist.') 192 | ]); 193 | else 194 | log = E('pre', { 'wrap': 'pre' }, [ 195 | _('Unknown error: %s').format(err) 196 | ]); 197 | 198 | dom.content(log_textarea, log); 199 | }); 200 | })); 201 | 202 | return E([ 203 | E('style', [ css ]), 204 | E('div', {'class': 'cbi-map'}, [ 205 | E('h3', {'name': 'content', 'style': 'align-items: center; display: flex;'}, [ 206 | _('%s log').format(name), 207 | log_level_el || '', 208 | E('button', { 209 | 'class': 'btn cbi-button cbi-button-action', 210 | 'style': 'margin-left: 4px;', 211 | 'click': ui.createHandlerFn(this, () => { 212 | return L.resolveDefault(callLogClean(filename), {}); 213 | }) 214 | }, [ _('Clean log') ]) 215 | ]), 216 | E('div', {'class': 'cbi-section'}, [ 217 | log_textarea, 218 | E('div', {'style': 'text-align:right'}, 219 | E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)) 220 | ) 221 | ]) 222 | ]) 223 | ]); 224 | } 225 | 226 | return view.extend({ 227 | render() { 228 | let m, s, o; 229 | 230 | m = new form.Map('homeproxy'); 231 | 232 | s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check')); 233 | s.anonymous = true; 234 | 235 | o = s.option(form.DummyValue, '_check_baidu', _('BaiDu')); 236 | o.cfgvalue = L.bind(getConnStat, this, o, 'baidu'); 237 | 238 | o = s.option(form.DummyValue, '_check_google', _('Google')); 239 | o.cfgvalue = L.bind(getConnStat, this, o, 'google'); 240 | 241 | s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management')); 242 | s.anonymous = true; 243 | 244 | o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version')); 245 | o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip4'); 246 | o.rawhtml = true; 247 | 248 | o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version')); 249 | o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip6'); 250 | o.rawhtml = true; 251 | 252 | o = s.option(form.DummyValue, '_china_list_version', _('China list version')); 253 | o.cfgvalue = L.bind(getResVersion, this, o, 'china_list'); 254 | o.rawhtml = true; 255 | 256 | o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version')); 257 | o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list'); 258 | o.rawhtml = true; 259 | 260 | o = s.option(form.Value, 'github_token', _('GitHub token')); 261 | o.password = true; 262 | o.renderWidget = function() { 263 | let node = form.Value.prototype.renderWidget.apply(this, arguments); 264 | 265 | (node.querySelector('.control-group') || node).appendChild(E('button', { 266 | 'class': 'cbi-button cbi-button-apply', 267 | 'title': _('Save'), 268 | 'click': ui.createHandlerFn(this, () => { 269 | return this.map.save(null, true).then(() => { 270 | ui.changes.apply(true); 271 | }); 272 | }, this.option) 273 | }, [ _('Save') ])); 274 | 275 | return node; 276 | } 277 | 278 | s = m.section(form.NamedSection, 'config', 'homeproxy'); 279 | s.anonymous = true; 280 | 281 | o = s.option(form.DummyValue, '_homeproxy_logview'); 282 | o.render = L.bind(getRuntimeLog, this, o, _('HomeProxy')); 283 | 284 | o = s.option(form.DummyValue, '_sing-box-c_logview'); 285 | o.render = L.bind(getRuntimeLog, this, o, _('sing-box client')); 286 | 287 | o = s.option(form.DummyValue, '_sing-box-s_logview'); 288 | o.render = L.bind(getRuntimeLog, this, o, _('sing-box server')); 289 | 290 | return m.render(); 291 | }, 292 | 293 | handleSaveApply: null, 294 | handleSave: null, 295 | handleReset: null 296 | }); 297 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/migrate_config.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | /* 3 | * SPDX-License-Identifier: GPL-2.0-only 4 | * 5 | * Copyright (C) 2025 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { cursor } from 'uci'; 11 | import { isEmpty, parseURL } from 'homeproxy'; 12 | 13 | const uci = cursor(); 14 | 15 | const uciconfig = 'homeproxy'; 16 | uci.load(uciconfig); 17 | 18 | const uciinfra = 'infra', 19 | ucimigration = 'migration', 20 | ucimain = 'config', 21 | ucinode = 'node', 22 | ucidns = 'dns', 23 | ucidnsserver = 'dns_server', 24 | ucidnsrule = 'dns_rule', 25 | ucirouting = 'routing', 26 | uciroutingnode = 'routing_node', 27 | uciroutingrule = 'routing_rule', 28 | uciserver = 'server'; 29 | 30 | /* chinadns-ng has been removed */ 31 | if (uci.get(uciconfig, uciinfra, 'china_dns_port')) 32 | uci.delete(uciconfig, uciinfra, 'china_dns_port'); 33 | 34 | /* chinadns server now only accepts single server */ 35 | const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server'); 36 | if (type(china_dns_server) === 'array') { 37 | uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]); 38 | } else { 39 | if (china_dns_server === 'wan_114') 40 | uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114'); 41 | else if (match(china_dns_server, /,/)) 42 | uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]); 43 | } 44 | 45 | /* github_token option has been moved to config section */ 46 | const github_token = uci.get(uciconfig, uciinfra, 'github_token'); 47 | if (github_token) { 48 | uci.set(uciconfig, ucimain, 'github_token', github_token); 49 | uci.delete(uciconfig, uciinfra, 'github_token') 50 | } 51 | 52 | /* ntp_server was introduced */ 53 | if (!uci.get(uciconfig, uciinfra, 'ntp_server')) 54 | uci.set(uciconfig, uciinfra, 'ntp_server', 'nil'); 55 | 56 | /* tun_gso was deprecated in sb 1.11 */ 57 | if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso'))) 58 | uci.delete(uciconfig, uciinfra, 'tun_gso'); 59 | 60 | /* create migration section */ 61 | if (!uci.get(uciconfig, ucimigration)) 62 | uci.set(uciconfig, ucimigration, uciconfig); 63 | 64 | /* delete old crontab command */ 65 | const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab'); 66 | if (!migration_crontab) { 67 | system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"'); 68 | uci.set(uciconfig, ucimigration, 'crontab', '1'); 69 | } 70 | 71 | /* log_level was introduced */ 72 | if (isEmpty(uci.get(uciconfig, ucimain, 'log_level'))) 73 | uci.set(uciconfig, ucimain, 'log_level', 'warn'); 74 | 75 | if (isEmpty(uci.get(uciconfig, uciserver, 'log_level'))) 76 | uci.set(uciconfig, uciserver, 'log_level', 'warn'); 77 | 78 | /* empty value defaults to all ports now */ 79 | if (uci.get(uciconfig, ucimain, 'routing_port') === 'all') 80 | uci.delete(uciconfig, ucimain, 'routing_port'); 81 | 82 | /* experimental section was removed */ 83 | if (uci.get(uciconfig, 'experimental')) 84 | uci.delete(uciconfig, 'experimental'); 85 | 86 | /* block-dns was removed from built-in dns servers */ 87 | const default_dns_server = uci.get(uciconfig, ucidns, 'default_server'); 88 | if (default_dns_server === 'block-dns') { 89 | /* append a rule at last to block all DNS queries */ 90 | uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule); 91 | uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns'); 92 | uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1'); 93 | uci.set(uciconfig, '_migration_dns_final_block', 'mode', 'default'); 94 | uci.set(uciconfig, '_migration_dns_final_block', 'action', 'reject'); 95 | uci.set(uciconfig, ucidns, 'default_server', 'default-dns'); 96 | } 97 | 98 | const dns_server_migration = {}; 99 | /* DNS servers options */ 100 | uci.foreach(uciconfig, ucidnsserver, (cfg) => { 101 | /* legacy format was deprecated in sb 1.12 */ 102 | if (cfg.address) { 103 | const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address)); 104 | /* RCode was moved into DNS rules */ 105 | if (addr.protocol === 'rcode') { 106 | dns_server_migration[cfg['.name']] = { action: 'predefined' }; 107 | switch (addr.hostname) { 108 | case 'success': 109 | dns_server_migration[cfg['.name']].rcode = 'NOERROR'; 110 | break; 111 | case 'format_error': 112 | dns_server_migration[cfg['.name']].rcode = 'FORMERR'; 113 | break; 114 | case 'server_failure': 115 | dns_server_migration[cfg['.name']].rcode = 'SERVFAIL'; 116 | break; 117 | case 'name_error': 118 | dns_server_migration[cfg['.name']].rcode = 'NXDOMAIN'; 119 | break; 120 | case 'not_implemented': 121 | dns_server_migration[cfg['.name']].rcode = 'NOTIMP'; 122 | break; 123 | case 'refused': 124 | default: 125 | dns_server_migration[cfg['.name']].rcode = 'REFUSED'; 126 | break; 127 | } 128 | 129 | uci.delete(uciconfig, cfg['.name']); 130 | return; 131 | } 132 | uci.set(uciconfig, cfg['.name'], 'type', addr.protocol); 133 | uci.set(uciconfig, cfg['.name'], 'server', addr.hostname); 134 | uci.set(uciconfig, cfg['.name'], 'server_port', addr.port); 135 | uci.set(uciconfig, cfg['.name'], 'path', (addr.pathname !== '/') ? addr.pathname : null); 136 | uci.delete(uciconfig, cfg['.name'], 'address'); 137 | } 138 | 139 | if (cfg.strategy) { 140 | if (cfg['.name'] === default_dns_server) 141 | uci.set(uciconfig, ucidns, 'default_strategy', cfg.strategy); 142 | dns_server_migration[cfg['.name']] = { strategy: cfg.strategy }; 143 | uci.delete(uciconfig, cfg['.name'], 'strategy'); 144 | } 145 | 146 | if (cfg.client_subnet) { 147 | if (cfg['.name'] === default_dns_server) 148 | uci.set(uciconfig, ucidns, 'client_subnet', cfg.client_subnet); 149 | 150 | if (isEmpty(dns_server_migration[cfg['.name']])) 151 | dns_server_migration[cfg['.name']] = {}; 152 | dns_server_migration[cfg['.name']].client_subnet = cfg.client_subnet; 153 | uci.delete(uciconfig, cfg['.name'], 'client_subnet'); 154 | } 155 | }); 156 | 157 | /* DNS rules options */ 158 | uci.foreach(uciconfig, ucidnsrule, (cfg) => { 159 | /* outbound was removed in sb 1.12 */ 160 | if (cfg.outbound) { 161 | uci.delete(uciconfig, cfg['.name']); 162 | if (!cfg.enabled) 163 | return; 164 | 165 | map(cfg.outbound, (outbound) => { 166 | switch (outbound) { 167 | case 'direct-out': 168 | case 'block-out': 169 | break; 170 | case 'any-out': 171 | uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server); 172 | break; 173 | default: 174 | uci.set(uciconfig, cfg.outbound, 'domain_resolver', cfg.server); 175 | break; 176 | } 177 | }); 178 | 179 | return; 180 | } 181 | 182 | /* rule_set_ipcidr_match_source was renamed in sb 1.10 */ 183 | if (cfg.rule_set_ipcidr_match_source === '1') 184 | uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source'); 185 | 186 | /* block-dns was moved into action in sb 1.11 */ 187 | if (cfg.server === 'block-dns') { 188 | uci.set(uciconfig, cfg['.name'], 'action', 'reject'); 189 | uci.delete(uciconfig, cfg['.name'], 'server'); 190 | } else if (!cfg.action) { 191 | /* add missing 'action' field */ 192 | uci.set(uciconfig, cfg['.name'], 'action', 'route'); 193 | } 194 | 195 | /* strategy and client_subnet were moved into dns rules */ 196 | if (dns_server_migration[cfg.server]) { 197 | if (dns_server_migration[cfg.server].strategy) 198 | uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy); 199 | 200 | if (dns_server_migration[cfg.server].client_subnet) 201 | uci.set(uciconfig, cfg['.name'], 'client_subnet', dns_server_migration[cfg.server].client_subnet); 202 | 203 | if (dns_server_migration[cfg.server].rcode) { 204 | uci.set(uciconfig, cfg['.name'], 'action', 'predefined'); 205 | uci.set(uciconfig, cfg['.name'], 'rcode', dns_server_migration[cfg.server].rcode); 206 | uci.delete(uciconfig, cfg['.name'], 'server'); 207 | } 208 | } 209 | }); 210 | 211 | /* nodes options */ 212 | uci.foreach(uciconfig, ucinode, (cfg) => { 213 | /* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */ 214 | if (!isEmpty(cfg.tls_ech_tls_disable_drs)) 215 | uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs'); 216 | 217 | /* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */ 218 | if (!isEmpty(cfg.tls_ech_enable_pqss)) 219 | uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss'); 220 | 221 | /* wireguard_gso was deprecated in sb 1.11 */ 222 | if (!isEmpty(cfg.wireguard_gso)) 223 | uci.delete(uciconfig, cfg['.name'], 'wireguard_gso'); 224 | }); 225 | 226 | /* routing rules options */ 227 | uci.foreach(uciconfig, uciroutingrule, (cfg) => { 228 | /* rule_set_ipcidr_match_source was renamed in sb 1.10 */ 229 | if (cfg.rule_set_ipcidr_match_source === '1') 230 | uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source'); 231 | 232 | /* block-out was moved into action in sb 1.11 */ 233 | if (cfg.outbound === 'block-out') { 234 | uci.set(uciconfig, cfg['.name'], 'action', 'reject'); 235 | uci.delete(uciconfig, cfg['.name'], 'outbound'); 236 | } else if (!cfg.action) { 237 | /* add missing 'action' field */ 238 | uci.set(uciconfig, cfg['.name'], 'action', 'route'); 239 | } 240 | }); 241 | 242 | /* server options */ 243 | /* auto_firewall was moved into server options */ 244 | const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall'); 245 | if (!isEmpty(auto_firewall)) 246 | uci.delete(uciconfig, uciserver, 'auto_firewall'); 247 | 248 | uci.foreach(uciconfig, uciserver, (cfg) => { 249 | /* auto_firewall was moved into server options */ 250 | if (auto_firewall === '1') 251 | uci.set(uciconfig, cfg['.name'], 'firewall' , '1'); 252 | 253 | /* sniff_override was deprecated in sb 1.11 */ 254 | if (!isEmpty(cfg.sniff_override)) 255 | uci.delete(uciconfig, cfg['.name'], 'sniff_override'); 256 | 257 | /* domain_strategy is now pointless without sniff override */ 258 | if (!isEmpty(cfg.domain_strategy)) 259 | uci.delete(uciconfig, cfg['.name'], 'domain_strategy'); 260 | }); 261 | 262 | if (!isEmpty(uci.changes(uciconfig))) 263 | uci.commit(uciconfig); 264 | -------------------------------------------------------------------------------- /root/etc/init.d/homeproxy: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2022-2023 ImmortalWrt.org 5 | 6 | USE_PROCD=1 7 | 8 | START=99 9 | STOP=10 10 | 11 | CONF="homeproxy" 12 | PROG="/usr/bin/sing-box" 13 | 14 | HP_DIR="/etc/homeproxy" 15 | RUN_DIR="/var/run/homeproxy" 16 | LOG_PATH="$RUN_DIR/homeproxy.log" 17 | 18 | # we don't know which is the default server, just take the first one 19 | DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')" 20 | if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then 21 | DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-homeproxy.d" 22 | else 23 | DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d" 24 | fi 25 | 26 | log() { 27 | echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH" 28 | } 29 | 30 | start_service() { 31 | config_load "$CONF" 32 | 33 | local routing_mode proxy_mode 34 | config_get routing_mode "config" "routing_mode" "bypass_mainland_china" 35 | config_get proxy_mode "config" "proxy_mode" "redirect_tproxy" 36 | 37 | local outbound_node 38 | if [ "$routing_mode" != "custom" ]; then 39 | config_get outbound_node "config" "main_node" "nil" 40 | else 41 | config_get outbound_node "routing" "default_outbound" "nil" 42 | fi 43 | 44 | local server_enabled 45 | config_get_bool server_enabled "server" "enabled" "0" 46 | 47 | if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then 48 | return 1 49 | fi 50 | 51 | mkdir -p "$RUN_DIR" 52 | 53 | if [ "$outbound_node" != "nil" ]; then 54 | # Generate/Validate client config 55 | ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH" 56 | 57 | if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then 58 | log "Error: failed to generate client configuration." 59 | return 1 60 | elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then 61 | log "Error: wrong client configuration detected." 62 | return 1 63 | fi 64 | 65 | # Auto update 66 | local auto_update auto_update_time 67 | config_get_bool auto_update "subscription" "auto_update" "0" 68 | if [ "$auto_update" = "1" ]; then 69 | config_get auto_update_time "subscription" "auto_update_time" "2" 70 | sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null" 71 | echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root" 72 | /etc/init.d/cron restart 73 | fi 74 | 75 | # DNSMasq rules 76 | local ipv6_support dns_port 77 | config_get_bool ipv6_support "config" "ipv6_support" "0" 78 | config_get dns_port "infra" "dns_port" "5333" 79 | mkdir -p "$DNSMASQ_DIR" 80 | echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" 81 | case "$routing_mode" in 82 | "bypass_mainland_china"|"custom"|"global") 83 | cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf" 84 | no-poll 85 | no-resolv 86 | server=127.0.0.1#$dns_port 87 | EOF 88 | ;; 89 | "gfwlist") 90 | local gfw_nftset_v6 91 | [ "$ipv6_support" -eq "0" ] || gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6" 92 | sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \ 93 | "$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf" 94 | ;; 95 | "proxy_mainland_china") 96 | sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \ 97 | "$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf" 98 | ;; 99 | esac 100 | 101 | if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then 102 | local wan_nftset_v6 103 | [ "$ipv6_support" -eq "0" ] || wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6" 104 | sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \ 105 | "$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf" 106 | fi 107 | /etc/init.d/dnsmasq restart >"/dev/null" 2>&1 108 | 109 | # Setup routing table 110 | local table_mark 111 | config_get table_mark "infra" "table_mark" "100" 112 | case "$proxy_mode" in 113 | "redirect_tproxy") 114 | local outbound_udp_node 115 | config_get outbound_udp_node "config" "main_udp_node" "nil" 116 | if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then 117 | local tproxy_mark 118 | config_get tproxy_mark "infra" "tproxy_mark" "101" 119 | 120 | ip rule add fwmark "$tproxy_mark" table "$table_mark" 121 | ip route add local 0.0.0.0/0 dev lo table "$table_mark" 122 | 123 | if [ "$ipv6_support" -eq "1" ]; then 124 | ip -6 rule add fwmark "$tproxy_mark" table "$table_mark" 125 | ip -6 route add local ::/0 dev lo table "$table_mark" 126 | fi 127 | fi 128 | ;; 129 | "redirect_tun"|"tun") 130 | local tun_name tun_mark 131 | config_get tun_name "infra" "tun_name" "singtun0" 132 | config_get tun_mark "infra" "tun_mark" "102" 133 | 134 | ip tuntap add mode tun user root name "$tun_name" 135 | sleep 1s 136 | ip link set "$tun_name" up 137 | 138 | ip route replace default dev "$tun_name" table "$table_mark" 139 | ip rule add fwmark "$tun_mark" lookup "$table_mark" 140 | 141 | ip -6 route replace default dev "$tun_name" table "$table_mark" 142 | ip -6 rule add fwmark "$tun_mark" lookup "$table_mark" 143 | ;; 144 | esac 145 | 146 | # sing-box (client) 147 | procd_open_instance "sing-box-c" 148 | 149 | procd_set_param command "$PROG" 150 | procd_append_param command run --config "$RUN_DIR/sing-box-c.json" 151 | 152 | # QUIC-GO GSO is broken on kernel 6.6 currently 153 | uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true" 154 | 155 | if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then 156 | procd_add_jail "sing-box-c" log procfs 157 | procd_add_jail_mount "$RUN_DIR/sing-box-c.json" 158 | procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log" 159 | [ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$RUN_DIR/cache.db" 160 | procd_add_jail_mount "$HP_DIR/certs/" 161 | procd_add_jail_mount "/etc/ssl/" 162 | procd_add_jail_mount "/etc/localtime" 163 | procd_add_jail_mount "/etc/TZ" 164 | procd_set_param capabilities "/etc/capabilities/homeproxy.json" 165 | procd_set_param no_new_privs 1 166 | procd_set_param user sing-box 167 | procd_set_param group sing-box 168 | fi 169 | 170 | procd_set_param limits core="unlimited" 171 | procd_set_param limits nofile="1000000 1000000" 172 | procd_set_param stderr 1 173 | procd_set_param respawn 174 | 175 | procd_close_instance 176 | fi 177 | 178 | if [ "$server_enabled" = "1" ]; then 179 | # Generate/Validate server config 180 | ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH" 181 | 182 | if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then 183 | log "Error: failed to generate server configuration." 184 | return 1 185 | elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then 186 | log "Error: wrong server configuration detected." 187 | return 1 188 | fi 189 | 190 | # sing-box (server) 191 | procd_open_instance "sing-box-s" 192 | 193 | procd_set_param command "$PROG" 194 | procd_append_param command run --config "$RUN_DIR/sing-box-s.json" 195 | 196 | # QUIC-GO GSO is broken on kernel 6.6 currently 197 | uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true" 198 | 199 | if [ -x "/sbin/ujail" ]; then 200 | procd_add_jail "sing-box-s" log procfs 201 | procd_add_jail_mount "$RUN_DIR/sing-box-s.json" 202 | procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log" 203 | procd_add_jail_mount_rw "$HP_DIR/certs/" 204 | procd_add_jail_mount "/etc/acme/" 205 | procd_add_jail_mount "/etc/ssl/" 206 | procd_add_jail_mount "/etc/localtime" 207 | procd_add_jail_mount "/etc/TZ" 208 | procd_set_param capabilities "/etc/capabilities/homeproxy.json" 209 | procd_set_param no_new_privs 1 210 | procd_set_param user sing-box 211 | procd_set_param group sing-box 212 | fi 213 | 214 | procd_set_param limits core="unlimited" 215 | procd_set_param limits nofile="1000000 1000000" 216 | procd_set_param stderr 1 217 | procd_set_param respawn 218 | 219 | procd_close_instance 220 | fi 221 | 222 | # log-cleaner 223 | procd_open_instance "log-cleaner" 224 | procd_set_param command "$HP_DIR/scripts/clean_log.sh" 225 | procd_set_param respawn 226 | procd_close_instance 227 | 228 | case "$routing_mode" in 229 | "bypass_mainland_china") 230 | # Prepare cache db 231 | [ -e "$RUN_DIR/cache.db" ] || touch "$RUN_DIR/cache.db" 232 | ;; 233 | "custom") 234 | # Prepare ruleset directory 235 | [ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset" 236 | ;; 237 | esac 238 | 239 | [ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log" 240 | if [ "$server_enabled" = "1" ]; then 241 | echo > "$RUN_DIR/sing-box-s.log" 242 | mkdir -p "$HP_DIR/certs" 243 | fi 244 | 245 | # Update permissions for ujail 246 | chown -R sing-box:sing-box "$RUN_DIR" 247 | 248 | # Setup firewall 249 | ucode "$HP_DIR/scripts/firewall_pre.uc" 250 | [ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft" 251 | fw4 reload >"/dev/null" 2>&1 252 | 253 | log "sing-box $(sing-box version -n) started." 254 | } 255 | 256 | stop_service() { 257 | sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null" 258 | /etc/init.d/cron restart >"/dev/null" 2>&1 259 | 260 | # Setup firewall 261 | # Load config 262 | config_load "$CONF" 263 | local table_mark tproxy_mark tun_mark tun_name 264 | config_get table_mark "infra" "table_mark" "100" 265 | config_get tproxy_mark "infra" "tproxy_mark" "101" 266 | config_get tun_mark "infra" "tun_mark" "102" 267 | config_get tun_name "infra" "tun_name" "singtun0" 268 | 269 | # Tproxy 270 | ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" 271 | ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null" 272 | ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" 273 | ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null" 274 | 275 | # TUN 276 | ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null" 277 | ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null" 278 | 279 | ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null" 280 | ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null" 281 | 282 | # Nftables rules 283 | for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \ 284 | "homeproxy_redirect" "homeproxy_redirect_proxy" \ 285 | "homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \ 286 | "homeproxy_mangle_prerouting" "homeproxy_mangle_output" \ 287 | "homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \ 288 | "homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \ 289 | "homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do 290 | nft flush chain inet fw4 "$i" 291 | nft delete chain inet fw4 "$i" 292 | done 2>"/dev/null" 293 | for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \ 294 | "homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \ 295 | "homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \ 296 | "homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \ 297 | "homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \ 298 | "homeproxy_routing_port"; do 299 | nft flush set inet fw4 "$i" 300 | nft delete set inet fw4 "$i" 301 | done 2>"/dev/null" 302 | echo 2>"/dev/null" > "$RUN_DIR/fw4_forward.nft" 303 | echo 2>"/dev/null" > "$RUN_DIR/fw4_input.nft" 304 | echo 2>"/dev/null" > "$RUN_DIR/fw4_post.nft" 305 | fw4 reload >"/dev/null" 2>&1 306 | 307 | # Remove DNS hijack 308 | rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR" 309 | /etc/init.d/dnsmasq restart >"/dev/null" 2>&1 310 | 311 | rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \ 312 | "$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log" 313 | 314 | log "Service stopped." 315 | } 316 | 317 | service_stopped() { 318 | # Load config 319 | config_load "$CONF" 320 | local tun_name 321 | config_get tun_name "infra" "tun_name" "singtun0" 322 | 323 | # TUN 324 | ip link set "$tun_name" down 2>"/dev/null" 325 | ip tuntap del mode tun name "$tun_name" 2>"/dev/null" 326 | } 327 | 328 | reload_service() { 329 | log "Reloading service..." 330 | 331 | stop 332 | start 333 | } 334 | 335 | service_triggers() { 336 | procd_add_reload_trigger "$CONF" 337 | procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload 338 | } 339 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/homeproxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-2.0-only 3 | * 4 | * Copyright (C) 2022-2025 ImmortalWrt.org 5 | */ 6 | 7 | 'use strict'; 8 | 'require baseclass'; 9 | 'require form'; 10 | 'require fs'; 11 | 'require rpc'; 12 | 'require uci'; 13 | 'require ui'; 14 | 15 | return baseclass.extend({ 16 | dns_strategy: { 17 | '': _('Default'), 18 | 'prefer_ipv4': _('Prefer IPv4'), 19 | 'prefer_ipv6': _('Prefer IPv6'), 20 | 'ipv4_only': _('IPv4 only'), 21 | 'ipv6_only': _('IPv6 only') 22 | }, 23 | 24 | shadowsocks_encrypt_length: { 25 | /* AEAD */ 26 | 'aes-128-gcm': 0, 27 | 'aes-192-gcm': 0, 28 | 'aes-256-gcm': 0, 29 | 'chacha20-ietf-poly1305': 0, 30 | 'xchacha20-ietf-poly1305': 0, 31 | /* AEAD 2022 */ 32 | '2022-blake3-aes-128-gcm': 16, 33 | '2022-blake3-aes-256-gcm': 32, 34 | '2022-blake3-chacha20-poly1305': 32 35 | }, 36 | 37 | shadowsocks_encrypt_methods: [ 38 | /* Stream */ 39 | 'none', 40 | /* AEAD */ 41 | 'aes-128-gcm', 42 | 'aes-192-gcm', 43 | 'aes-256-gcm', 44 | 'chacha20-ietf-poly1305', 45 | 'xchacha20-ietf-poly1305', 46 | /* AEAD 2022 */ 47 | '2022-blake3-aes-128-gcm', 48 | '2022-blake3-aes-256-gcm', 49 | '2022-blake3-chacha20-poly1305' 50 | ], 51 | 52 | tls_cipher_suites: [ 53 | 'TLS_RSA_WITH_AES_128_CBC_SHA', 54 | 'TLS_RSA_WITH_AES_256_CBC_SHA', 55 | 'TLS_RSA_WITH_AES_128_GCM_SHA256', 56 | 'TLS_RSA_WITH_AES_256_GCM_SHA384', 57 | 'TLS_AES_128_GCM_SHA256', 58 | 'TLS_AES_256_GCM_SHA384', 59 | 'TLS_CHACHA20_POLY1305_SHA256', 60 | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA', 61 | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA', 62 | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA', 63 | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA', 64 | 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256', 65 | 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', 66 | 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', 67 | 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 68 | 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256', 69 | 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256' 70 | ], 71 | 72 | tls_versions: [ 73 | '1.0', 74 | '1.1', 75 | '1.2', 76 | '1.3' 77 | ], 78 | 79 | CBIStaticList: form.DynamicList.extend({ 80 | __name__: 'CBI.StaticList', 81 | 82 | renderWidget: function(/* ... */) { 83 | let dl = form.DynamicList.prototype.renderWidget.apply(this, arguments); 84 | dl.querySelector('.add-item ul > li[data-value="-"]')?.remove(); 85 | return dl; 86 | } 87 | }), 88 | 89 | calcStringMD5(e) { 90 | /* Thanks to https://stackoverflow.com/a/41602636 */ 91 | let h = (a, b) => { 92 | let c, d, e, f, g; 93 | c = a & 2147483648; 94 | d = b & 2147483648; 95 | e = a & 1073741824; 96 | f = b & 1073741824; 97 | g = (a & 1073741823) + (b & 1073741823); 98 | return e & f ? g ^ 2147483648 ^ c ^ d : e | f ? g & 1073741824 ? g ^ 3221225472 ^ c ^ d : g ^ 1073741824 ^ c ^ d : g ^ c ^ d; 99 | }, k = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b & c | ~b & d, e), g))) << f | a >>> 32 - f, b), 100 | l = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b & d | c & ~d, e), g))) << f | a >>> 32 - f, b), 101 | m = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b ^ c ^ d, e), g))) << f | a >>> 32 - f, b), 102 | n = (a, b, c, d, e, f, g) => h((a = h(a, h(h(c ^ (b | ~d), e), g))) << f | a >>> 32 - f, b), 103 | p = a => { let b = '', d = ''; for (let c = 0; c <= 3; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2); return b; }; 104 | 105 | let f = [], q, r, s, t, a, b, c, d; 106 | e = (() => { 107 | e = e.replace(/\r\n/g, '\n'); 108 | let b = ''; 109 | for (let d = 0; d < e.length; d++) { 110 | let c = e.charCodeAt(d); 111 | b += c < 128 ? String.fromCharCode(c) : c < 2048 ? String.fromCharCode(c >> 6 | 192) + String.fromCharCode(c & 63 | 128) : 112 | String.fromCharCode(c >> 12 | 224) + String.fromCharCode(c >> 6 & 63 | 128) + String.fromCharCode(c & 63 | 128); 113 | } 114 | return b; 115 | })(); 116 | f = (() => { 117 | let c = e.length, a = c + 8, d = 16 * ((a - a % 64) / 64 + 1), b = Array(d - 1), f = 0, g = 0; 118 | for (; g < c;) a = (g - g % 4) / 4, f = g % 4 * 8, b[a] |= e.charCodeAt(g) << f, g++; 119 | a = (g - g % 4) / 4, b[a] |= 128 << g % 4 * 8, b[d - 2] = c << 3, b[d - 1] = c >>> 29; 120 | return b; 121 | })(); 122 | 123 | a = 1732584193, b = 4023233417, c = 2562383102, d = 271733878; 124 | for (e = 0; e < f.length; e += 16) { 125 | q = a, r = b, s = c, t = d; 126 | a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), 127 | c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), 128 | a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), 129 | c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), 130 | a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), 131 | c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), 132 | a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), 133 | c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), 134 | a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), 135 | c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), 136 | a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), 137 | c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), 138 | a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), 139 | c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), 140 | a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), 141 | c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), 142 | a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), 143 | c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), 144 | a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), 145 | c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), 146 | a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), 147 | c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), 148 | a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), 149 | c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), 150 | a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), 151 | c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), 152 | a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), 153 | c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), 154 | a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), 155 | c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), 156 | a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), 157 | c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), 158 | a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t); 159 | } 160 | return (p(a) + p(b) + p(c) + p(d)).toLowerCase(); 161 | }, 162 | 163 | decodeBase64Str(str) { 164 | if (!str) 165 | return null; 166 | 167 | /* Thanks to luci-app-ssr-plus */ 168 | str = str.replace(/-/g, '+').replace(/_/g, '/'); 169 | let padding = (4 - str.length % 4) % 4; 170 | if (padding) 171 | str = str + Array(padding + 1).join('='); 172 | 173 | return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => 174 | '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) 175 | ).join('')); 176 | }, 177 | 178 | getBuiltinFeatures() { 179 | const callGetSingBoxFeatures = rpc.declare({ 180 | object: 'luci.homeproxy', 181 | method: 'singbox_get_features', 182 | expect: { '': {} } 183 | }); 184 | 185 | return L.resolveDefault(callGetSingBoxFeatures(), {}); 186 | }, 187 | 188 | generateRand(type, length) { 189 | let byteArr; 190 | if (['base64', 'hex'].includes(type)) 191 | byteArr = crypto.getRandomValues(new Uint8Array(length)); 192 | switch (type) { 193 | case 'base64': 194 | /* Thanks to https://stackoverflow.com/questions/9267899 */ 195 | return btoa(String.fromCharCode.apply(null, byteArr)); 196 | case 'hex': 197 | return Array.from(byteArr, (byte) => 198 | (byte & 255).toString(16).padStart(2, '0') 199 | ).join(''); 200 | case 'uuid': 201 | /* Thanks to https://stackoverflow.com/a/2117523 */ 202 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => 203 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 204 | ); 205 | default: 206 | return null; 207 | }; 208 | }, 209 | 210 | loadDefaultLabel(uciconfig, ucisection) { 211 | let label = uci.get(uciconfig, ucisection, 'label'); 212 | if (label) { 213 | return label; 214 | } else { 215 | uci.set(uciconfig, ucisection, 'label', ucisection); 216 | return ucisection; 217 | } 218 | }, 219 | 220 | loadModalTitle(title, addtitle, uciconfig, ucisection) { 221 | let label = uci.get(uciconfig, ucisection, 'label'); 222 | return label ? title + ' » ' + label : addtitle; 223 | }, 224 | 225 | renderSectionAdd(section, extra_class) { 226 | let el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]), 227 | nameEl = el.querySelector('.cbi-section-create-name'); 228 | ui.addValidator(nameEl, 'uciname', true, (v) => { 229 | let button = el.querySelector('.cbi-section-create > .cbi-button-add'); 230 | let uciconfig = section.uciconfig || section.map.config; 231 | 232 | if (!v) { 233 | button.disabled = true; 234 | return true; 235 | } else if (uci.get(uciconfig, v)) { 236 | button.disabled = true; 237 | return _('Expecting: %s').format(_('unique UCI identifier')); 238 | } else { 239 | button.disabled = null; 240 | return true; 241 | } 242 | }, 'blur', 'keyup'); 243 | 244 | return el; 245 | }, 246 | 247 | uploadCertificate(_option, type, filename, ev) { 248 | const callWriteCertificate = rpc.declare({ 249 | object: 'luci.homeproxy', 250 | method: 'certificate_write', 251 | params: ['filename'], 252 | expect: { '': {} } 253 | }); 254 | 255 | return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target) 256 | .then(L.bind((_btn, res) => { 257 | return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => { 258 | if (ret.result === true) 259 | ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size))); 260 | else 261 | ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error))); 262 | }); 263 | }, this, ev.target)) 264 | .catch((e) => { ui.addNotification(null, E('p', e.message)) }); 265 | }, 266 | 267 | validateBase64Key(length, section_id, value) { 268 | /* Thanks to luci-proto-wireguard */ 269 | if (section_id && value) 270 | if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') 271 | return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length)); 272 | 273 | return true; 274 | }, 275 | 276 | validateCertificatePath(section_id, value) { 277 | if (section_id && value) 278 | if (!value.match(/^(\/etc\/homeproxy\/certs\/|\/etc\/acme\/|\/etc\/ssl\/).+$/)) 279 | return _('Expecting: %s').format(_('/etc/homeproxy/certs/..., /etc/acme/..., /etc/ssl/...')); 280 | 281 | return true; 282 | }, 283 | 284 | validatePortRange(section_id, value) { 285 | if (section_id && value) { 286 | value = value.match(/^(\d+)?\:(\d+)?$/); 287 | if (value && (value[1] || value[2])) { 288 | if (!value[1]) 289 | value[1] = 0; 290 | else if (!value[2]) 291 | value[2] = 65535; 292 | 293 | if (value[1] < value[2] && value[2] <= 65535) 294 | return true; 295 | } 296 | 297 | return _('Expecting: %s').format( _('valid port range (port1:port2)')); 298 | } 299 | 300 | return true; 301 | }, 302 | 303 | validateUniqueValue(uciconfig, ucisection, ucioption, section_id, value) { 304 | if (section_id) { 305 | if (!value) 306 | return _('Expecting: %s').format(_('non-empty value')); 307 | if (ucioption === 'node' && value === 'urltest') 308 | return true; 309 | 310 | let duplicate = false; 311 | uci.sections(uciconfig, ucisection, (res) => { 312 | if (res['.name'] !== section_id) 313 | if (res[ucioption] === value) 314 | duplicate = true 315 | }); 316 | if (duplicate) 317 | return _('Expecting: %s').format(_('unique value')); 318 | } 319 | 320 | return true; 321 | }, 322 | 323 | validateUUID(section_id, value) { 324 | if (section_id) { 325 | if (!value) 326 | return _('Expecting: %s').format(_('non-empty value')); 327 | else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null) 328 | return _('Expecting: %s').format(_('valid uuid')); 329 | } 330 | 331 | return true; 332 | } 333 | }); 334 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/update_subscriptions.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | /* 3 | * SPDX-License-Identifier: GPL-2.0-only 4 | * 5 | * Copyright (C) 2023 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { md5 } from 'digest'; 11 | import { open } from 'fs'; 12 | import { connect } from 'ubus'; 13 | import { cursor } from 'uci'; 14 | 15 | import { urldecode, urlencode } from 'luci.http'; 16 | import { init_action } from 'luci.sys'; 17 | 18 | import { 19 | wGET, decodeBase64Str, getTime, isEmpty, parseURL, 20 | validation, HP_DIR, RUN_DIR 21 | } from 'homeproxy'; 22 | 23 | /* UCI config start */ 24 | const uci = cursor(); 25 | 26 | const uciconfig = 'homeproxy'; 27 | uci.load(uciconfig); 28 | 29 | const ucimain = 'config', 30 | ucinode = 'node', 31 | ucisubscription = 'subscription'; 32 | 33 | const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0', 34 | filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled', 35 | filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [], 36 | packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp', 37 | subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [], 38 | user_agent = uci.get(uciconfig, ucisubscription, 'user_agent'), 39 | via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0'; 40 | 41 | const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china'; 42 | let main_node, main_udp_node; 43 | if (routing_mode !== 'custom') { 44 | main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil'; 45 | main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil'; 46 | } 47 | /* UCI config end */ 48 | 49 | /* String helper start */ 50 | function filter_check(name) { 51 | if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords)) 52 | return false; 53 | 54 | let ret = false; 55 | for (let i in filter_keywords) { 56 | const patten = regexp(i); 57 | if (match(name, patten)) 58 | ret = true; 59 | } 60 | if (filter_mode === 'whitelist') 61 | ret = !ret; 62 | 63 | return ret; 64 | } 65 | /* String helper end */ 66 | 67 | /* Common var start */ 68 | const node_cache = {}, 69 | node_result = []; 70 | 71 | const ubus = connect(); 72 | const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {}; 73 | /* Common var end */ 74 | 75 | /* Log */ 76 | system(`mkdir -p ${RUN_DIR}`); 77 | function log(...args) { 78 | const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a'); 79 | logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`); 80 | logfile.close(); 81 | } 82 | 83 | function parse_uri(uri) { 84 | let config, url, params; 85 | 86 | if (type(uri) === 'object') { 87 | if (uri.nodetype === 'sip008') { 88 | /* https://shadowsocks.org/guide/sip008.html */ 89 | config = { 90 | label: uri.remarks, 91 | type: 'shadowsocks', 92 | address: uri.server, 93 | port: uri.server_port, 94 | shadowsocks_encrypt_method: uri.method, 95 | password: uri.password, 96 | shadowsocks_plugin: uri.plugin, 97 | shadowsocks_plugin_opts: uri.plugin_opts 98 | }; 99 | } 100 | } else if (type(uri) === 'string') { 101 | uri = split(trim(uri), '://'); 102 | 103 | switch (uri[0]) { 104 | case 'anytls': 105 | /* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */ 106 | url = parseURL('http://' + uri[1]) || {}; 107 | params = url.searchParams || {}; 108 | 109 | config = { 110 | label: url.hash ? urldecode(url.hash) : null, 111 | type: 'anytls', 112 | address: url.hostname, 113 | port: url.port, 114 | password: urldecode(url.username), 115 | tls: '1', 116 | tls_sni: params.sni, 117 | tls_insecure: (params.insecure === '1') ? '1' : '0' 118 | }; 119 | 120 | break; 121 | case 'http': 122 | case 'https': 123 | url = parseURL('http://' + uri[1]) || {}; 124 | 125 | config = { 126 | label: url.hash ? urldecode(url.hash) : null, 127 | type: 'http', 128 | address: url.hostname, 129 | port: url.port, 130 | username: url.username ? urldecode(url.username) : null, 131 | password: url.password ? urldecode(url.password) : null, 132 | tls: (uri[0] === 'https') ? '1' : '0' 133 | }; 134 | 135 | break; 136 | case 'hysteria': 137 | /* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */ 138 | url = parseURL('http://' + uri[1]) || {}; 139 | params = url.searchParams; 140 | 141 | if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) { 142 | log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); 143 | if (!sing_features.with_quic) 144 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 145 | 146 | return null; 147 | } 148 | 149 | config = { 150 | label: url.hash ? urldecode(url.hash) : null, 151 | type: 'hysteria', 152 | address: url.hostname, 153 | port: url.port, 154 | hysteria_protocol: params.protocol || 'udp', 155 | hysteria_auth_type: params.auth ? 'string' : null, 156 | hysteria_auth_payload: params.auth, 157 | hysteria_obfs_password: params.obfsParam, 158 | hysteria_down_mbps: params.downmbps, 159 | hysteria_up_mbps: params.upmbps, 160 | tls: '1', 161 | tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0', 162 | tls_sni: params.peer, 163 | tls_alpn: params.alpn 164 | }; 165 | 166 | break; 167 | case 'hysteria2': 168 | case 'hy2': 169 | /* https://v2.hysteria.network/docs/developers/URI-Scheme/ */ 170 | url = parseURL('http://' + uri[1]) || {}; 171 | params = url.searchParams; 172 | 173 | if (!sing_features.with_quic) { 174 | log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); 175 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 176 | return null; 177 | } 178 | 179 | config = { 180 | label: url.hash ? urldecode(url.hash) : null, 181 | type: 'hysteria2', 182 | address: url.hostname, 183 | port: url.port, 184 | password: url.username ? ( 185 | urldecode(url.username + (url.password ? (':' + url.password) : '')) 186 | ) : null, 187 | hysteria_obfs_type: params.obfs, 188 | hysteria_obfs_password: params['obfs-password'], 189 | tls: '1', 190 | tls_insecure: (params.insecure === '1') ? '1' : '0', 191 | tls_sni: params.sni 192 | }; 193 | 194 | break; 195 | case 'socks': 196 | case 'socks4': 197 | case 'socks4a': 198 | case 'socsk5': 199 | case 'socks5h': 200 | url = parseURL('http://' + uri[1]) || {}; 201 | 202 | config = { 203 | label: url.hash ? urldecode(url.hash) : null, 204 | type: 'socks', 205 | address: url.hostname, 206 | port: url.port, 207 | username: url.username ? urldecode(url.username) : null, 208 | password: url.password ? urldecode(url.password) : null, 209 | socks_version: (match(uri[0], /4/)) ? '4' : '5' 210 | }; 211 | 212 | break; 213 | case 'ss': 214 | /* "Lovely" Shadowrocket format */ 215 | const ss_suri = split(uri[1], '#'); 216 | let ss_slabel = ''; 217 | if (length(ss_suri) <= 2) { 218 | if (length(ss_suri) === 2) 219 | ss_slabel = '#' + urlencode(ss_suri[1]); 220 | if (decodeBase64Str(ss_suri[0])) 221 | uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel; 222 | } 223 | 224 | /* Legacy format is not supported, it should be never appeared in modern subscriptions */ 225 | /* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */ 226 | 227 | /* SIP002 format https://shadowsocks.org/guide/sip002.html */ 228 | url = parseURL('http://' + uri[1]) || {}; 229 | 230 | let ss_userinfo = {}; 231 | if (url.username && url.password) 232 | /* User info encoded with URIComponent */ 233 | ss_userinfo = [url.username, urldecode(url.password)]; 234 | else if (url.username) 235 | /* User info encoded with base64 */ 236 | ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':', 2); 237 | 238 | let ss_plugin, ss_plugin_opts; 239 | if (url.search && url.searchParams.plugin) { 240 | const ss_plugin_info = split(url.searchParams.plugin, ';', 2); 241 | ss_plugin = ss_plugin_info[0]; 242 | if (ss_plugin === 'simple-obfs') 243 | /* Fix non-standard plugin name */ 244 | ss_plugin = 'obfs-local'; 245 | ss_plugin_opts = ss_plugin_info[1]; 246 | } 247 | 248 | config = { 249 | label: url.hash ? urldecode(url.hash) : null, 250 | type: 'shadowsocks', 251 | address: url.hostname, 252 | port: url.port, 253 | shadowsocks_encrypt_method: ss_userinfo[0], 254 | password: ss_userinfo[1], 255 | shadowsocks_plugin: ss_plugin, 256 | shadowsocks_plugin_opts: ss_plugin_opts 257 | }; 258 | 259 | break; 260 | case 'trojan': 261 | /* https://p4gefau1t.github.io/trojan-go/developer/url/ */ 262 | url = parseURL('http://' + uri[1]) || {}; 263 | params = url.searchParams || {}; 264 | 265 | config = { 266 | label: url.hash ? urldecode(url.hash) : null, 267 | type: 'trojan', 268 | address: url.hostname, 269 | port: url.port, 270 | password: urldecode(url.username), 271 | transport: (params.type !== 'tcp') ? params.type : null, 272 | tls: '1', 273 | tls_sni: params.sni 274 | }; 275 | switch(params.type) { 276 | case 'grpc': 277 | config.grpc_servicename = params.serviceName; 278 | break; 279 | case 'ws': 280 | config.ws_host = params.host ? urldecode(params.host) : null; 281 | config.ws_path = params.path ? urldecode(params.path) : null; 282 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 283 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 284 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 285 | config.ws_path = split(config.ws_path, '?ed=')[0]; 286 | } 287 | break; 288 | } 289 | 290 | break; 291 | case 'tuic': 292 | /* https://github.com/daeuniverse/dae/discussions/182 */ 293 | url = parseURL('http://' + uri[1]) || {}; 294 | params = url.searchParams || {}; 295 | 296 | if (!sing_features.with_quic) { 297 | log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); 298 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 299 | 300 | return null; 301 | } 302 | 303 | config = { 304 | label: url.hash ? urldecode(url.hash) : null, 305 | type: 'tuic', 306 | address: url.hostname, 307 | port: url.port, 308 | uuid: url.username, 309 | password: url.password ? urldecode(url.password) : null, 310 | tuic_congestion_control: params.congestion_control, 311 | tuic_udp_relay_mode: params.udp_relay_mode, 312 | tls: '1', 313 | tls_sni: params.sni, 314 | tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null, 315 | }; 316 | 317 | break; 318 | case 'vless': 319 | /* https://github.com/XTLS/Xray-core/discussions/716 */ 320 | url = parseURL('http://' + uri[1]) || {}; 321 | params = url.searchParams; 322 | 323 | /* Unsupported protocol */ 324 | if (params.type === 'kcp') { 325 | log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); 326 | return null; 327 | } else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) { 328 | log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); 329 | if (!sing_features.with_quic) 330 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 331 | 332 | return null; 333 | } 334 | 335 | config = { 336 | label: url.hash ? urldecode(url.hash) : null, 337 | type: 'vless', 338 | address: url.hostname, 339 | port: url.port, 340 | uuid: url.username, 341 | transport: (params.type !== 'tcp') ? params.type : null, 342 | tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0', 343 | tls_sni: params.sni, 344 | tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null, 345 | tls_reality: (params.security === 'reality') ? '1' : '0', 346 | tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null, 347 | tls_reality_short_id: params.sid, 348 | tls_utls: sing_features.with_utls ? params.fp : null, 349 | vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null 350 | }; 351 | switch(params.type) { 352 | case 'grpc': 353 | config.grpc_servicename = params.serviceName; 354 | break; 355 | case 'http': 356 | case 'tcp': 357 | if (params.type === 'http' || params.headerType === 'http') { 358 | config.http_host = params.host ? split(urldecode(params.host), ',') : null; 359 | config.http_path = params.path ? urldecode(params.path) : null; 360 | } 361 | break; 362 | case 'httpupgrade': 363 | config.httpupgrade_host = params.host ? urldecode(params.host) : null; 364 | config.http_path = params.path ? urldecode(params.path) : null; 365 | break; 366 | case 'ws': 367 | config.ws_host = params.host ? urldecode(params.host) : null; 368 | config.ws_path = params.path ? urldecode(params.path) : null; 369 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 370 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 371 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 372 | config.ws_path = split(config.ws_path, '?ed=')[0]; 373 | } 374 | break; 375 | } 376 | 377 | break; 378 | case 'vmess': 379 | /* "Lovely" shadowrocket format */ 380 | if (match(uri, /&/)) { 381 | log(sprintf('Skipping unsupported %s format.', uri[0])); 382 | return null; 383 | } 384 | 385 | /* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */ 386 | try { 387 | uri = json(decodeBase64Str(uri[1])) || {}; 388 | } catch(e) { 389 | log(sprintf('Skipping unsupported %s format.', uri[0])); 390 | return null; 391 | } 392 | 393 | if (uri.v != '2') { 394 | log(sprintf('Skipping unsupported %s format.', uri[0])); 395 | return null; 396 | /* Unsupported protocol */ 397 | } else if (uri.net === 'kcp') { 398 | log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add)); 399 | return null; 400 | } else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) { 401 | log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add)); 402 | if (!sing_features.with_quic) 403 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 404 | 405 | return null; 406 | } 407 | /* 408 | * https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6 409 | * else if (uri.aid && int(uri.aid) !== 0) { 410 | * log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add)); 411 | * return null; 412 | * } 413 | */ 414 | 415 | config = { 416 | label: uri.ps ? urldecode(uri.ps) : null, 417 | type: 'vmess', 418 | address: uri.add, 419 | port: uri.port, 420 | uuid: uri.id, 421 | vmess_alterid: uri.aid, 422 | vmess_encrypt: uri.scy || 'auto', 423 | vmess_global_padding: '1', 424 | transport: (uri.net !== 'tcp') ? uri.net : null, 425 | tls: (uri.tls === 'tls') ? '1' : '0', 426 | tls_sni: uri.sni || uri.host, 427 | tls_alpn: uri.alpn ? split(uri.alpn, ',') : null, 428 | tls_utls: sing_features.with_utls ? uri.fp : null 429 | }; 430 | switch (uri.net) { 431 | case 'grpc': 432 | config.grpc_servicename = uri.path; 433 | break; 434 | case 'h2': 435 | case 'tcp': 436 | if (uri.net === 'h2' || uri.type === 'http') { 437 | config.transport = 'http'; 438 | config.http_host = uri.host ? split(uri.host, ',') : null; 439 | config.http_path = uri.path; 440 | } 441 | break; 442 | case 'httpupgrade': 443 | config.httpupgrade_host = uri.host; 444 | config.http_path = uri.path; 445 | break; 446 | case 'ws': 447 | config.ws_host = uri.host; 448 | config.ws_path = uri.path; 449 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 450 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 451 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 452 | config.ws_path = split(config.ws_path, '?ed=')[0]; 453 | } 454 | break; 455 | } 456 | 457 | break; 458 | } 459 | } 460 | 461 | if (!isEmpty(config)) { 462 | if (config.address) 463 | config.address = replace(config.address, /\[|\]/g, ''); 464 | 465 | if (!validation('host', config.address) || !validation('port', config.port)) { 466 | log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL')); 467 | return null; 468 | } else if (!config.label) 469 | config.label = (validation('ip6addr', config.address) ? 470 | `[${config.address}]` : config.address) + ':' + config.port; 471 | } 472 | 473 | return config; 474 | } 475 | 476 | function main() { 477 | if (via_proxy !== '1') { 478 | log('Stopping service...'); 479 | init_action('homeproxy', 'stop'); 480 | } 481 | 482 | for (let url in subscription_urls) { 483 | url = replace(url, /#.*$/, ''); 484 | const groupHash = md5(url); 485 | node_cache[groupHash] = {}; 486 | 487 | const res = wGET(url, user_agent); 488 | if (isEmpty(res)) { 489 | log(sprintf('Failed to fetch resources from %s.', url)); 490 | continue; 491 | } 492 | 493 | let nodes; 494 | try { 495 | nodes = json(res).servers || json(res); 496 | 497 | /* Shadowsocks SIP008 format */ 498 | if (nodes[0].server && nodes[0].method) 499 | map(nodes, (_, i) => nodes[i].nodetype = 'sip008'); 500 | } catch(e) { 501 | nodes = decodeBase64Str(res); 502 | nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : []; 503 | } 504 | 505 | let count = 0; 506 | for (let node in nodes) { 507 | let config; 508 | if (!isEmpty(node)) 509 | config = parse_uri(node); 510 | if (isEmpty(config)) 511 | continue; 512 | 513 | const label = config.label; 514 | config.label = null; 515 | const confHash = md5(sprintf('%J', config)), 516 | nameHash = md5(label); 517 | config.label = label; 518 | 519 | if (filter_check(config.label)) 520 | log(sprintf('Skipping blacklist node: %s.', config.label)); 521 | else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash]) 522 | log(sprintf('Skipping duplicate node: %s.', config.label)); 523 | else { 524 | if (config.tls === '1' && allow_insecure === '1') 525 | config.tls_insecure = '1'; 526 | if (config.type in ['vless', 'vmess']) 527 | config.packet_encoding = packet_encoding; 528 | 529 | config.grouphash = groupHash; 530 | push(node_result, []); 531 | push(node_result[length(node_result)-1], config); 532 | node_cache[groupHash][confHash] = config; 533 | node_cache[groupHash][nameHash] = config; 534 | 535 | count++; 536 | } 537 | } 538 | 539 | if (count == 0) 540 | log(sprintf('No valid node found in %s.', url)); 541 | else 542 | log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url)); 543 | } 544 | 545 | if (isEmpty(node_result)) { 546 | log('Failed to update subscriptions: no valid node found.'); 547 | 548 | if (via_proxy !== '1') { 549 | log('Starting service...'); 550 | init_action('homeproxy', 'start'); 551 | } 552 | 553 | return false; 554 | } 555 | 556 | let added = 0, removed = 0; 557 | uci.foreach(uciconfig, ucinode, (cfg) => { 558 | /* Nodes created by the user */ 559 | if (!cfg.grouphash) 560 | return null; 561 | 562 | /* Empty object - failed to fetch nodes */ 563 | if (length(node_cache[cfg.grouphash]) === 0) 564 | return null; 565 | 566 | if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) { 567 | uci.delete(uciconfig, cfg['.name']); 568 | removed++; 569 | 570 | log(sprintf('Removing node: %s.', cfg.label || cfg['name'])); 571 | } else { 572 | map(keys(cfg), (v) => { 573 | if (v in node_cache[cfg.grouphash][cfg['.name']]) 574 | uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]); 575 | else 576 | uci.delete(uciconfig, cfg['.name'], v); 577 | }); 578 | node_cache[cfg.grouphash][cfg['.name']].isExisting = true; 579 | } 580 | }); 581 | for (let nodes in node_result) 582 | map(nodes, (node) => { 583 | if (node.isExisting) 584 | return null; 585 | 586 | const nameHash = md5(node.label); 587 | uci.set(uciconfig, nameHash, 'node'); 588 | map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v])); 589 | 590 | added++; 591 | log(sprintf('Adding node: %s.', node.label)); 592 | }); 593 | uci.commit(uciconfig); 594 | 595 | let need_restart = (via_proxy !== '1'); 596 | if (!isEmpty(main_node)) { 597 | const first_server = uci.get_first(uciconfig, ucinode); 598 | if (first_server) { 599 | let main_urltest_nodes; 600 | if (main_node === 'urltest') { 601 | main_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_urltest_nodes'), (v) => { 602 | if (!uci.get(uciconfig, v)) { 603 | log(sprintf('Node %s is gone, removing from urltest list.', v)); 604 | return false; 605 | } 606 | return true; 607 | }); 608 | } 609 | 610 | if ((main_node === 'urltest') ? !length(main_urltest_nodes) : !uci.get(uciconfig, main_node)) { 611 | uci.set(uciconfig, ucimain, 'main_node', first_server); 612 | uci.commit(uciconfig); 613 | need_restart = true; 614 | 615 | log('Main node is gone, switching to the first node.'); 616 | } 617 | 618 | if (!isEmpty(main_udp_node) && main_udp_node !== 'same') { 619 | let main_udp_urltest_nodes; 620 | if (main_udp_node === 'urltest') { 621 | main_udp_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'), (v) => { 622 | if (!uci.get(uciconfig, v)) { 623 | log(sprintf('Node %s is gone, removing from urltest list.', v)); 624 | return false; 625 | } 626 | return true; 627 | }); 628 | } 629 | 630 | if ((main_udp_node === 'urltest') ? !length(main_udp_urltest_nodes) : !uci.get(uciconfig, main_udp_node)) { 631 | uci.set(uciconfig, ucimain, 'main_udp_node', first_server); 632 | uci.commit(uciconfig); 633 | need_restart = true; 634 | 635 | log('Main UDP node is gone, switching to the first node.'); 636 | } 637 | } 638 | } else { 639 | uci.set(uciconfig, ucimain, 'main_node', 'nil'); 640 | uci.set(uciconfig, ucimain, 'main_udp_node', 'nil'); 641 | uci.commit(uciconfig); 642 | need_restart = true; 643 | 644 | log('No available node, disable tproxy.'); 645 | } 646 | } 647 | 648 | if (need_restart) { 649 | log('Restarting service...'); 650 | init_action('homeproxy', 'stop'); 651 | init_action('homeproxy', 'start'); 652 | } 653 | 654 | log(sprintf('%s nodes added, %s removed.', added, removed)); 655 | log('Successfully updated subscriptions.'); 656 | } 657 | 658 | if (!isEmpty(subscription_urls)) 659 | try { 660 | call(main); 661 | } catch(e) { 662 | log('[FATAL ERROR] An error occurred during updating subscriptions:'); 663 | log(sprintf('%s: %s', e.type, e.message)); 664 | log(e.stacktrace[0].context); 665 | 666 | log('Restarting service...'); 667 | init_action('homeproxy', 'stop'); 668 | init_action('homeproxy', 'start'); 669 | } 670 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/firewall_post.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl -S 2 | 3 | {%- 4 | import { readfile } from 'fs'; 5 | import { cursor } from 'uci'; 6 | import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc'; 7 | 8 | const fw4 = require('fw4'); 9 | 10 | function array_to_nftarr(array) { 11 | if (type(array) !== 'array') 12 | return null; 13 | 14 | return `{ ${join(', ', uniq(array))} }`; 15 | } 16 | 17 | function resolve_ipv6(str) { 18 | if (isEmpty(str)) 19 | return null; 20 | 21 | let ipv6 = fw4.parse_subnet(str)?.[0]; 22 | if (!ipv6 || ipv6.family !== 6) 23 | return null; 24 | 25 | if (ipv6.bits > -1) 26 | return `${ipv6.addr}/${ipv6.bits}`; 27 | else 28 | return `& ${ipv6.mask} == ${ipv6.addr}`; 29 | } 30 | 31 | function resolve_mark(str) { 32 | if (isEmpty(str)) 33 | return null; 34 | 35 | let mark = fw4.parse_mark(str); 36 | if (isEmpty(mark)) 37 | return null; 38 | 39 | if (mark.mask === 0xffffffff) 40 | return fw4.hex(mark.mark); 41 | else if (mark.mark === 0) 42 | return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`; 43 | else if (mark.mark === mark.mask) 44 | return `mark or ${fw4.hex(mark.mark)}`; 45 | else if (mark.mask === 0) 46 | return `mark xor ${fw4.hex(mark.mark)}`; 47 | else 48 | return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`; 49 | } 50 | 51 | /* Misc config */ 52 | const resources_dir = '/etc/homeproxy/resources'; 53 | 54 | /* UCI config start */ 55 | const cfgname = 'homeproxy'; 56 | const uci = cursor(); 57 | uci.load(cfgname); 58 | 59 | const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china'; 60 | let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic; 61 | 62 | if (routing_mode !== 'custom') { 63 | outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil'; 64 | outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil'; 65 | china_dns_server = uci.get(cfgname, 'config', 'china_dns_server'); 66 | } else { 67 | outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil'; 68 | bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0'; 69 | } 70 | 71 | let routing_port = uci.get(cfgname, 'config', 'routing_port'); 72 | if (routing_port === 'common') 73 | routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418'; 74 | 75 | const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy', 76 | ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0'; 77 | 78 | let self_mark, redirect_port, 79 | tproxy_port, tproxy_mark, 80 | tun_name, tun_mark; 81 | 82 | if (match(proxy_mode, /redirect/)) { 83 | self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100'; 84 | redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331'; 85 | } 86 | if (match(proxy_mode, /tproxy/)) 87 | if (outbound_udp_node !== 'nil' || routing_mode === 'custom') { 88 | tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332'; 89 | tproxy_mark = resolve_mark(uci.get(cfgname, 'infra', 'tproxy_mark') || '101'); 90 | } 91 | if (match(proxy_mode, /tun/)) { 92 | tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0'; 93 | tun_mark = resolve_mark(uci.get(cfgname, 'infra', 'tun_mark') || '102'); 94 | } 95 | 96 | const control_options = [ 97 | "listen_interfaces", "lan_proxy_mode", 98 | "lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips", 99 | "lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips", 100 | "lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips", 101 | "lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips", 102 | "wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips", 103 | "wan_direct_ipv4_ips", "wan_direct_ipv6_ips" 104 | ]; 105 | const control_info = {}; 106 | 107 | for (let i in control_options) 108 | control_info[i] = uci.get(cfgname, 'control', i); 109 | 110 | const dns_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0', 111 | dns_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53'; 112 | /* UCI config end */ 113 | -%} 114 | 115 | {# Reserved addresses -#} 116 | set homeproxy_local_addr_v4 { 117 | type ipv4_addr 118 | flags interval 119 | auto-merge 120 | elements = { 121 | 0.0.0.0/8, 122 | 10.0.0.0/8, 123 | 100.64.0.0/10, 124 | 127.0.0.0/8, 125 | 169.254.0.0/16, 126 | 172.16.0.0/12, 127 | 192.0.0.0/24, 128 | 192.0.2.0/24, 129 | 192.31.196.0/24, 130 | 192.52.193.0/24, 131 | 192.88.99.0/24, 132 | 192.168.0.0/16, 133 | 192.175.48.0/24, 134 | 198.18.0.0/15, 135 | 198.51.100.0/24, 136 | 203.0.113.0/24, 137 | 224.0.0.0/4, 138 | 240.0.0.0/4 139 | } 140 | } 141 | {% if (ipv6_support === '1'): %} 142 | set homeproxy_local_addr_v6 { 143 | type ipv6_addr 144 | flags interval 145 | auto-merge 146 | elements = { 147 | ::/128, 148 | ::1/128, 149 | ::ffff:0:0/96, 150 | 100::/64, 151 | 64:ff9b::/96, 152 | 2001::/32, 153 | 2001:10::/28, 154 | 2001:20::/28, 155 | 2001:db8::/28, 156 | 2002::/16, 157 | fc00::/7, 158 | fe80::/10, 159 | ff00::/8 160 | } 161 | } 162 | {% endif %} 163 | 164 | {% if (routing_mode === 'gfwlist'): %} 165 | set homeproxy_gfw_list_v4 { 166 | type ipv4_addr 167 | flags interval 168 | auto-merge 169 | } 170 | {% if (ipv6_support === '1'): %} 171 | set homeproxy_gfw_list_v6 { 172 | type ipv6_addr 173 | flags interval 174 | auto-merge 175 | } 176 | {% endif /* ipv6_support */ %} 177 | {% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %} 178 | set homeproxy_mainland_addr_v4 { 179 | type ipv4_addr 180 | flags interval 181 | auto-merge 182 | elements = { 183 | {% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %} 184 | {{ cnip4 }}, 185 | {% endfor %} 186 | } 187 | } 188 | {% if ((ipv6_support === '1') || china_dns_server): %} 189 | set homeproxy_mainland_addr_v6 { 190 | type ipv6_addr 191 | flags interval 192 | auto-merge 193 | elements = { 194 | {% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %} 195 | {{ cnip6 }}, 196 | {% endfor %} 197 | } 198 | } 199 | {% endif /* ipv6_support */ %} 200 | {% endif /* routing_mode */ %} 201 | 202 | {# WAN ACL addresses #} 203 | set homeproxy_wan_proxy_addr_v4 { 204 | type ipv4_addr 205 | flags interval 206 | auto-merge 207 | {% if (control_info.wan_proxy_ipv4_ips): %} 208 | elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} } 209 | {% endif %} 210 | } 211 | 212 | {% if (ipv6_support === '1'): %} 213 | set homeproxy_wan_proxy_addr_v6 { 214 | type ipv6_addr 215 | flags interval 216 | auto-merge 217 | {% if (control_info.wan_proxy_ipv6_ips): %} 218 | elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} } 219 | {% endif /* wan_proxy_ipv6_ips*/ %} 220 | } 221 | {% endif /* ipv6_support */ %} 222 | 223 | set homeproxy_wan_direct_addr_v4 { 224 | type ipv4_addr 225 | flags interval 226 | auto-merge 227 | {% if (control_info.wan_direct_ipv4_ips): %} 228 | elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} } 229 | {% endif %} 230 | } 231 | 232 | {% if (ipv6_support === '1'): %} 233 | set homeproxy_wan_direct_addr_v6 { 234 | type ipv6_addr 235 | flags interval 236 | auto-merge 237 | {% if (control_info.wan_direct_ipv6_ips): %} 238 | elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} } 239 | {% endif /* wan_direct_ipv6_ips */ %} 240 | } 241 | {% endif /* ipv6_support */ %} 242 | 243 | {% if (routing_port): %} 244 | set homeproxy_routing_port { 245 | type inet_service 246 | flags interval 247 | auto-merge 248 | elements = { {{ join(', ', split(routing_port, ',')) }} } 249 | } 250 | {% endif %} 251 | 252 | {# DNS hijack & TCP redirect #} 253 | chain dstnat { 254 | {% if (dns_hijacked !== '1'): %} 255 | {% if (control_info.listen_interfaces): %} 256 | meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }} 257 | {%- endif /* listen_interfaces */ %} 258 | meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dns_port }} comment "!{{ cfgname }}: DNS hijack" 259 | {% endif /* dns_hijacked */ %} 260 | {% if (match(proxy_mode, /redirect/)): %} 261 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac 262 | {% endif /* proxy_mode */ %} 263 | } 264 | 265 | {# TCP redirect #} 266 | {% if (match(proxy_mode, /redirect/)): %} 267 | chain homeproxy_redirect_proxy { 268 | meta l4proto tcp counter redirect to :{{ redirect_port }} 269 | } 270 | 271 | chain homeproxy_redirect_proxy_port { 272 | {% if (routing_port): %} 273 | tcp dport != @homeproxy_routing_port counter return 274 | {% endif %} 275 | goto homeproxy_redirect_proxy 276 | } 277 | 278 | chain homeproxy_redirect_lanac { 279 | {% if (control_info.listen_interfaces): %} 280 | meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return 281 | {% endif %} 282 | meta mark {{ self_mark }} counter return 283 | 284 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 285 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 286 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect 287 | {% endif /* lan_proxy_ipv4_ips */ %} 288 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 289 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect 290 | {% endfor /* lan_proxy_ipv6_ips */ %} 291 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 292 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect 293 | {% endif /* lan_proxy_mac_addrs */ %} 294 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 295 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 296 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 297 | {% endif /* lan_direct_ipv4_ips */ %} 298 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 299 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 300 | {% endfor /* lan_direct_ipv6_ips */ %} 301 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 302 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 303 | {% endif /* lan_direct_mac_addrs */ %} 304 | {% endif /* lan_proxy_mode */ %} 305 | 306 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 307 | counter goto homeproxy_redirect 308 | {% endif %} 309 | } 310 | 311 | chain homeproxy_redirect { 312 | meta mark {{ self_mark }} counter return 313 | 314 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port 315 | {% if (ipv6_support === '1'): %} 316 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port 317 | {% endif %} 318 | 319 | ip daddr @homeproxy_local_addr_v4 counter return 320 | {% if (ipv6_support === '1'): %} 321 | ip6 daddr @homeproxy_local_addr_v6 counter return 322 | {% endif %} 323 | 324 | {% if (routing_mode !== 'custom'): %} 325 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 326 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port 327 | {% endif /* lan_global_proxy_ipv4_ips */ %} 328 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 329 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port 330 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 331 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 332 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port 333 | {% endif /* lan_global_proxy_mac_addrs */ %} 334 | {% endif /* routing_mode */ %} 335 | 336 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 337 | {% if (ipv6_support === '1'): %} 338 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 339 | {% endif /* ipv6_support */ %} 340 | 341 | {% if (routing_mode === 'gfwlist'): %} 342 | ip daddr != @homeproxy_gfw_list_v4 counter return 343 | {% if (ipv6_support === '1'): %} 344 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 345 | {% endif /* ipv6_support */ %} 346 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 347 | ip daddr @homeproxy_mainland_addr_v4 counter return 348 | {% if (ipv6_support === '1'): %} 349 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 350 | {% endif /* ipv6_support */ %} 351 | {% elif (routing_mode === 'proxy_mainland_china'): %} 352 | ip daddr != @homeproxy_mainland_addr_v4 counter return 353 | {% if (ipv6_support === '1'): %} 354 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 355 | {% endif /* ipv6_support */ %} 356 | {% endif /* routing_mode */ %} 357 | 358 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 359 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy 360 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 361 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 362 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy 363 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 364 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 365 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy 366 | {% endif /* lan_gaming_mode_mac_addrs */ %} 367 | 368 | counter goto homeproxy_redirect_proxy_port 369 | } 370 | 371 | chain homeproxy_output_redir { 372 | type nat hook output priority filter -105; policy accept 373 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect 374 | } 375 | {% endif %} 376 | 377 | {# UDP tproxy #} 378 | {% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %} 379 | chain homeproxy_mangle_tproxy { 380 | meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept 381 | {% if (ipv6_support === '1'): %} 382 | meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept 383 | {% endif %} 384 | } 385 | 386 | chain homeproxy_mangle_tproxy_port { 387 | {% if (routing_port): %} 388 | udp dport != @homeproxy_routing_port counter return 389 | {% endif %} 390 | goto homeproxy_mangle_tproxy 391 | } 392 | 393 | chain homeproxy_mangle_mark { 394 | {% if (routing_port): %} 395 | udp dport != @homeproxy_routing_port counter return 396 | {% endif %} 397 | meta l4proto udp meta mark set {{ tproxy_mark }} counter accept 398 | } 399 | 400 | chain homeproxy_mangle_lanac { 401 | {% if (control_info.listen_interfaces): %} 402 | meta iifname != {{ array_to_nftarr(uniq([...control_info.listen_interfaces, ...['lo']])) }} counter return 403 | {% endif %} 404 | meta iifname != lo udp dport 53 counter return 405 | meta mark {{ self_mark }} counter return 406 | 407 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 408 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 409 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting 410 | {% endif /* lan_proxy_ipv4_ips */ %} 411 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 412 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting 413 | {% endfor /* lan_proxy_ipv6_ips */ %} 414 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 415 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting 416 | {% endif /* lan_proxy_mac_addrs */ %} 417 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 418 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 419 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 420 | {% endif /* lan_direct_ipv4_ips */ %} 421 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 422 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 423 | {% endfor /* lan_direct_ipv6_ips */ %} 424 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 425 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 426 | {% endif /* lan_direct_mac_addrs */ %} 427 | {% endif /* lan_proxy_mode */ %} 428 | 429 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 430 | counter goto homeproxy_mangle_prerouting 431 | {% endif %} 432 | } 433 | 434 | chain homeproxy_mangle_prerouting { 435 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port 436 | {% if (ipv6_support === '1'): %} 437 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port 438 | {% endif %} 439 | 440 | ip daddr @homeproxy_local_addr_v4 counter return 441 | {% if (ipv6_support === '1'): %} 442 | ip6 daddr @homeproxy_local_addr_v6 counter return 443 | {% endif %} 444 | 445 | {% if (routing_mode !== 'custom'): %} 446 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 447 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port 448 | {% endif /* lan_global_proxy_ipv4_ips */ %} 449 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 450 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port 451 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 452 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 453 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port 454 | {% endif /* lan_global_proxy_mac_addrs */ %} 455 | {% endif /* routing_mode */ %} 456 | 457 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 458 | {% if (ipv6_support === '1'): %} 459 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 460 | {% endif /* ipv6_support */ %} 461 | 462 | {% if (routing_mode === 'gfwlist'): %} 463 | ip daddr != @homeproxy_gfw_list_v4 counter return 464 | {% if (ipv6_support === '1'): %} 465 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 466 | {% endif /* ipv6_support */ %} 467 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 468 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 469 | ip daddr @homeproxy_mainland_addr_v4 counter return 470 | {% if (ipv6_support === '1'): %} 471 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 472 | {% endif /* ipv6_support */ %} 473 | {% if (routing_mode !== 'custom'): %} 474 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 475 | {% endif /* routing_mode */ %} 476 | {% elif (routing_mode === 'proxy_mainland_china'): %} 477 | ip daddr != @homeproxy_mainland_addr_v4 counter return 478 | {% if (ipv6_support === '1'): %} 479 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 480 | {% endif /* ipv6_support */ %} 481 | {% endif /* routing_mode */ %} 482 | 483 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 484 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy 485 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 486 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 487 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy 488 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 489 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 490 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy 491 | {% endif /* lan_gaming_mode_mac_addrs */ %} 492 | 493 | counter goto homeproxy_mangle_tproxy_port 494 | } 495 | 496 | chain homeproxy_mangle_output { 497 | meta mark {{ self_mark }} counter return 498 | 499 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark 500 | {% if (ipv6_support === '1'): %} 501 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark 502 | {% endif %} 503 | 504 | ip daddr @homeproxy_local_addr_v4 counter return 505 | {% if (ipv6_support === '1'): %} 506 | ip6 daddr @homeproxy_local_addr_v6 counter return 507 | {% endif %} 508 | 509 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 510 | {% if (ipv6_support === '1'): %} 511 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 512 | {% endif /* ipv6_support */ %} 513 | 514 | {% if (routing_mode === 'gfwlist'): %} 515 | ip daddr != @homeproxy_gfw_list_v4 counter return 516 | {% if (ipv6_support === '1'): %} 517 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 518 | {% endif /* ipv6_support */ %} 519 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 520 | ip daddr @homeproxy_mainland_addr_v4 counter return 521 | {% if (ipv6_support === '1'): %} 522 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 523 | {% endif /* ipv6_support */ %} 524 | {% elif (routing_mode === 'proxy_mainland_china'): %} 525 | ip daddr != @homeproxy_mainland_addr_v4 counter return 526 | {% if (ipv6_support === '1'): %} 527 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 528 | {% endif /* ipv6_support */ %} 529 | {% endif /* routing_mode */ %} 530 | 531 | counter goto homeproxy_mangle_mark 532 | } 533 | 534 | chain mangle_prerouting { 535 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac 536 | } 537 | 538 | chain mangle_output { 539 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output 540 | } 541 | {% endif %} 542 | 543 | {# TUN #} 544 | {% if (match(proxy_mode, /tun/)): %} 545 | chain homeproxy_mangle_lanac { 546 | iifname {{ tun_name }} counter return 547 | udp dport 53 counter return 548 | 549 | {% if (control_info.listen_interfaces): %} 550 | meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return 551 | {% endif %} 552 | 553 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 554 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 555 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun 556 | {% endif /* lan_proxy_ipv4_ips */ %} 557 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 558 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun 559 | {% endfor /* lan_proxy_ipv6_ips */ %} 560 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 561 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun 562 | {% endif /* lan_proxy_mac_addrs */ %} 563 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 564 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 565 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 566 | {% endif /* lan_direct_ipv4_ips */ %} 567 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 568 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 569 | {% endfor /* lan_direct_ipv6_ips */ %} 570 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 571 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 572 | {% endif /* lan_direct_mac_addrs */ %} 573 | {% endif /* lan_proxy_mode */ %} 574 | 575 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 576 | counter goto homeproxy_mangle_tun 577 | {% endif %} 578 | } 579 | 580 | chain homeproxy_mangle_tun_mark { 581 | {% if (routing_port): %} 582 | {% if (proxy_mode === 'tun'): %} 583 | tcp dport != @homeproxy_routing_port counter return 584 | {% endif /* proxy_mode */ %} 585 | udp dport != @homeproxy_routing_port counter return 586 | {% endif /* routing_port */ %} 587 | 588 | counter meta mark set {{ tun_mark }} 589 | } 590 | 591 | chain homeproxy_mangle_tun { 592 | iifname {{ tun_name }} counter return 593 | 594 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark 595 | {% if (ipv6_support === '1'): %} 596 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark 597 | {% endif %} 598 | 599 | ip daddr @homeproxy_local_addr_v4 counter return 600 | {% if (ipv6_support === '1'): %} 601 | ip6 daddr @homeproxy_local_addr_v6 counter return 602 | {% endif %} 603 | 604 | {% if (routing_mode !== 'custom'): %} 605 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 606 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark 607 | {% endif /* lan_global_proxy_ipv4_ips */ %} 608 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 609 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark 610 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 611 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 612 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark 613 | {% endif /* lan_global_proxy_mac_addrs */ %} 614 | {% endif /* routing_mode */ %} 615 | 616 | {% if (control_info.wan_direct_ipv4_ips): %} 617 | ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return 618 | {% endif /* wan_direct_ipv4_ips */ %} 619 | {% if (control_info.wan_direct_ipv6_ips): %} 620 | ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return 621 | {% endif /* wan_direct_ipv6_ips */ %} 622 | 623 | {% if (routing_mode === 'gfwlist'): %} 624 | ip daddr != @homeproxy_gfw_list_v4 counter return 625 | {% if (ipv6_support === '1'): %} 626 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 627 | {% endif /* ipv6_support */ %} 628 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 629 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 630 | ip daddr @homeproxy_mainland_addr_v4 counter return 631 | {% if (ipv6_support === '1'): %} 632 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 633 | {% endif /* ipv6_support */ %} 634 | {% if (routing_mode !== 'custom'): %} 635 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 636 | {% endif /* routing_mode */ %} 637 | {% elif (routing_mode === 'proxy_mainland_china'): %} 638 | ip daddr != @homeproxy_mainland_addr_v4 counter return 639 | {% if (ipv6_support === '1'): %} 640 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 641 | {% endif /* ipv6_support */ %} 642 | {% endif /* routing_mode */ %} 643 | 644 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 645 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter meta mark set {{ tun_mark }} 646 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 647 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 648 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter meta mark set {{ tun_mark }} 649 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 650 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 651 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter meta mark set {{ tun_mark }} 652 | {% endif /* lan_gaming_mode_mac_addrs */ %} 653 | 654 | counter goto homeproxy_mangle_tun_mark 655 | } 656 | 657 | chain mangle_prerouting { 658 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac 659 | } 660 | 661 | chain mangle_output { 662 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun 663 | } 664 | {% endif %} 665 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip6.txt: -------------------------------------------------------------------------------- 1 | 2001:250::/30 2 | 2001:254::/31 3 | 2001:256:100::/48 4 | 2001:678:120::/48 5 | 2001:678:10d0::/48 6 | 2001:67c:ebc::/48 7 | 2001:7fa:5::/48 8 | 2001:c68::/32 9 | 2001:cc0::/32 10 | 2001:da8::/32 11 | 2001:daa:1::/48 12 | 2001:daa:2::/47 13 | 2001:daa:4::/47 14 | 2001:daa:6::/48 15 | 2001:dc7::/32 16 | 2001:dd8:1::/48 17 | 2001:dd9::/48 18 | 2001:df0:ac40::/48 19 | 2001:df3:3a80::/48 20 | 2001:df3:8b80::/48 21 | 2001:df5:4740::/48 22 | 2001:df7:1480::/48 23 | 2001:4510:400::/40 24 | 2400:1160::/32 25 | 2400:3200::/32 26 | 2400:5280:f803::/48 27 | 2400:54a0:21c0::/48 28 | 2400:54a0:21c3::/48 29 | 2400:5a60:2::/48 30 | 2400:5f60::/32 31 | 2400:6000::/32 32 | 2400:6460::/39 33 | 2400:6460:300::/40 34 | 2400:6460:500::/40 35 | 2400:6600::/32 36 | 2400:6e60:1301::/48 37 | 2400:75aa::/32 38 | 2400:7bc0:20::/43 39 | 2400:7fc0::/40 40 | 2400:7fc0:220::/44 41 | 2400:7fc0:240::/44 42 | 2400:7fc0:2a0::/44 43 | 2400:7fc0:2c0::/44 44 | 2400:7fc0:4000::/40 45 | 2400:7fc0:4100::/48 46 | 2400:7fc0:6000::/40 47 | 2400:7fc0:8000::/36 48 | 2400:7fc0:a000::/36 49 | 2400:7fc0:bb00::/40 50 | 2400:7fc0:c000::/36 51 | 2400:8200::/32 52 | 2400:87c0::/32 53 | 2400:89c0:1010::/44 54 | 2400:89c0:1020::/44 55 | 2400:89c0:1050::/46 56 | 2400:89c0:1130::/44 57 | 2400:89c0:1150::/48 58 | 2400:89c0:2100::/48 59 | 2400:89c0:2200::/48 60 | 2400:89c0:3010::/44 61 | 2400:89c0:6000::/48 62 | 2400:89c0:6100::/48 63 | 2400:8fc0:571::/48 64 | 2400:9020:f010::/46 65 | 2400:9340::/32 66 | 2400:9380:8001::/48 67 | 2400:9380:8003::/48 68 | 2400:9380:8021::/48 69 | 2400:9380:8040::/48 70 | 2400:9380:8140::/48 71 | 2400:9380:8201::/48 72 | 2400:9380:8301::/48 73 | 2400:9380:9001::/48 74 | 2400:9380:9002::/48 75 | 2400:9380:9005::/48 76 | 2400:9380:9009::/48 77 | 2400:9380:900a::/48 78 | 2400:9380:9020::/47 79 | 2400:9380:9040::/48 80 | 2400:9380:9050::/47 81 | 2400:9380:9060::/48 82 | 2400:9380:9071::/48 83 | 2400:9380:9080::/47 84 | 2400:9380:90b1::/48 85 | 2400:9380:90b2::/47 86 | 2400:9380:90b4::/46 87 | 2400:9380:9100::/47 88 | 2400:9380:9121::/48 89 | 2400:9380:9201::/48 90 | 2400:9380:9202::/48 91 | 2400:9380:9220::/47 92 | 2400:9380:9240::/48 93 | 2400:9380:9250::/47 94 | 2400:9380:9260::/48 95 | 2400:9380:9271::/48 96 | 2400:9380:9280::/47 97 | 2400:9380:92b1::/48 98 | 2400:9380:92b2::/47 99 | 2400:9380:92b4::/46 100 | 2400:95e0::/48 101 | 2400:9600:8800::/48 102 | 2400:9620::/32 103 | 2400:a860:1::/48 104 | 2400:a860:2::/47 105 | 2400:a860:4::/47 106 | 2400:a860:6::/48 107 | 2400:a980::/29 108 | 2400:ae00:1981::/48 109 | 2400:b200::/32 110 | 2400:b600::/32 111 | 2400:b620::/48 112 | 2400:b700::/48 113 | 2400:be00::/48 114 | 2400:cb80:e2e::/47 115 | 2400:cb80:e30::/44 116 | 2400:cb80:e40::/44 117 | 2400:da00::/32 118 | 2400:dd00::/28 119 | 2400:ee00:ffec::/46 120 | 2400:ee00:fff0::/44 121 | 2400:f6e0::/32 122 | 2400:fe00::/32 123 | 2401:20::/40 124 | 2401:800::/32 125 | 2401:1160::/32 126 | 2401:11a0:10::/44 127 | 2401:11a0:d150::/48 128 | 2401:11a0:d152::/48 129 | 2401:11a0:d158::/48 130 | 2401:1320::/32 131 | 2401:1d40::/32 132 | 2401:2e00::/32 133 | 2401:3480::/36 134 | 2401:3480:2000::/48 135 | 2401:3480:3000::/36 136 | 2401:34a0::/31 137 | 2401:3800::/32 138 | 2401:5c20:10::/48 139 | 2401:70e0::/32 140 | 2401:71c0::/48 141 | 2401:7700::/32 142 | 2401:7e00::/32 143 | 2401:8be0::/48 144 | 2401:8d00::/46 145 | 2401:8d00:4::/48 146 | 2401:8d00:7::/48 147 | 2401:8d00:8::/47 148 | 2401:8d00:b::/48 149 | 2401:8d00:c::/48 150 | 2401:8d00:f::/48 151 | 2401:8d00:14::/48 152 | 2401:9a00::/44 153 | 2401:9a00:10::/46 154 | 2401:a140:1::/48 155 | 2401:b180::/32 156 | 2401:b400::/45 157 | 2401:b400:8::/47 158 | 2401:b400:11::/48 159 | 2401:b400:14::/48 160 | 2401:b400:16::/47 161 | 2401:b400:20::/47 162 | 2401:b680::/32 163 | 2401:be00::/32 164 | 2401:c020:6::/48 165 | 2401:c020:8::/47 166 | 2401:c020:14::/48 167 | 2401:cb80::/32 168 | 2401:cc00::/32 169 | 2401:ce00::/32 170 | 2401:d0e0:2::/47 171 | 2401:d180::/46 172 | 2401:d180:10::/47 173 | 2401:d180:111::/48 174 | 2401:d180:2120::/48 175 | 2401:d920::/48 176 | 2401:de00::/32 177 | 2401:ec00::/32 178 | 2401:f860:86::/47 179 | 2401:f860:88::/47 180 | 2401:f860:90::/46 181 | 2401:f860:94::/47 182 | 2401:f860:97::/48 183 | 2401:f860:100::/40 184 | 2401:fa00:40::/43 185 | 2402:840:d000::/46 186 | 2402:840:e000::/46 187 | 2402:840:f000::/38 188 | 2402:c60::/48 189 | 2402:1440::/32 190 | 2402:1460::/32 191 | 2402:2000::/32 192 | 2402:20e0:f000::/48 193 | 2402:3180::/46 194 | 2402:3180:8000::/33 195 | 2402:3c00::/32 196 | 2402:3f80:1400::/40 197 | 2402:4b80::/32 198 | 2402:4e00::/32 199 | 2402:5ec0::/32 200 | 2402:6e80::/32 201 | 2402:6f40::/48 202 | 2402:6f40:2::/48 203 | 2402:7d80::/48 204 | 2402:7d80:240::/47 205 | 2402:7d80:8888::/48 206 | 2402:8bc0::/32 207 | 2402:8cc0::/40 208 | 2402:8cc0:200::/40 209 | 2402:92c0::/48 210 | 2402:93c0::/48 211 | 2402:93c0:20::/48 212 | 2402:93c0:100::/48 213 | 2402:9a80::/32 214 | 2402:a200::/32 215 | 2402:b8c0:6::/48 216 | 2402:b8c0:86::/48 217 | 2402:b8c0:106::/48 218 | 2402:b8c0:186::/48 219 | 2402:d340::/32 220 | 2402:db40:5100::/48 221 | 2402:db40:5900::/48 222 | 2402:db40:5f00::/46 223 | 2402:dfc0::/44 224 | 2402:dfc0:50::/44 225 | 2402:e380:100::/40 226 | 2402:e480::/32 227 | 2402:e740::/32 228 | 2402:e7c0::/32 229 | 2402:ef40::/32 230 | 2402:f000::/32 231 | 2402:f140:ff10::/46 232 | 2402:f140:ff14::/48 233 | 2402:f8c0::/47 234 | 2402:f8c0:2::/48 235 | 2402:f8c0:5::/48 236 | 2403:600::/32 237 | 2403:c80::/32 238 | 2403:1b80::/48 239 | 2403:1ec0:1200::/48 240 | 2403:1ec0:1400::/48 241 | 2403:1ec0:1600::/48 242 | 2403:1ec0:1610::/48 243 | 2403:1ec0:1900::/48 244 | 2403:2040::/48 245 | 2403:2b40::/32 246 | 2403:3140::/32 247 | 2403:4240::/32 248 | 2403:4280::/47 249 | 2403:4300::/32 250 | 2403:4c80::/48 251 | 2403:5c80::/48 252 | 2403:6380:14::/47 253 | 2403:6380:40::/47 254 | 2403:6380:42::/48 255 | 2403:6380:60::/44 256 | 2403:6a00::/32 257 | 2403:7580::/32 258 | 2403:8080:101::/48 259 | 2403:8c00::/32 260 | 2403:9b00::/32 261 | 2403:a100::/48 262 | 2403:a140:10::/48 263 | 2403:a140:100::/40 264 | 2403:a200::/32 265 | 2403:ac00::/32 266 | 2403:ad80:101c::/48 267 | 2403:ad80:8008::/48 268 | 2403:ad80:8047::/48 269 | 2403:b400::/32 270 | 2403:c980::/32 271 | 2403:d400::/32 272 | 2403:f4c0::/48 273 | 2403:ffc0:1100::/40 274 | 2403:ffc0:1200::/39 275 | 2404:bc0:1::/48 276 | 2404:bc0:4000::/43 277 | 2404:bc0:4100::/43 278 | 2404:bc0:4200::/43 279 | 2404:bc0:4300::/44 280 | 2404:bc0:4400::/43 281 | 2404:bc0:4500::/43 282 | 2404:bc0:4f00::/43 283 | 2404:1c80::/32 284 | 2404:3700::/48 285 | 2404:4dc0::/32 286 | 2404:6380::/48 287 | 2404:6380:1000::/48 288 | 2404:6380:8001::/48 289 | 2404:6500:dcb3::/48 290 | 2404:7180:a000::/48 291 | 2404:7180:a010::/48 292 | 2404:7180:aa00::/48 293 | 2404:7180:aa10::/48 294 | 2404:7180:b001::/48 295 | 2404:7180:b002::/48 296 | 2404:7180:b010::/48 297 | 2404:7180:c001::/48 298 | 2404:7180:c002::/48 299 | 2404:7180:c011::/48 300 | 2404:7180:c012::/48 301 | 2404:7180:f000::/48 302 | 2404:7180:f010::/48 303 | 2404:7240::/33 304 | 2404:7600::/32 305 | 2404:7940::/32 306 | 2404:c2c0::/40 307 | 2404:c2c0:240::/44 308 | 2404:c2c0:280::/44 309 | 2404:c2c0:2c0::/44 310 | 2404:c2c0:501::/48 311 | 2404:c2c0:4000::/40 312 | 2404:c2c0:4100::/48 313 | 2404:c2c0:6000::/40 314 | 2404:c2c0:8000::/36 315 | 2404:c2c0:bb00::/40 316 | 2404:c2c0:c000::/36 317 | 2404:c300::/32 318 | 2404:c940::/48 319 | 2404:e280::/47 320 | 2404:e5c0::/32 321 | 2404:e8c0::/32 322 | 2404:f4c0::/32 323 | 2405:80:1::/48 324 | 2405:80:13::/48 325 | 2405:6c0:2::/48 326 | 2405:6c0:4::/48 327 | 2405:1480:1000::/48 328 | 2405:1480:2000::/48 329 | 2405:1480:3000::/47 330 | 2405:3140:11::/48 331 | 2405:3140:21::/48 332 | 2405:3140:31::/48 333 | 2405:3140:3a::/48 334 | 2405:66c0::/32 335 | 2405:68c0:21::/48 336 | 2405:6940::/48 337 | 2405:6f00:c101::/48 338 | 2405:6f00:c102::/48 339 | 2405:6f00:c170::/47 340 | 2405:6f00:c602::/48 341 | 2405:7040:6000::/47 342 | 2405:78c0:6e00::/43 343 | 2405:8a40::/32 344 | 2405:a900:ffee::/48 345 | 2405:a900:fffe::/48 346 | 2405:ad00::/32 347 | 2405:b7c0::/32 348 | 2405:d900::/32 349 | 2405:e000::/32 350 | 2405:f580::/32 351 | 2405:f940::/32 352 | 2406:280::/32 353 | 2406:840:9000::/44 354 | 2406:840:e080::/44 355 | 2406:840:e0cf::/48 356 | 2406:840:e0e0::/46 357 | 2406:840:e0e4::/47 358 | 2406:840:e0e8::/48 359 | 2406:840:e10f::/48 360 | 2406:840:e14f::/48 361 | 2406:840:e201::/48 362 | 2406:840:e230::/44 363 | 2406:840:e620::/47 364 | 2406:840:e666::/47 365 | 2406:840:e720::/44 366 | 2406:840:e80f::/48 367 | 2406:840:eb00::/46 368 | 2406:840:eb04::/47 369 | 2406:840:eb07::/48 370 | 2406:840:eb08::/48 371 | 2406:840:eb0b::/48 372 | 2406:840:eb0f::/48 373 | 2406:840:ee40::/47 374 | 2406:840:ee44::/48 375 | 2406:840:ee4b::/48 376 | 2406:840:ee4d::/48 377 | 2406:840:eee5::/48 378 | 2406:840:f200::/47 379 | 2406:840:f203::/48 380 | 2406:840:f380::/44 381 | 2406:840:fc80::/44 382 | 2406:840:fcd0::/48 383 | 2406:840:fcf0::/46 384 | 2406:840:fcf4::/47 385 | 2406:840:fd40::/42 386 | 2406:840:fd80::/42 387 | 2406:840:fdc0::/44 388 | 2406:840:fdd1::/48 389 | 2406:840:fe27::/48 390 | 2406:840:fe90::/46 391 | 2406:840:fe94::/48 392 | 2406:840:fe96::/47 393 | 2406:840:fe98::/46 394 | 2406:840:fea9::/48 395 | 2406:840:feab::/48 396 | 2406:840:feac::/48 397 | 2406:840:feaf::/48 398 | 2406:840:fec0::/48 399 | 2406:840:fec4::/48 400 | 2406:840:fec8::/48 401 | 2406:840:feca::/48 402 | 2406:840:fecc::/47 403 | 2406:840:fecf::/48 404 | 2406:840:fed8::/48 405 | 2406:840:fedf::/48 406 | 2406:840:fef1::/48 407 | 2406:840:fef3::/48 408 | 2406:840:fef9::/48 409 | 2406:840:fefb::/48 410 | 2406:840:fefc::/48 411 | 2406:840:feff::/48 412 | 2406:1080::/48 413 | 2406:2700::/32 414 | 2406:3340::/32 415 | 2406:3d80::/32 416 | 2406:4d00::/48 417 | 2406:52c0::/32 418 | 2406:5340:6666::/48 419 | 2406:5340:8888::/48 420 | 2406:6100::/32 421 | 2406:b640:100::/48 422 | 2406:b640:4100::/48 423 | 2406:cac0::/40 424 | 2406:cac0:200::/40 425 | 2406:cf00::/48 426 | 2406:cf00:1000::/43 427 | 2406:d440:100::/43 428 | 2406:d440:200::/43 429 | 2406:d440:300::/43 430 | 2406:d440:ff00::/48 431 | 2406:e3c0::/32 432 | 2406:e500::/33 433 | 2407:23c0::/48 434 | 2407:2840::/48 435 | 2407:3740::/48 436 | 2407:37c0::/32 437 | 2407:5380::/32 438 | 2407:6c40:1100::/48 439 | 2407:6c40:1210::/48 440 | 2407:6c40:1500::/48 441 | 2407:6c40:1600::/40 442 | 2407:8f40:2::/48 443 | 2407:9f00::/32 444 | 2407:ad80::/32 445 | 2407:ae80::/32 446 | 2407:b380:8000::/48 447 | 2407:bc00::/32 448 | 2407:c080::/35 449 | 2407:c080:4000::/37 450 | 2407:c080:5000::/37 451 | 2407:c080:6000::/36 452 | 2407:c080:8000::/36 453 | 2407:d9c0::/32 454 | 2408:4000::/22 455 | 2408:8000::/48 456 | 2408:8000:2::/47 457 | 2408:8000:1000::/36 458 | 2408:8000:2000::/35 459 | 2408:8000:4000::/34 460 | 2408:8000:8000::/33 461 | 2408:8001::/32 462 | 2408:8020::/30 463 | 2408:8024::/31 464 | 2408:8026::/32 465 | 2408:802a:8000::/33 466 | 2408:802c::/32 467 | 2408:803e::/32 468 | 2408:8056::/32 469 | 2408:805c::/30 470 | 2408:8060::/33 471 | 2408:80c2::/33 472 | 2408:80c5::/33 473 | 2408:80ca::/33 474 | 2408:80ca:8000::/34 475 | 2408:80da::/33 476 | 2408:80da:8000::/34 477 | 2408:80e0:4000::/34 478 | 2408:80e0:8000::/33 479 | 2408:80e2::/33 480 | 2408:80e9:4000::/34 481 | 2408:80ea:4000::/34 482 | 2408:80ea:8000::/33 483 | 2408:80f0:4000::/34 484 | 2408:80f0:8000::/33 485 | 2408:80f1::/42 486 | 2408:80f1:40::/43 487 | 2408:80f1:70::/44 488 | 2408:80f1:80::/41 489 | 2408:80f1:100::/43 490 | 2408:80f1:120::/44 491 | 2408:80f1:160::/43 492 | 2408:80f1:180::/43 493 | 2408:80f1:1b0::/44 494 | 2408:80f1:1c0::/43 495 | 2408:80f1:1e0::/44 496 | 2408:80f1:200::/40 497 | 2408:80f5:4000::/34 498 | 2408:80f9:4000::/34 499 | 2408:80fa:4000::/34 500 | 2408:80fa:8000::/33 501 | 2408:8120:1::/48 502 | 2408:8120:2::/48 503 | 2408:8120:7000::/36 504 | 2408:8140:2000::/48 505 | 2408:815f:e000::/35 506 | 2408:8181::/40 507 | 2408:8181:6000::/40 508 | 2408:8181:8000::/40 509 | 2408:8181:a000::/40 510 | 2408:8181:a220::/44 511 | 2408:8181:e000::/40 512 | 2408:8182:6000::/40 513 | 2408:8182:c000::/40 514 | 2408:8183:4000::/40 515 | 2408:8183:8000::/40 516 | 2408:81a2:2000::/35 517 | 2408:81a2:4000::/35 518 | 2408:81a3:6000::/35 519 | 2408:81a3:c800::/48 520 | 2408:81a3:ca66::/48 521 | 2408:8206::/31 522 | 2408:8208::/29 523 | 2408:8210::/30 524 | 2408:8214::/31 525 | 2408:821a::/31 526 | 2408:8220::/31 527 | 2408:8226::/32 528 | 2408:822a::/31 529 | 2408:822e::/31 530 | 2408:8230::/29 531 | 2408:8238::/31 532 | 2408:823c::/31 533 | 2408:8240::/32 534 | 2408:8244::/30 535 | 2408:8248::/30 536 | 2408:824c::/32 537 | 2408:824e::/31 538 | 2408:8250::/30 539 | 2408:8254::/32 540 | 2408:8256::/31 541 | 2408:8258::/30 542 | 2408:825c::/31 543 | 2408:825f::/32 544 | 2408:8260::/32 545 | 2408:8262::/31 546 | 2408:8264::/31 547 | 2408:8266::/32 548 | 2408:826a::/32 549 | 2408:826c::/30 550 | 2408:8270::/32 551 | 2408:8274::/30 552 | 2408:8278::/31 553 | 2408:827a::/32 554 | 2408:8306::/31 555 | 2408:8308::/30 556 | 2408:8310::/30 557 | 2408:832a::/32 558 | 2408:832e::/31 559 | 2408:8330::/30 560 | 2408:8338::/32 561 | 2408:8340::/32 562 | 2408:8344::/30 563 | 2408:8348::/30 564 | 2408:834e::/31 565 | 2408:8350::/30 566 | 2408:8354::/32 567 | 2408:8356::/31 568 | 2408:8358::/30 569 | 2408:8360::/30 570 | 2408:8364::/31 571 | 2408:836c::/30 572 | 2408:8374::/30 573 | 2408:8378::/31 574 | 2408:837a::/32 575 | 2408:8406:c0::/42 576 | 2408:8406:100::/41 577 | 2408:8406:180::/42 578 | 2408:8406:cc0::/42 579 | 2408:8406:d00::/41 580 | 2408:8406:d80::/42 581 | 2408:8406:18c0::/42 582 | 2408:8406:1900::/41 583 | 2408:8406:1980::/42 584 | 2408:8406:24c0::/42 585 | 2408:8406:2500::/41 586 | 2408:8406:2580::/42 587 | 2408:8406:30c0::/42 588 | 2408:8406:3100::/41 589 | 2408:8406:3180::/42 590 | 2408:8406:3cc0::/42 591 | 2408:8406:3d00::/41 592 | 2408:8406:3d80::/42 593 | 2408:8406:48c0::/42 594 | 2408:8406:4900::/41 595 | 2408:8406:4980::/42 596 | 2408:8406:54c0::/42 597 | 2408:8406:5500::/41 598 | 2408:8406:5580::/42 599 | 2408:8406:60c0::/42 600 | 2408:8406:6100::/41 601 | 2408:8406:6180::/42 602 | 2408:8406:6cc0::/42 603 | 2408:8406:6d00::/41 604 | 2408:8406:6d80::/42 605 | 2408:8406:78c0::/42 606 | 2408:8406:7900::/41 607 | 2408:8406:7980::/42 608 | 2408:8406:84c0::/42 609 | 2408:8406:8500::/41 610 | 2408:8406:8580::/42 611 | 2408:8406:90c0::/42 612 | 2408:8406:9100::/41 613 | 2408:8406:9180::/42 614 | 2408:8406:9cc0::/42 615 | 2408:8406:9d00::/41 616 | 2408:8406:9d80::/42 617 | 2408:8406:a8c0::/42 618 | 2408:8406:a900::/41 619 | 2408:8406:a980::/42 620 | 2408:8406:b4c0::/42 621 | 2408:8406:b500::/41 622 | 2408:8406:b580::/42 623 | 2408:8409::/40 624 | 2408:8409:100::/41 625 | 2408:8409:1a0::/43 626 | 2408:8409:1c0::/43 627 | 2408:8409:c00::/40 628 | 2408:8409:d00::/41 629 | 2408:8409:da0::/43 630 | 2408:8409:dc0::/43 631 | 2408:8409:1800::/40 632 | 2408:8409:1900::/41 633 | 2408:8409:19a0::/43 634 | 2408:8409:19c0::/43 635 | 2408:8409:2400::/40 636 | 2408:8409:2500::/41 637 | 2408:8409:25a0::/43 638 | 2408:8409:25c0::/43 639 | 2408:8409:3000::/40 640 | 2408:8409:3100::/41 641 | 2408:8409:31a0::/43 642 | 2408:8409:31c0::/43 643 | 2408:8409:3c00::/40 644 | 2408:8409:3d00::/41 645 | 2408:8409:3da0::/43 646 | 2408:8409:3dc0::/43 647 | 2408:8409:4800::/40 648 | 2408:8409:4900::/41 649 | 2408:8409:49a0::/43 650 | 2408:8409:49c0::/43 651 | 2408:8409:5400::/40 652 | 2408:8409:5500::/41 653 | 2408:8409:55a0::/43 654 | 2408:8409:55c0::/43 655 | 2408:8409:6000::/40 656 | 2408:8409:6100::/41 657 | 2408:8409:61a0::/43 658 | 2408:8409:61c0::/43 659 | 2408:8409:6c00::/40 660 | 2408:8409:6d00::/41 661 | 2408:8409:6da0::/43 662 | 2408:8409:6dc0::/43 663 | 2408:8409:7800::/40 664 | 2408:8409:7900::/41 665 | 2408:8409:79a0::/43 666 | 2408:8409:79c0::/43 667 | 2408:8409:8400::/40 668 | 2408:8409:8500::/41 669 | 2408:8409:85a0::/43 670 | 2408:8409:85c0::/43 671 | 2408:8409:9000::/40 672 | 2408:8409:9100::/41 673 | 2408:8409:91a0::/43 674 | 2408:8409:91c0::/43 675 | 2408:8409:9c00::/40 676 | 2408:8409:9d00::/41 677 | 2408:8409:9da0::/43 678 | 2408:8409:9dc0::/43 679 | 2408:8409:a800::/40 680 | 2408:8409:a900::/41 681 | 2408:8409:a9a0::/43 682 | 2408:8409:a9c0::/43 683 | 2408:8409:b400::/40 684 | 2408:8409:b500::/41 685 | 2408:8409:b5a0::/43 686 | 2408:8409:b5c0::/43 687 | 2408:840c::/40 688 | 2408:840c:200::/40 689 | 2408:840c:d00::/40 690 | 2408:840c:f00::/40 691 | 2408:840c:1a00::/40 692 | 2408:840c:1c00::/40 693 | 2408:840c:2700::/40 694 | 2408:840c:2900::/40 695 | 2408:840c:3400::/40 696 | 2408:840c:3600::/40 697 | 2408:840c:4e00::/40 698 | 2408:840c:5000::/40 699 | 2408:840c:5b00::/40 700 | 2408:840c:5d00::/40 701 | 2408:840c:6800::/40 702 | 2408:840c:6a00::/40 703 | 2408:840c:7500::/40 704 | 2408:840c:7700::/40 705 | 2408:840c:8200::/40 706 | 2408:840c:8400::/40 707 | 2408:840c:8f00::/40 708 | 2408:840c:9100::/40 709 | 2408:840c:9c00::/40 710 | 2408:840c:9e00::/40 711 | 2408:840c:a900::/40 712 | 2408:840c:ab00::/40 713 | 2408:840c:b600::/40 714 | 2408:840c:b800::/40 715 | 2408:840c:c300::/40 716 | 2408:840c:c500::/40 717 | 2408:840c:d000::/40 718 | 2408:840c:d200::/40 719 | 2408:840c:dd00::/40 720 | 2408:840c:de00::/40 721 | 2408:840d::/42 722 | 2408:840d:200::/42 723 | 2408:840d:400::/42 724 | 2408:840d:600::/42 725 | 2408:840d:d00::/42 726 | 2408:840d:f00::/42 727 | 2408:840d:1100::/42 728 | 2408:840d:1300::/42 729 | 2408:840d:1a00::/42 730 | 2408:840d:1c00::/42 731 | 2408:840d:1e00::/42 732 | 2408:840d:2000::/42 733 | 2408:840d:2700::/42 734 | 2408:840d:2900::/42 735 | 2408:840d:2b00::/42 736 | 2408:840d:2d00::/42 737 | 2408:840d:3400::/42 738 | 2408:840d:3600::/42 739 | 2408:840d:3800::/42 740 | 2408:840d:3a00::/42 741 | 2408:840d:4e00::/42 742 | 2408:840d:5000::/42 743 | 2408:840d:5200::/42 744 | 2408:840d:5400::/42 745 | 2408:840d:5b00::/42 746 | 2408:840d:5d00::/42 747 | 2408:840d:5f00::/42 748 | 2408:840d:6100::/42 749 | 2408:840d:6800::/42 750 | 2408:840d:6a00::/42 751 | 2408:840d:6c00::/42 752 | 2408:840d:6e00::/42 753 | 2408:840d:7500::/42 754 | 2408:840d:7700::/42 755 | 2408:840d:7900::/42 756 | 2408:840d:7b00::/42 757 | 2408:840d:8200::/42 758 | 2408:840d:8400::/42 759 | 2408:840d:8600::/42 760 | 2408:840d:8800::/42 761 | 2408:840d:8f00::/42 762 | 2408:840d:9100::/42 763 | 2408:840d:9300::/42 764 | 2408:840d:9500::/42 765 | 2408:840d:9c00::/42 766 | 2408:840d:9e00::/42 767 | 2408:840d:a000::/42 768 | 2408:840d:a200::/42 769 | 2408:840d:a900::/42 770 | 2408:840d:ab00::/42 771 | 2408:840d:ad00::/42 772 | 2408:840d:af00::/42 773 | 2408:840d:b600::/42 774 | 2408:840d:b800::/42 775 | 2408:840d:ba00::/42 776 | 2408:840d:bc00::/42 777 | 2408:840d:c300::/42 778 | 2408:840d:c500::/42 779 | 2408:840d:c700::/42 780 | 2408:840d:c900::/42 781 | 2408:840d:d000::/42 782 | 2408:840d:d200::/42 783 | 2408:840d:d400::/42 784 | 2408:840d:d600::/42 785 | 2408:840d:dd00::/42 786 | 2408:840d:de00::/42 787 | 2408:840e:dd00::/40 788 | 2408:840e:de00::/40 789 | 2408:840f:1e0::/46 790 | 2408:840f:1e4::/47 791 | 2408:8410::/30 792 | 2408:8414::/31 793 | 2408:8417::/32 794 | 2408:8418::/32 795 | 2408:841a::/31 796 | 2408:841c::/31 797 | 2408:841e::/32 798 | 2408:8420::/31 799 | 2408:8422::/32 800 | 2408:8426::/31 801 | 2408:842a::/31 802 | 2408:842c::/32 803 | 2408:842e::/32 804 | 2408:8430::/31 805 | 2408:8434::/30 806 | 2408:8438::/31 807 | 2408:843c::/30 808 | 2408:8440::/31 809 | 2408:8444::/30 810 | 2408:8448::/32 811 | 2408:844b::/32 812 | 2408:844c::/30 813 | 2408:8452::/31 814 | 2408:8454::/32 815 | 2408:8456::/31 816 | 2408:8458::/30 817 | 2408:845c::/31 818 | 2408:8460::/30 819 | 2408:8464::/31 820 | 2408:8466::/32 821 | 2408:8469::/32 822 | 2408:846a::/31 823 | 2408:846c::/30 824 | 2408:8470::/31 825 | 2408:8474::/30 826 | 2408:8478::/31 827 | 2408:847a::/32 828 | 2408:84e1::/32 829 | 2408:84e2::/31 830 | 2408:84e4::/30 831 | 2408:84e9::/32 832 | 2408:84eb::/32 833 | 2408:84ec::/30 834 | 2408:84f0::/28 835 | 2408:856c::/31 836 | 2408:8606::/31 837 | 2408:8608::/29 838 | 2408:8610::/30 839 | 2408:8614::/31 840 | 2408:861a::/31 841 | 2408:861c::/32 842 | 2408:8620::/31 843 | 2408:8624::/31 844 | 2408:8626::/32 845 | 2408:862a::/31 846 | 2408:862d::/32 847 | 2408:862e::/31 848 | 2408:8630::/29 849 | 2408:8638::/31 850 | 2408:863c::/31 851 | 2408:8640::/32 852 | 2408:8642::/32 853 | 2408:8644::/30 854 | 2408:8648::/31 855 | 2408:864c::/32 856 | 2408:864e::/31 857 | 2408:8650::/30 858 | 2408:8656::/31 859 | 2408:8658::/30 860 | 2408:865c::/31 861 | 2408:865f::/32 862 | 2408:8660::/32 863 | 2408:8662::/31 864 | 2408:8664::/31 865 | 2408:8666::/32 866 | 2408:866a::/31 867 | 2408:866c::/30 868 | 2408:8670::/32 869 | 2408:8674::/30 870 | 2408:8678::/31 871 | 2408:867a::/32 872 | 2408:8706::/31 873 | 2408:8708::/29 874 | 2408:8710::/30 875 | 2408:8719::/32 876 | 2408:871a::/31 877 | 2408:8720::/30 878 | 2408:8726::/32 879 | 2408:872b::/32 880 | 2408:872f::/32 881 | 2408:8730::/30 882 | 2408:8734::/31 883 | 2408:8736::/32 884 | 2408:8738::/32 885 | 2408:873c::/31 886 | 2408:8740::/32 887 | 2408:8742::/32 888 | 2408:8744::/30 889 | 2408:8748::/29 890 | 2408:8752::/32 891 | 2408:8756::/31 892 | 2408:8758::/30 893 | 2408:875c::/32 894 | 2408:8760::/32 895 | 2408:8762::/31 896 | 2408:8764::/31 897 | 2408:8766::/32 898 | 2408:8768::/32 899 | 2408:876a::/32 900 | 2408:876c::/30 901 | 2408:8770::/32 902 | 2408:8772::/31 903 | 2408:8774::/32 904 | 2408:8776::/31 905 | 2408:8778::/31 906 | 2408:877a::/32 907 | 2408:877c::/30 908 | 2408:8806::/42 909 | 2408:8806:40::/43 910 | 2408:880c::/30 911 | 2408:8810::/30 912 | 2408:8814::/31 913 | 2408:8818::/31 914 | 2408:882c::/32 915 | 2408:883a::/32 916 | 2408:8844::/43 917 | 2408:8856::/31 918 | 2408:8858::/30 919 | 2408:8862::/31 920 | 2408:8864::/31 921 | 2408:8866::/32 922 | 2408:886e::/31 923 | 2408:8872::/32 924 | 2408:8878::/31 925 | 2408:887e::/32 926 | 2408:8906:20::/44 927 | 2408:8907:9000::/44 928 | 2408:890c::/31 929 | 2408:8912::/31 930 | 2408:8914::/30 931 | 2408:891c::/32 932 | 2408:8920::/32 933 | 2408:8924::/32 934 | 2408:892c::/32 935 | 2408:8936::/32 936 | 2408:893a::/32 937 | 2408:8940::/32 938 | 2408:8948::/32 939 | 2408:894c::/32 940 | 2408:894e::/32 941 | 2408:8956::/31 942 | 2408:8958::/30 943 | 2408:8962::/31 944 | 2408:8964::/31 945 | 2408:8966::/32 946 | 2408:896c::/32 947 | 2408:896e::/31 948 | 2408:8972::/32 949 | 2408:8978::/30 950 | 2408:897e::/32 951 | 2408:8a00:c000::/36 952 | 2408:8a00:d000::/37 953 | 2408:8a00:e000::/35 954 | 2408:8a01::/36 955 | 2408:8a02:b110::/44 956 | 2408:8a02:b120::/44 957 | 2408:8a04:8000::/36 958 | 2408:8a04:e000::/40 959 | 2408:8a05:6000::/35 960 | 2408:8a05:8000::/36 961 | 2408:8a06::/47 962 | 2408:8a06:100::/47 963 | 2408:8a21:4000::/35 964 | 2408:8a22:9200::/39 965 | 2408:8a22:9400::/38 966 | 2408:8a22:9800::/40 967 | 2408:8a22:9a00::/39 968 | 2408:8a22:9c00::/38 969 | 2408:8a22:a000::/37 970 | 2408:8a23:4000::/34 971 | 2408:8a24:4000::/34 972 | 2408:8a26:c000::/34 973 | 2408:8a27:4000::/35 974 | 2409:2000::/31 975 | 2409:2002::/32 976 | 2409:27fa::/48 977 | 2409:27fa:f000::/48 978 | 2409:27fb::/48 979 | 2409:27fc::/48 980 | 2409:27fe::/33 981 | 2409:8000::/20 982 | 240a:2000::/40 983 | 240a:2001:100::/40 984 | 240a:2001:1000::/36 985 | 240a:4020:83a::/48 986 | 240a:4020:883a::/48 987 | 240a:4021:83a::/48 988 | 240a:4021:883a::/48 989 | 240a:4090:50::/48 990 | 240a:4090:120::/48 991 | 240a:4090:250::/48 992 | 240a:4090:1000::/39 993 | 240a:4090:1200::/40 994 | 240a:4090:2010::/48 995 | 240a:4090:2041::/48 996 | 240a:4090:2061::/48 997 | 240a:4090:3000::/39 998 | 240a:4090:3200::/40 999 | 240a:4090:5000::/39 1000 | 240a:4090:5200::/40 1001 | 240a:4090:7000::/39 1002 | 240a:4090:7200::/40 1003 | 240a:40b0:83a::/48 1004 | 240a:40b0:283a::/48 1005 | 240a:40b0:483a::/48 1006 | 240a:40b0:683a::/48 1007 | 240a:40c0:8000::/43 1008 | 240a:40c0:a000::/43 1009 | 240a:40c0:c000::/43 1010 | 240a:40c0:e000::/43 1011 | 240a:40c1::/43 1012 | 240a:40c1:2000::/43 1013 | 240a:40c1:4000::/43 1014 | 240a:40c1:6000::/43 1015 | 240a:40c1:a000::/43 1016 | 240a:40c1:c000::/43 1017 | 240a:40c1:e000::/43 1018 | 240a:40c2::/43 1019 | 240a:40c2:2000::/43 1020 | 240a:40c2:4000::/43 1021 | 240a:40c2:6000::/43 1022 | 240a:40c2:8000::/43 1023 | 240a:40c2:a000::/43 1024 | 240a:40c2:c000::/43 1025 | 240a:40c2:e000::/43 1026 | 240a:40c3::/43 1027 | 240a:40c3:2000::/43 1028 | 240a:40c3:4000::/43 1029 | 240a:40c3:6000::/43 1030 | 240a:40c3:8000::/43 1031 | 240a:40c3:c000::/43 1032 | 240a:40c3:e000::/43 1033 | 240a:40c4::/43 1034 | 240a:40c4:2000::/43 1035 | 240a:40c4:4000::/43 1036 | 240a:4280::/26 1037 | 240a:42c0::/27 1038 | 240a:42e0::/28 1039 | 240a:42f0::/29 1040 | 240a:42f8::/30 1041 | 240a:6001::/48 1042 | 240a:a000::/20 1043 | 240a:c000::/20 1044 | 240b:e001::/32 1045 | 240b:e002::/31 1046 | 240b:e004::/30 1047 | 240b:e008::/29 1048 | 240b:e010::/32 1049 | 240c:6::/47 1050 | 240c:6:3::/48 1051 | 240c:f:1::/48 1052 | 240c:4000::/22 1053 | 240c:c000::/20 1054 | 240d:4000::/21 1055 | 240e::/20 1056 | 2602:2e0:ff::/48 1057 | 2602:f7ee:ee::/48 1058 | 2602:f92a:1314::/48 1059 | 2602:f92a:a470::/47 1060 | 2602:f92a:dead::/48 1061 | 2602:f92a:e100::/44 1062 | 2602:f93b:c00::/38 1063 | 2602:f9ba:a8::/48 1064 | 2602:fab0:11::/48 1065 | 2602:feda:1bf::/48 1066 | 2602:feda:1d1::/48 1067 | 2602:feda:1d3::/48 1068 | 2602:feda:1df::/48 1069 | 2602:feda:2d0::/44 1070 | 2602:feda:2f0::/44 1071 | 2605:9d80:8001::/48 1072 | 2605:9d80:8011::/48 1073 | 2605:9d80:8021::/48 1074 | 2605:9d80:8031::/48 1075 | 2605:9d80:8041::/48 1076 | 2605:9d80:8081::/48 1077 | 2605:9d80:9003::/48 1078 | 2605:9d80:9013::/48 1079 | 2605:9d80:9023::/48 1080 | 2605:9d80:9033::/48 1081 | 2605:9d80:9042::/48 1082 | 2605:9d80:9071::/48 1083 | 2605:9d80:9092::/48 1084 | 2804:1e48:9001::/48 1085 | 2804:1e48:9002::/48 1086 | 2a01:f100:1f8::/48 1087 | 2a04:3e00:1002::/48 1088 | 2a04:f580:8010::/47 1089 | 2a04:f580:8090::/48 1090 | 2a04:f580:8210::/47 1091 | 2a04:f580:8290::/48 1092 | 2a04:f580:9010::/48 1093 | 2a04:f580:9012::/47 1094 | 2a04:f580:9020::/48 1095 | 2a04:f580:9030::/48 1096 | 2a04:f580:9040::/48 1097 | 2a04:f580:9050::/48 1098 | 2a04:f580:9060::/48 1099 | 2a04:f580:9070::/48 1100 | 2a04:f580:9080::/48 1101 | 2a04:f580:9210::/48 1102 | 2a04:f580:9212::/47 1103 | 2a04:f580:9220::/48 1104 | 2a04:f580:9230::/48 1105 | 2a04:f580:9240::/48 1106 | 2a04:f580:9250::/48 1107 | 2a04:f580:9260::/48 1108 | 2a04:f580:9270::/48 1109 | 2a04:f580:9280::/48 1110 | 2a04:f580:9290::/48 1111 | 2a06:1284::/32 1112 | 2a06:3603::/32 1113 | 2a06:3604::/31 1114 | 2a06:3607::/32 1115 | 2a06:9f81:4600::/43 1116 | 2a06:9f81:4620::/44 1117 | 2a06:9f81:4640::/43 1118 | 2a06:a005:260::/43 1119 | 2a06:a005:280::/43 1120 | 2a06:a005:2a0::/44 1121 | 2a06:a005:8d0::/44 1122 | 2a06:a005:a13::/48 1123 | 2a06:a005:1c40::/44 1124 | 2a09:54c5::/32 1125 | 2a09:b280:ff81::/48 1126 | 2a09:b280:ff83::/48 1127 | 2a09:b280:ff84::/47 1128 | 2a0a:2840::/30 1129 | 2a0a:2845:aab8::/46 1130 | 2a0a:2845:d647::/48 1131 | 2a0a:2846::/48 1132 | 2a0a:6040:ec00::/40 1133 | 2a0a:6044:6600::/40 1134 | 2a0a:d686:8000::/40 1135 | 2a0b:2542::/48 1136 | 2a0b:4340:a6::/48 1137 | 2a0b:4e07:b8::/47 1138 | 2a0c:9a40:84e0::/48 1139 | 2a0c:9a40:8fc1::/48 1140 | 2a0c:9a40:8fc2::/47 1141 | 2a0c:9a40:8fc4::/48 1142 | 2a0c:b641:571::/48 1143 | 2a0c:b641:722::/47 1144 | 2a0e:9b00::/29 1145 | 2a0e:aa01:1fff::/48 1146 | 2a0e:aa06::/40 1147 | 2a0e:aa06:490::/44 1148 | 2a0e:aa06:500::/44 1149 | 2a0e:aa06:520::/48 1150 | 2a0e:aa06:525::/48 1151 | 2a0e:aa06:541::/48 1152 | 2a0e:aa07:e01b::/48 1153 | 2a0e:aa07:e021::/48 1154 | 2a0e:aa07:e025::/48 1155 | 2a0e:aa07:e030::/48 1156 | 2a0e:aa07:e035::/48 1157 | 2a0e:aa07:e039::/48 1158 | 2a0e:aa07:e044::/48 1159 | 2a0e:aa07:e151::/48 1160 | 2a0e:aa07:e155::/48 1161 | 2a0e:aa07:e15f::/48 1162 | 2a0e:aa07:e160::/47 1163 | 2a0e:aa07:e162::/48 1164 | 2a0e:aa07:e16a::/48 1165 | 2a0e:aa07:e1a0::/44 1166 | 2a0e:aa07:e200::/44 1167 | 2a0e:aa07:e210::/48 1168 | 2a0e:aa07:e21c::/47 1169 | 2a0e:aa07:e220::/44 1170 | 2a0e:aa07:f0d0::/46 1171 | 2a0e:aa07:f0d4::/47 1172 | 2a0e:aa07:f0d8::/48 1173 | 2a0e:aa07:f0de::/47 1174 | 2a0e:b107:12b::/48 1175 | 2a0e:b107:272::/48 1176 | 2a0e:b107:740::/44 1177 | 2a0e:b107:c10::/47 1178 | 2a0e:b107:178d::/48 1179 | 2a0e:b107:178e::/48 1180 | 2a0f:1cc5:20::/44 1181 | 2a0f:1cc5:100::/43 1182 | 2a0f:1cc5:600::/47 1183 | 2a0f:1cc5:610::/48 1184 | 2a0f:1cc5:900::/40 1185 | 2a0f:1cc5:f00::/47 1186 | 2a0f:1cc5:f03::/48 1187 | 2a0f:1cc5:f05::/48 1188 | 2a0f:1cc5:f06::/48 1189 | 2a0f:1cc5:1c00::/48 1190 | 2a0f:1cc6:b100::/46 1191 | 2a0f:5707:ac01::/48 1192 | 2a0f:6284:300::/40 1193 | 2a0f:6284:400::/42 1194 | 2a0f:6284:440::/43 1195 | 2a0f:6284:3000::/36 1196 | 2a0f:6284:4b00::/40 1197 | 2a0f:6284:4c00::/44 1198 | 2a0f:6284:4c20::/44 1199 | 2a0f:6284:4c30::/48 1200 | 2a0f:6284:4c40::/43 1201 | 2a0f:6284:4c60::/44 1202 | 2a0f:6284:4c80::/43 1203 | 2a0f:6d80::/29 1204 | 2a0f:7803:f680::/44 1205 | 2a0f:7803:fa21::/48 1206 | 2a0f:7803:fa22::/47 1207 | 2a0f:7803:fa24::/46 1208 | 2a0f:7803:faf3::/48 1209 | 2a0f:7803:fe81::/48 1210 | 2a0f:7d07::/32 1211 | 2a0f:85c1:ba5::/48 1212 | 2a0f:85c1:bfe::/48 1213 | 2a0f:85c1:ce1::/48 1214 | 2a0f:85c1:cf1::/48 1215 | 2a0f:85c1:d90::/48 1216 | 2a0f:9400:6110::/48 1217 | 2a0f:9400:7700::/48 1218 | 2a0f:ac00::/29 1219 | 2a0f:ea47:fc1d::/48 1220 | 2a10:ccc0:d00::/47 1221 | 2a10:ccc0:d03::/48 1222 | 2a10:ccc0:d0a::/47 1223 | 2a10:ccc0:d0c::/47 1224 | 2a10:ccc6:66c8::/48 1225 | 2a10:ccc6:66cc::/46 1226 | 2a12:f8c3::/36 1227 | 2a13:1800::/48 1228 | 2a13:1800:10::/48 1229 | 2a13:1800:80::/44 1230 | 2a13:1800:300::/44 1231 | 2a13:1801:180::/43 1232 | 2a13:a5c3:ff21::/48 1233 | 2a13:a5c3:ff50::/44 1234 | 2a13:a5c7:1800::/40 1235 | 2a13:a5c7:2100::/48 1236 | 2a13:a5c7:2102::/48 1237 | 2a13:a5c7:2121::/48 1238 | 2a13:a5c7:2801::/48 1239 | 2a13:a5c7:31a0::/43 1240 | 2a13:a5c7:3301::/48 1241 | 2a13:a5c7:3304::/48 1242 | 2a13:a5c7:3306::/47 1243 | 2a13:aac4:f000::/44 1244 | 2a14:7c0:4a01::/48 1245 | 2a14:7c0:5103::/48 1246 | 2a14:4c41::/32 1247 | 2a14:67c1:70::/48 1248 | 2a14:67c1:73::/48 1249 | 2a14:67c1:74::/48 1250 | 2a14:67c1:702::/47 1251 | 2a14:67c1:704::/48 1252 | 2a14:67c1:800::/48 1253 | 2a14:67c1:802::/47 1254 | 2a14:67c1:804::/48 1255 | 2a14:67c1:806::/47 1256 | 2a14:67c1:a010::/44 1257 | 2a14:67c1:a020::/48 1258 | 2a14:67c1:a023::/48 1259 | 2a14:67c1:a024::/48 1260 | 2a14:67c1:a02a::/48 1261 | 2a14:67c1:a02f::/48 1262 | 2a14:67c1:a040::/47 1263 | 2a14:67c1:a100::/43 1264 | 2a14:67c1:a124::/48 1265 | 2a14:67c1:a126::/48 1266 | 2a14:67c1:a128::/48 1267 | 2a14:67c1:a144::/48 1268 | 2a14:67c1:b000::/48 1269 | 2a14:67c1:b066::/48 1270 | 2a14:67c1:b068::/47 1271 | 2a14:67c1:b100::/46 1272 | 2a14:67c1:b105::/48 1273 | 2a14:67c1:b107::/48 1274 | 2a14:67c1:b110::/48 1275 | 2a14:67c1:b130::/46 1276 | 2a14:67c1:b134::/47 1277 | 2a14:67c1:b136::/48 1278 | 2a14:67c1:b140::/48 1279 | 2a14:67c1:b142::/48 1280 | 2a14:67c1:b4a1::/48 1281 | 2a14:67c1:b4a8::/47 1282 | 2a14:67c1:b4c0::/45 1283 | 2a14:67c1:b4e0::/43 1284 | 2a14:67c1:b500::/47 1285 | 2a14:67c1:b514::/48 1286 | 2a14:67c1:b549::/48 1287 | 2a14:67c1:b578::/48 1288 | 2a14:67c1:b582::/48 1289 | 2a14:67c1:b586::/48 1290 | 2a14:67c1:b588::/47 1291 | 2a14:67c1:b590::/48 1292 | 2a14:67c1:b599::/48 1293 | 2a14:67c1:c300::/40 1294 | 2a14:67c1:c600::/40 1295 | 2a14:7580:730::/44 1296 | 2a14:7580:740::/44 1297 | 2a14:7580:750::/47 1298 | 2a14:7580:775::/48 1299 | 2a14:7580:777::/48 1300 | 2a14:7580:9200::/40 1301 | 2a14:7580:9400::/39 1302 | 2a14:7580:d000::/38 1303 | 2a14:7580:df00::/40 1304 | 2a14:7580:e200::/40 1305 | 2a14:7580:fe00::/40 1306 | 2a14:7580:ffe4::/48 1307 | 2a14:7580:fff4::/48 1308 | 2a14:7580:fff7::/48 1309 | 2a14:7580:fffa::/48 1310 | 2a14:7581:ffb::/48 1311 | 2a14:7581:ffd::/48 1312 | 2a14:7581:30b6::/48 1313 | 2a14:7581:3100::/40 1314 | 2a14:7581:3401::/48 1315 | 2a14:7583:f201::/48 1316 | 2a14:7583:f300::/46 1317 | 2a14:7583:f304::/47 1318 | 2a14:7583:f4f1::/48 1319 | 2a14:7583:f4fe::/48 1320 | 2a14:7583:f500::/48 1321 | 2a14:7583:f701::/48 1322 | 2a14:7583:f702::/47 1323 | 2a14:7583:f704::/46 1324 | 2a14:7583:f708::/46 1325 | 2a14:7583:f70c::/48 1326 | 2a14:7583:f743::/48 1327 | 2a14:7583:f744::/48 1328 | 2a14:7583:f764::/48 1329 | 2c0f:f7a8:8011::/48 1330 | 2c0f:f7a8:8050::/48 1331 | 2c0f:f7a8:805f::/48 1332 | 2c0f:f7a8:8150::/48 1333 | 2c0f:f7a8:815f::/48 1334 | 2c0f:f7a8:8211::/48 1335 | 2c0f:f7a8:9010::/48 1336 | 2c0f:f7a8:9020::/48 1337 | 2c0f:f7a8:9041::/48 1338 | 2c0f:f7a8:9210::/47 1339 | 2c0f:f7a8:9220::/48 1340 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/generate_client.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | /* 3 | * SPDX-License-Identifier: GPL-2.0-only 4 | * 5 | * Copyright (C) 2023-2025 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { readfile, writefile } from 'fs'; 11 | import { isnan } from 'math'; 12 | import { connect } from 'ubus'; 13 | import { cursor } from 'uci'; 14 | 15 | import { 16 | isEmpty, parseURL, strToBool, strToInt, strToTime, 17 | removeBlankAttrs, validation, HP_DIR, RUN_DIR 18 | } from 'homeproxy'; 19 | 20 | const ubus = connect(); 21 | 22 | /* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */ 23 | 24 | /* UCI config start */ 25 | const uci = cursor(); 26 | 27 | const uciconfig = 'homeproxy'; 28 | uci.load(uciconfig); 29 | 30 | const uciinfra = 'infra', 31 | ucimain = 'config', 32 | ucicontrol = 'control'; 33 | 34 | const ucidnssetting = 'dns', 35 | ucidnsserver = 'dns_server', 36 | ucidnsrule = 'dns_rule'; 37 | 38 | const uciroutingsetting = 'routing', 39 | uciroutingnode = 'routing_node', 40 | uciroutingrule = 'routing_rule'; 41 | 42 | const ucinode = 'node'; 43 | const uciruleset = 'ruleset'; 44 | 45 | const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china'; 46 | 47 | let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0]; 48 | if (!wan_dns) 49 | wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '223.5.5.5'; 50 | 51 | const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333'; 52 | 53 | const ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server') || 'time.apple.com'; 54 | 55 | const ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0'; 56 | 57 | let main_node, main_udp_node, dedicated_udp_node, default_outbound, default_outbound_dns, 58 | domain_strategy, sniff_override, dns_server, china_dns_server, dns_default_strategy, 59 | dns_default_server, dns_disable_cache, dns_disable_cache_expire, dns_independent_cache, 60 | dns_client_subnet, cache_file_store_rdrc, cache_file_rdrc_timeout, direct_domain_list, 61 | proxy_domain_list; 62 | 63 | if (routing_mode !== 'custom') { 64 | main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil'; 65 | main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil'; 66 | dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]); 67 | 68 | dns_server = uci.get(uciconfig, ucimain, 'dns_server'); 69 | if (isEmpty(dns_server) || dns_server === 'wan') 70 | dns_server = wan_dns; 71 | 72 | if (routing_mode === 'bypass_mainland_china') { 73 | china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server'); 74 | if (isEmpty(china_dns_server) || type(china_dns_server) !== 'string' || china_dns_server === 'wan') 75 | china_dns_server = wan_dns; 76 | } 77 | dns_default_strategy = (ipv6_support !== '1') ? 'ipv4_only' : null; 78 | 79 | direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt')); 80 | if (direct_domain_list) 81 | direct_domain_list = split(direct_domain_list, /[\r\n]/); 82 | 83 | proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt')); 84 | if (proxy_domain_list) 85 | proxy_domain_list = split(proxy_domain_list, /[\r\n]/); 86 | 87 | sniff_override = uci.get(uciconfig, uciinfra, 'sniff_override') || '1'; 88 | } else { 89 | /* DNS settings */ 90 | dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy'); 91 | dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server'); 92 | dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache'); 93 | dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire'); 94 | dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache'); 95 | dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet'); 96 | cache_file_store_rdrc = uci.get(uciconfig, ucidnssetting, 'cache_file_store_rdrc'), 97 | cache_file_rdrc_timeout = uci.get(uciconfig, ucidnssetting, 'cache_file_rdrc_timeout'); 98 | 99 | /* Routing settings */ 100 | default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil'; 101 | default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns'; 102 | domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy'); 103 | sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override'); 104 | } 105 | 106 | const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy', 107 | default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface'); 108 | 109 | const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330'; 110 | 111 | let self_mark, redirect_port, tproxy_port, tun_name, 112 | tun_addr4, tun_addr6, tun_mtu, tcpip_stack, 113 | endpoint_independent_nat, udp_timeout; 114 | 115 | if (routing_mode === 'custom') 116 | udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout'); 117 | else 118 | udp_timeout = uci.get(uciconfig, 'infra', 'udp_timeout'); 119 | 120 | if (match(proxy_mode, /redirect/)) { 121 | self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100'; 122 | redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331'; 123 | } 124 | if (match(proxy_mode), /tproxy/) 125 | if (main_udp_node !== 'nil' || routing_mode === 'custom') 126 | tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332'; 127 | if (match(proxy_mode), /tun/) { 128 | tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0'; 129 | tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30'; 130 | tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126'; 131 | tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000'; 132 | tcpip_stack = 'system'; 133 | if (routing_mode === 'custom') { 134 | tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system'; 135 | endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat'); 136 | } 137 | } 138 | 139 | const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn'; 140 | /* UCI config end */ 141 | 142 | /* Config helper start */ 143 | function parse_port(strport) { 144 | if (type(strport) !== 'array' || isEmpty(strport)) 145 | return null; 146 | 147 | let ports = []; 148 | for (let i in strport) 149 | push(ports, int(i)); 150 | 151 | return ports; 152 | 153 | } 154 | 155 | function parse_dnsserver(server_addr, default_protocol) { 156 | if (isEmpty(server_addr)) 157 | return null; 158 | 159 | if (!match(server_addr, /:\/\//)) 160 | server_addr = (default_protocol || 'udp') + '://' + (validation('ip6addr', server_addr) ? `[${server_addr}]` : server_addr); 161 | server_addr = parseURL(server_addr); 162 | 163 | return { 164 | type: server_addr.protocol, 165 | server: server_addr.hostname, 166 | server_port: strToInt(server_addr.port), 167 | path: (server_addr.pathname !== '/') ? server_addr.pathname : null, 168 | } 169 | } 170 | 171 | function parse_dnsquery(strquery) { 172 | if (type(strquery) !== 'array' || isEmpty(strquery)) 173 | return null; 174 | 175 | let querys = []; 176 | for (let i in strquery) 177 | isnan(int(i)) ? push(querys, i) : push(querys, int(i)); 178 | 179 | return querys; 180 | 181 | } 182 | 183 | function generate_endpoint(node) { 184 | if (type(node) !== 'object' || isEmpty(node)) 185 | return null; 186 | 187 | const endpoint = { 188 | type: node.type, 189 | tag: 'cfg-' + node['.name'] + '-out', 190 | address: node.wireguard_local_address, 191 | mtu: strToInt(node.wireguard_mtu), 192 | private_key: node.wireguard_private_key, 193 | peers: (node.type === 'wireguard') ? [ 194 | { 195 | address: node.address, 196 | port: strToInt(node.port), 197 | allowed_ips: [ 198 | '0.0.0.0/0', 199 | '::/0' 200 | ], 201 | persistent_keepalive_interval: strToInt(node.wireguard_persistent_keepalive_interval), 202 | public_key: node.wireguard_peer_public_key, 203 | pre_shared_key: node.wireguard_pre_shared_key, 204 | reserved: parse_port(node.wireguard_reserved), 205 | } 206 | ] : null, 207 | system: (node.type === 'wireguard') ? false : null, 208 | tcp_fast_open: strToBool(node.tcp_fast_open), 209 | tcp_multi_path: strToBool(node.tcp_multi_path), 210 | udp_fragment: strToBool(node.udp_fragment) 211 | }; 212 | 213 | return endpoint; 214 | } 215 | 216 | function generate_outbound(node) { 217 | if (type(node) !== 'object' || isEmpty(node)) 218 | return null; 219 | 220 | const outbound = { 221 | type: node.type, 222 | tag: 'cfg-' + node['.name'] + '-out', 223 | routing_mark: strToInt(self_mark), 224 | 225 | server: node.address, 226 | server_port: strToInt(node.port), 227 | /* Hysteria(2) */ 228 | server_ports: node.hysteria_hopping_port, 229 | 230 | username: (node.type !== 'ssh') ? node.username : null, 231 | user: (node.type === 'ssh') ? node.username : null, 232 | password: node.password, 233 | 234 | /* Direct */ 235 | override_address: node.override_address, 236 | override_port: strToInt(node.override_port), 237 | proxy_protocol: strToInt(node.proxy_protocol), 238 | /* AnyTLS */ 239 | idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval), 240 | idle_session_timeout: strToTime(node.anytls_idle_session_timeout), 241 | min_idle_session: strToInt(node.anytls_min_idle_session), 242 | /* Hysteria (2) */ 243 | hop_interval: strToTime(node.hysteria_hop_interval), 244 | up_mbps: strToInt(node.hysteria_up_mbps), 245 | down_mbps: strToInt(node.hysteria_down_mbps), 246 | obfs: node.hysteria_obfs_type ? { 247 | type: node.hysteria_obfs_type, 248 | password: node.hysteria_obfs_password 249 | } : node.hysteria_obfs_password, 250 | auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null, 251 | auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null, 252 | recv_window_conn: strToInt(node.hysteria_recv_window_conn), 253 | recv_window: strToInt(node.hysteria_revc_window), 254 | disable_mtu_discovery: strToBool(node.hysteria_disable_mtu_discovery), 255 | /* Shadowsocks */ 256 | method: node.shadowsocks_encrypt_method, 257 | plugin: node.shadowsocks_plugin, 258 | plugin_opts: node.shadowsocks_plugin_opts, 259 | /* ShadowTLS / Socks */ 260 | version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null), 261 | /* SSH */ 262 | client_version: node.ssh_client_version, 263 | host_key: node.ssh_host_key, 264 | host_key_algorithms: node.ssh_host_key_algo, 265 | private_key: node.ssh_priv_key, 266 | private_key_passphrase: node.ssh_priv_key_pp, 267 | /* Tuic */ 268 | uuid: node.uuid, 269 | congestion_control: node.tuic_congestion_control, 270 | udp_relay_mode: node.tuic_udp_relay_mode, 271 | udp_over_stream: strToBool(node.tuic_udp_over_stream), 272 | zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt), 273 | heartbeat: strToTime(node.tuic_heartbeat), 274 | /* VLESS / VMess */ 275 | flow: node.vless_flow, 276 | alter_id: strToInt(node.vmess_alterid), 277 | security: node.vmess_encrypt, 278 | global_padding: strToBool(node.vmess_global_padding), 279 | authenticated_length: strToBool(node.vmess_authenticated_length), 280 | packet_encoding: node.packet_encoding, 281 | 282 | multiplex: (node.multiplex === '1') ? { 283 | enabled: true, 284 | protocol: node.multiplex_protocol, 285 | max_connections: strToInt(node.multiplex_max_connections), 286 | min_streams: strToInt(node.multiplex_min_streams), 287 | max_streams: strToInt(node.multiplex_max_streams), 288 | padding: strToBool(node.multiplex_padding), 289 | brutal: (node.multiplex_brutal === '1') ? { 290 | enabled: true, 291 | up_mbps: strToInt(node.multiplex_brutal_up), 292 | down_mbps: strToInt(node.multiplex_brutal_down) 293 | } : null 294 | } : null, 295 | tls: (node.tls === '1') ? { 296 | enabled: true, 297 | server_name: node.tls_sni, 298 | insecure: strToBool(node.tls_insecure), 299 | alpn: node.tls_alpn, 300 | min_version: node.tls_min_version, 301 | max_version: node.tls_max_version, 302 | cipher_suites: node.tls_cipher_suites, 303 | certificate_path: node.tls_cert_path, 304 | ech: (node.tls_ech === '1') ? { 305 | enabled: true, 306 | config: node.tls_ech_config, 307 | config_path: node.tls_ech_config_path 308 | } : null, 309 | utls: !isEmpty(node.tls_utls) ? { 310 | enabled: true, 311 | fingerprint: node.tls_utls 312 | } : null, 313 | reality: (node.tls_reality === '1') ? { 314 | enabled: true, 315 | public_key: node.tls_reality_public_key, 316 | short_id: node.tls_reality_short_id 317 | } : null 318 | } : null, 319 | transport: !isEmpty(node.transport) ? { 320 | type: node.transport, 321 | host: node.http_host || node.httpupgrade_host, 322 | path: node.http_path || node.ws_path, 323 | headers: node.ws_host ? { 324 | Host: node.ws_host 325 | } : null, 326 | method: node.http_method, 327 | max_early_data: strToInt(node.websocket_early_data), 328 | early_data_header_name: node.websocket_early_data_header, 329 | service_name: node.grpc_servicename, 330 | idle_timeout: (node.http_idle_timeout), 331 | ping_timeout: (node.http_ping_timeout), 332 | permit_without_stream: strToBool(node.grpc_permit_without_stream) 333 | } : null, 334 | udp_over_tcp: (node.udp_over_tcp === '1') ? { 335 | enabled: true, 336 | version: strToInt(node.udp_over_tcp_version) 337 | } : null, 338 | tcp_fast_open: strToBool(node.tcp_fast_open), 339 | tcp_multi_path: strToBool(node.tcp_multi_path), 340 | udp_fragment: strToBool(node.udp_fragment) 341 | }; 342 | 343 | return outbound; 344 | } 345 | 346 | function get_outbound(cfg) { 347 | if (isEmpty(cfg)) 348 | return null; 349 | 350 | if (type(cfg) === 'array') { 351 | if ('any-out' in cfg) 352 | return 'any'; 353 | 354 | let outbounds = []; 355 | for (let i in cfg) 356 | push(outbounds, get_outbound(i)); 357 | return outbounds; 358 | } else { 359 | switch (cfg) { 360 | case 'block-out': 361 | case 'direct-out': 362 | return cfg; 363 | default: 364 | const node = uci.get(uciconfig, cfg, 'node'); 365 | if (isEmpty(node)) 366 | die(sprintf("%s's node is missing, please check your configuration.", cfg)); 367 | else if (node === 'urltest') 368 | return 'cfg-' + cfg + '-out'; 369 | else 370 | return 'cfg-' + node + '-out'; 371 | } 372 | } 373 | } 374 | 375 | function get_resolver(cfg) { 376 | if (isEmpty(cfg)) 377 | return null; 378 | 379 | switch (cfg) { 380 | case 'default-dns': 381 | case 'system-dns': 382 | return cfg; 383 | default: 384 | return 'cfg-' + cfg + '-dns'; 385 | } 386 | } 387 | 388 | function get_ruleset(cfg) { 389 | if (isEmpty(cfg)) 390 | return null; 391 | 392 | let rules = []; 393 | for (let i in cfg) 394 | push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule'); 395 | return rules; 396 | } 397 | /* Config helper end */ 398 | 399 | const config = {}; 400 | 401 | /* Log */ 402 | config.log = { 403 | disabled: false, 404 | level: log_level, 405 | output: RUN_DIR + '/sing-box-c.log', 406 | timestamp: true 407 | }; 408 | 409 | /* NTP */ 410 | if (!isEmpty(ntp_server)) 411 | config.ntp = { 412 | enabled: true, 413 | server: ntp_server, 414 | detour: 'direct-out', 415 | domain_resolver: 'default-dns', 416 | }; 417 | 418 | /* DNS start */ 419 | /* Default settings */ 420 | config.dns = { 421 | servers: [ 422 | { 423 | tag: 'default-dns', 424 | type: 'udp', 425 | server: wan_dns, 426 | detour: self_mark ? 'direct-out' : null 427 | }, 428 | { 429 | tag: 'system-dns', 430 | type: 'local', 431 | detour: self_mark ? 'direct-out' : null 432 | } 433 | ], 434 | rules: [], 435 | strategy: dns_default_strategy, 436 | disable_cache: strToBool(dns_disable_cache), 437 | disable_expire: strToBool(dns_disable_cache_expire), 438 | independent_cache: strToBool(dns_independent_cache), 439 | client_subnet: dns_client_subnet 440 | }; 441 | 442 | if (!isEmpty(main_node)) { 443 | /* Main DNS */ 444 | push(config.dns.servers, { 445 | tag: 'main-dns', 446 | domain_resolver: { 447 | server: 'default-dns', 448 | strategy: (ipv6_support !== '1') ? 'ipv4_only' : null 449 | }, 450 | detour: 'main-out', 451 | ...parse_dnsserver(dns_server, 'tcp') 452 | }); 453 | config.dns.final = 'main-dns'; 454 | 455 | if (length(direct_domain_list)) 456 | push(config.dns.rules, { 457 | rule_set: 'direct-domain', 458 | action: 'route', 459 | server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns' 460 | }); 461 | 462 | /* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */ 463 | if (routing_mode === 'gfwlist' || length(proxy_domain_list)) 464 | push(config.dns.rules, { 465 | rule_set: (routing_mode !== 'gfwlist') ? 'proxy-domain' : null, 466 | query_type: [64, 65], 467 | action: 'reject' 468 | }); 469 | 470 | if (routing_mode === 'bypass_mainland_china') { 471 | push(config.dns.servers, { 472 | tag: 'china-dns', 473 | domain_resolver: { 474 | server: 'default-dns', 475 | strategy: 'prefer_ipv6' 476 | }, 477 | detour: self_mark ? 'direct-out' : null, 478 | ...parse_dnsserver(china_dns_server) 479 | }); 480 | 481 | if (length(proxy_domain_list)) 482 | push(config.dns.rules, { 483 | rule_set: 'proxy-domain', 484 | action: 'route', 485 | server: 'main-dns' 486 | }); 487 | 488 | push(config.dns.rules, { 489 | rule_set: 'geosite-cn', 490 | action: 'route', 491 | server: 'china-dns', 492 | strategy: 'prefer_ipv6' 493 | }); 494 | push(config.dns.rules, { 495 | type: 'logical', 496 | mode: 'and', 497 | rules: [ 498 | { 499 | rule_set: 'geosite-noncn', 500 | invert: true 501 | }, 502 | { 503 | rule_set: 'geoip-cn' 504 | } 505 | ], 506 | action: 'route', 507 | server: 'china-dns', 508 | strategy: 'prefer_ipv6' 509 | }); 510 | } 511 | } else if (!isEmpty(default_outbound)) { 512 | /* DNS servers */ 513 | uci.foreach(uciconfig, ucidnsserver, (cfg) => { 514 | if (cfg.enabled !== '1') 515 | return; 516 | 517 | let outbound = get_outbound(cfg.outbound); 518 | if (outbound === 'direct-out' && isEmpty(self_mark)) 519 | outbound = null; 520 | 521 | push(config.dns.servers, { 522 | tag: 'cfg-' + cfg['.name'] + '-dns', 523 | type: cfg.type, 524 | server: cfg.server, 525 | server_port: strToInt(cfg.server_port), 526 | path: cfg.path, 527 | headers: cfg.headers, 528 | tls: cfg.tls_sni ? { 529 | enabled: true, 530 | server_name: cfg.tls_sni 531 | } : null, 532 | domain_resolver: (cfg.address_resolver || cfg.address_strategy) ? { 533 | server: get_resolver(cfg.address_resolver || dns_default_server), 534 | strategy: cfg.address_strategy 535 | } : null, 536 | detour: outbound 537 | }); 538 | }); 539 | 540 | /* DNS rules */ 541 | uci.foreach(uciconfig, ucidnsrule, (cfg) => { 542 | if (cfg.enabled !== '1') 543 | return; 544 | 545 | push(config.dns.rules, { 546 | ip_version: strToInt(cfg.ip_version), 547 | query_type: parse_dnsquery(cfg.query_type), 548 | network: cfg.network, 549 | protocol: cfg.protocol, 550 | domain: cfg.domain, 551 | domain_suffix: cfg.domain_suffix, 552 | domain_keyword: cfg.domain_keyword, 553 | domain_regex: cfg.domain_regex, 554 | port: parse_port(cfg.port), 555 | port_range: cfg.port_range, 556 | source_ip_cidr: cfg.source_ip_cidr, 557 | source_ip_is_private: strToBool(cfg.source_ip_is_private), 558 | ip_cidr: cfg.ip_cidr, 559 | ip_is_private: strToBool(cfg.ip_is_private), 560 | source_port: parse_port(cfg.source_port), 561 | source_port_range: cfg.source_port_range, 562 | process_name: cfg.process_name, 563 | process_path: cfg.process_path, 564 | process_path_regex: cfg.process_path_regex, 565 | user: cfg.user, 566 | rule_set: get_ruleset(cfg.rule_set), 567 | rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source), 568 | invert: strToBool(cfg.invert), 569 | outbound: get_outbound(cfg.outbound), 570 | action: cfg.action, 571 | server: get_resolver(cfg.server), 572 | strategy: cfg.domain_strategy, 573 | disable_cache: strToBool(cfg.dns_disable_cache), 574 | rewrite_ttl: strToInt(cfg.rewrite_ttl), 575 | client_subnet: cfg.client_subnet, 576 | method: cfg.reject_method, 577 | no_drop: strToBool(cfg.reject_no_drop), 578 | rcode: cfg.predefined_rcode, 579 | answer: cfg.predefined_answer, 580 | ns: cfg.predefined_ns, 581 | extra: cfg.predefined_extra 582 | }); 583 | }); 584 | 585 | if (isEmpty(config.dns.rules)) 586 | config.dns.rules = null; 587 | 588 | config.dns.final = get_resolver(dns_default_server); 589 | } 590 | /* DNS end */ 591 | 592 | /* Inbound start */ 593 | config.inbounds = []; 594 | 595 | push(config.inbounds, { 596 | type: 'direct', 597 | tag: 'dns-in', 598 | listen: '::', 599 | listen_port: int(dns_port) 600 | }); 601 | 602 | push(config.inbounds, { 603 | type: 'mixed', 604 | tag: 'mixed-in', 605 | listen: '::', 606 | listen_port: int(mixed_port), 607 | udp_timeout: strToTime(udp_timeout), 608 | sniff: true, 609 | sniff_override_destination: strToBool(sniff_override), 610 | set_system_proxy: false 611 | }); 612 | 613 | if (match(proxy_mode, /redirect/)) 614 | push(config.inbounds, { 615 | type: 'redirect', 616 | tag: 'redirect-in', 617 | 618 | listen: '::', 619 | listen_port: int(redirect_port), 620 | sniff: true, 621 | sniff_override_destination: strToBool(sniff_override) 622 | }); 623 | if (match(proxy_mode, /tproxy/)) 624 | push(config.inbounds, { 625 | type: 'tproxy', 626 | tag: 'tproxy-in', 627 | 628 | listen: '::', 629 | listen_port: int(tproxy_port), 630 | network: 'udp', 631 | udp_timeout: strToTime(udp_timeout), 632 | sniff: true, 633 | sniff_override_destination: strToBool(sniff_override) 634 | }); 635 | if (match(proxy_mode, /tun/)) 636 | push(config.inbounds, { 637 | type: 'tun', 638 | tag: 'tun-in', 639 | 640 | interface_name: tun_name, 641 | address: (ipv6_support === '1') ? [tun_addr4, tun_addr6] : [tun_addr4], 642 | mtu: strToInt(tun_mtu), 643 | auto_route: false, 644 | endpoint_independent_nat: strToBool(endpoint_independent_nat), 645 | udp_timeout: strToTime(udp_timeout), 646 | stack: tcpip_stack, 647 | sniff: true, 648 | sniff_override_destination: strToBool(sniff_override) 649 | }); 650 | /* Inbound end */ 651 | 652 | /* Outbound start */ 653 | config.endpoints = []; 654 | 655 | /* Default outbounds */ 656 | config.outbounds = [ 657 | { 658 | type: 'direct', 659 | tag: 'direct-out', 660 | routing_mark: strToInt(self_mark) 661 | }, 662 | { 663 | type: 'block', 664 | tag: 'block-out' 665 | } 666 | ]; 667 | 668 | /* Main outbounds */ 669 | if (!isEmpty(main_node)) { 670 | let urltest_nodes = []; 671 | 672 | if (main_node === 'urltest') { 673 | const main_urltest_nodes = uci.get(uciconfig, ucimain, 'main_urltest_nodes') || []; 674 | const main_urltest_interval = uci.get(uciconfig, ucimain, 'main_urltest_interval'); 675 | const main_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_urltest_tolerance'); 676 | 677 | push(config.outbounds, { 678 | type: 'urltest', 679 | tag: 'main-out', 680 | outbounds: map(main_urltest_nodes, (k) => `cfg-${k}-out`), 681 | interval: strToTime(main_urltest_interval), 682 | tolerance: strToInt(main_urltest_tolerance), 683 | idle_timeout: (strToInt(main_urltest_interval) > 1800) ? `${main_urltest_interval * 2}s` : null, 684 | }); 685 | urltest_nodes = main_urltest_nodes; 686 | } else { 687 | const main_node_cfg = uci.get_all(uciconfig, main_node) || {}; 688 | if (main_node_cfg.type === 'wireguard') { 689 | push(config.endpoints, generate_endpoint(main_node_cfg)); 690 | config.endpoints[length(config.endpoints)-1].tag = 'main-out'; 691 | } else { 692 | push(config.outbounds, generate_outbound(main_node_cfg)); 693 | config.outbounds[length(config.outbounds)-1].tag = 'main-out'; 694 | } 695 | } 696 | 697 | if (main_udp_node === 'urltest') { 698 | const main_udp_urltest_nodes = uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes') || []; 699 | const main_udp_urltest_interval = uci.get(uciconfig, ucimain, 'main_udp_urltest_interval'); 700 | const main_udp_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_udp_urltest_tolerance'); 701 | 702 | push(config.outbounds, { 703 | type: 'urltest', 704 | tag: 'main-udp-out', 705 | outbounds: map(main_udp_urltest_nodes, (k) => `cfg-${k}-out`), 706 | interval: strToTime(main_udp_urltest_interval), 707 | tolerance: strToInt(main_udp_urltest_tolerance), 708 | idle_timeout: (strToInt(main_udp_urltest_interval) > 1800) ? `${main_udp_urltest_interval * 2}s` : null, 709 | }); 710 | urltest_nodes = [...urltest_nodes, ...filter(main_udp_urltest_nodes, (l) => !~index(urltest_nodes, l))]; 711 | } else if (dedicated_udp_node) { 712 | const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {}; 713 | if (main_udp_node_cfg.type === 'wireguard') { 714 | push(config.endpoints, generate_endpoint(main_udp_node_cfg)); 715 | config.endpoints[length(config.endpoints)-1].tag = 'main-udp-out'; 716 | } else { 717 | push(config.outbounds, generate_outbound(main_udp_node_cfg)); 718 | config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out'; 719 | } 720 | } 721 | 722 | for (let i in urltest_nodes) { 723 | const urltest_node = uci.get_all(uciconfig, i) || {}; 724 | if (urltest_node.type === 'wireguard') { 725 | push(config.endpoints, generate_endpoint(urltest_node)); 726 | config.endpoints[length(config.endpoints)-1].tag = 'cfg-' + i + '-out'; 727 | } else { 728 | push(config.outbounds, generate_outbound(urltest_node)); 729 | config.outbounds[length(config.outbounds)-1].tag = 'cfg-' + i + '-out'; 730 | } 731 | } 732 | } else if (!isEmpty(default_outbound)) { 733 | let urltest_nodes = [], 734 | routing_nodes = []; 735 | 736 | uci.foreach(uciconfig, uciroutingnode, (cfg) => { 737 | if (cfg.enabled !== '1') 738 | return; 739 | 740 | if (cfg.node === 'urltest') { 741 | push(config.outbounds, { 742 | type: 'urltest', 743 | tag: 'cfg-' + cfg['.name'] + '-out', 744 | outbounds: map(cfg.urltest_nodes, (k) => `cfg-${k}-out`), 745 | url: cfg.urltest_url, 746 | interval: strToTime(cfg.urltest_interval), 747 | tolerance: strToInt(cfg.urltest_tolerance), 748 | idle_timeout: strToTime(cfg.urltest_idle_timeout), 749 | interrupt_exist_connections: strToBool(cfg.urltest_interrupt_exist_connections) 750 | }); 751 | urltest_nodes = [...urltest_nodes, ...filter(cfg.urltest_nodes, (l) => !~index(urltest_nodes, l))]; 752 | } else { 753 | const outbound = uci.get_all(uciconfig, cfg.node) || {}; 754 | if (outbound.type === 'wireguard') { 755 | push(config.endpoints, generate_endpoint(outbound)); 756 | config.endpoints[length(config.endpoints)-1].bind_interface = cfg.bind_interface; 757 | config.endpoints[length(config.endpoints)-1].detour = get_outbound(cfg.outbound); 758 | if (cfg.domain_resolver) 759 | config.endpoints[length(config.endpoints)-1].domain_resolver = { 760 | server: get_resolver(cfg.domain_resolver), 761 | strategy: cfg.domain_strategy 762 | }; 763 | } else { 764 | push(config.outbounds, generate_outbound(outbound)); 765 | config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface; 766 | config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound); 767 | if (cfg.domain_resolver) 768 | config.outbounds[length(config.outbounds)-1].domain_resolver = { 769 | server: get_resolver(cfg.domain_resolver), 770 | strategy: cfg.domain_strategy 771 | }; 772 | } 773 | push(routing_nodes, cfg.node); 774 | } 775 | }); 776 | 777 | for (let i in filter(urltest_nodes, (l) => !~index(routing_nodes, l))) { 778 | const urltest_node = uci.get_all(uciconfig, i) || {}; 779 | if (urltest_node.type === 'wireguard') 780 | push(config.endpoints, generate_endpoint(urltest_node)); 781 | else 782 | push(config.outbounds, generate_outbound(urltest_node)); 783 | } 784 | } 785 | 786 | if (isEmpty(config.endpoints)) 787 | config.endpoints = null; 788 | /* Outbound end */ 789 | 790 | /* Routing rules start */ 791 | /* Default settings */ 792 | config.route = { 793 | rules: [ 794 | { 795 | inbound: 'dns-in', 796 | action: 'hijack-dns' 797 | } 798 | /* 799 | * leave for sing-box 1.13.0 800 | * { 801 | * action: 'sniff' 802 | * } 803 | */ 804 | ], 805 | rule_set: [], 806 | auto_detect_interface: isEmpty(default_interface) ? true : null, 807 | default_interface: default_interface 808 | }; 809 | 810 | /* Routing rules */ 811 | if (!isEmpty(main_node)) { 812 | /* Avoid DNS loop */ 813 | config.route.default_domain_resolver = { 814 | action: 'route', 815 | server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns', 816 | strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null 817 | }; 818 | 819 | /* Direct list */ 820 | if (length(direct_domain_list)) 821 | push(config.route.rules, { 822 | rule_set: 'direct-domain', 823 | action: 'route', 824 | outbound: 'direct-out' 825 | }); 826 | 827 | /* Main UDP out */ 828 | if (dedicated_udp_node) 829 | push(config.route.rules, { 830 | network: 'udp', 831 | action: 'route', 832 | outbound: 'main-udp-out' 833 | }); 834 | 835 | config.route.final = 'main-out'; 836 | 837 | /* Rule set */ 838 | /* Direct list */ 839 | if (length(direct_domain_list)) 840 | push(config.route.rule_set, { 841 | type: 'inline', 842 | tag: 'direct-domain', 843 | rules: [ 844 | { 845 | domain_keyword: direct_domain_list, 846 | } 847 | ] 848 | }); 849 | 850 | /* Proxy list */ 851 | if (length(proxy_domain_list)) 852 | push(config.route.rule_set, { 853 | type: 'inline', 854 | tag: 'proxy-domain', 855 | rules: [ 856 | { 857 | domain_keyword: proxy_domain_list, 858 | } 859 | ] 860 | }); 861 | 862 | if (routing_mode === 'bypass_mainland_china') { 863 | push(config.route.rule_set, { 864 | type: 'remote', 865 | tag: 'geoip-cn', 866 | format: 'binary', 867 | url: 'https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs', 868 | download_detour: 'main-out' 869 | }); 870 | push(config.route.rule_set, { 871 | type: 'remote', 872 | tag: 'geosite-cn', 873 | format: 'binary', 874 | url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs', 875 | download_detour: 'main-out' 876 | }); 877 | push(config.route.rule_set, { 878 | type: 'remote', 879 | tag: 'geosite-noncn', 880 | format: 'binary', 881 | url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs', 882 | download_detour: 'main-out' 883 | }); 884 | } 885 | 886 | if (isEmpty(config.route.rule_set)) 887 | config.route.rule_set = null; 888 | } else if (!isEmpty(default_outbound)) { 889 | config.route.default_domain_resolver = { 890 | action: 'resolve', 891 | server: get_resolver(default_outbound_dns) 892 | }; 893 | 894 | if (domain_strategy) 895 | push(config.route.rules, { 896 | action: 'resolve', 897 | strategy: domain_strategy 898 | }); 899 | 900 | uci.foreach(uciconfig, uciroutingrule, (cfg) => { 901 | if (cfg.enabled !== '1') 902 | return null; 903 | 904 | push(config.route.rules, { 905 | ip_version: strToInt(cfg.ip_version), 906 | protocol: cfg.protocol, 907 | network: cfg.network, 908 | domain: cfg.domain, 909 | domain_suffix: cfg.domain_suffix, 910 | domain_keyword: cfg.domain_keyword, 911 | domain_regex: cfg.domain_regex, 912 | source_ip_cidr: cfg.source_ip_cidr, 913 | source_ip_is_private: strToBool(cfg.source_ip_is_private), 914 | ip_cidr: cfg.ip_cidr, 915 | ip_is_private: strToBool(cfg.ip_is_private), 916 | source_port: parse_port(cfg.source_port), 917 | source_port_range: cfg.source_port_range, 918 | port: parse_port(cfg.port), 919 | port_range: cfg.port_range, 920 | process_name: cfg.process_name, 921 | process_path: cfg.process_path, 922 | process_path_regex: cfg.process_path_regex, 923 | user: cfg.user, 924 | rule_set: get_ruleset(cfg.rule_set), 925 | rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source), 926 | rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty), 927 | invert: strToBool(cfg.invert), 928 | action: cfg.action, 929 | outbound: get_outbound(cfg.outbound), 930 | override_address: cfg.override_address, 931 | override_port: strToInt(cfg.override_port), 932 | udp_disable_domain_unmapping: strToBool(cfg.udp_disable_domain_unmapping), 933 | udp_connect: strToBool(cfg.udp_connect), 934 | udp_timeout: strToTime(cfg.udp_timeout), 935 | tls_fragment: strToBool(cfg.tls_fragment), 936 | tls_fragment_fallback_delay: strToTime(cfg.tls_fragment_fallback_delay), 937 | tls_record_fragment: strToBool(cfg.tls_record_fragment) 938 | }); 939 | }); 940 | 941 | config.route.final = get_outbound(default_outbound); 942 | 943 | /* Rule set */ 944 | uci.foreach(uciconfig, uciruleset, (cfg) => { 945 | if (cfg.enabled !== '1') 946 | return null; 947 | 948 | push(config.route.rule_set, { 949 | type: cfg.type, 950 | tag: 'cfg-' + cfg['.name'] + '-rule', 951 | format: cfg.format, 952 | path: cfg.path, 953 | url: cfg.url, 954 | download_detour: get_outbound(cfg.outbound), 955 | update_interval: cfg.update_interval 956 | }); 957 | }); 958 | } 959 | /* Routing rules end */ 960 | 961 | /* Experimental start */ 962 | if (routing_mode in ['bypass_mainland_china', 'custom']) { 963 | config.experimental = { 964 | cache_file: { 965 | enabled: true, 966 | path: RUN_DIR + '/cache.db', 967 | store_rdrc: strToBool(cache_file_store_rdrc), 968 | rdrc_timeout: strToTime(cache_file_rdrc_timeout), 969 | } 970 | }; 971 | } 972 | /* Experimental end */ 973 | 974 | system('mkdir -p ' + RUN_DIR); 975 | writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config))); 976 | --------------------------------------------------------------------------------