├── 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.ut │ │ │ ├── update_resources.sh │ │ │ ├── generate_server.uc │ │ │ ├── homeproxy.uc │ │ │ ├── update_subscriptions.uc │ │ │ ├── firewall_post.ut │ │ │ └── generate_client.uc │ ├── nginx │ │ └── conf.d │ │ │ └── homeproxy.locations │ ├── capabilities │ │ └── homeproxy.json │ ├── uci-defaults │ │ ├── luci-homeproxy │ │ └── luci-homeproxy-migration │ ├── config │ │ └── homeproxy │ └── init.d │ │ └── homeproxy └── usr │ └── share │ ├── rpcd │ ├── acl.d │ │ └── luci-app-homeproxy.json │ └── ucode │ │ └── luci.homeproxy │ └── luci │ └── menu.d │ └── luci-app-homeproxy.json ├── README ├── Makefile ├── .github ├── workflows │ ├── rescan-translation.yml │ └── build-ipk.yml └── build-ipk.sh ├── docs ├── Ruleset-URI-Scheme.md └── css │ └── ClearnessDark.css ├── .prepare.sh ├── htdocs └── luci-static │ └── resources │ ├── view │ └── homeproxy │ │ ├── status.js │ │ └── ruleset.js │ └── homeproxy.js └── LICENSE /root/etc/homeproxy/resources/gfw_list.ver: -------------------------------------------------------------------------------- 1 | 202408212210 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip4.ver: -------------------------------------------------------------------------------- 1 | 20240822150002 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip6.ver: -------------------------------------------------------------------------------- 1 | 20240822150002 2 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_list.ver: -------------------------------------------------------------------------------- 1 | 202408212210 2 | -------------------------------------------------------------------------------- /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/nginx/conf.d/homeproxy.locations: -------------------------------------------------------------------------------- 1 | location /homeproxy/ { 2 | proxy_set_header Host $host; 3 | proxy_set_header X-Real-IP $remote_addr; 4 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 5 | proxy_set_header X-Forwarded-Proto $scheme; 6 | proxy_pass http://localhost:9090/; 7 | proxy_redirect default; 8 | proxy_set_header Upgrade $http_upgrade; 9 | proxy_set_header Connection "upgrade"; 10 | proxy_http_version 1.1; 11 | add_header Cache-Control no-cache; 12 | } 13 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Recently, sagernet blocked my account unilaterally and without any reason. 2 | So this fork will not continue to provide support for new SB features in the future. 3 | Goodbye. everyone. 4 | 5 | For developers: 6 | The code of the dev/* branch has supported new SB features up to 1.10.0-beta.3. 7 | You can merge it yourself if necessary. 8 | 9 | TODO: 10 | - Subscription page slow response with a large number of nodes 11 | - Refactor nft rules 12 | - Support Clash selector, urltest etc. 13 | - Move ACL settings to a dedicated page 14 | - Any other improvements 15 | -------------------------------------------------------------------------------- /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="10" #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 | -------------------------------------------------------------------------------- /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/etc/uci-defaults/luci-homeproxy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | uci -q batch <<-EOF >"/dev/null" 4 | delete firewall.homeproxy_pre 5 | set firewall.homeproxy_pre=include 6 | set firewall.homeproxy_pre.type=nftables 7 | set firewall.homeproxy_pre.path="/var/run/homeproxy/fw4_pre.nft" 8 | set firewall.homeproxy_pre.position="table-pre" 9 | 10 | delete firewall.homeproxy_post 11 | set firewall.homeproxy_post=include 12 | set firewall.homeproxy_post.type=nftables 13 | set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft" 14 | set firewall.homeproxy_post.position="table-post" 15 | commit firewall 16 | EOF 17 | 18 | [ -z "$(uci -q get homeproxy.experimental)" ] && uci set homeproxy.experimental=homeproxy && uci commit homeproxy 19 | 20 | exit 0 21 | -------------------------------------------------------------------------------- /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 | "/etc/init.d/homeproxy reload *": [ "exec" ], 8 | "/var/run/homeproxy/homeproxy.log": [ "read" ], 9 | "/var/run/homeproxy/sing-box-c.log": [ "read" ], 10 | "/var/run/homeproxy/sing-box-s.log": [ "read" ] 11 | }, 12 | "ubus": { 13 | "service": [ "list" ], 14 | "luci.homeproxy": [ "*" ] 15 | }, 16 | "uci": [ "homeproxy" ] 17 | }, 18 | "write": { 19 | "file": { 20 | "/tmp/homeproxy_certificate.tmp": [ "write" ] 21 | }, 22 | "uci": [ "homeproxy" ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | +chinadns-ng \ 12 | +firewall4 \ 13 | +kmod-nft-tproxy \ 14 | +unzip 15 | 16 | PKG_NAME:=luci-app-homeproxy 17 | 18 | define Package/luci-app-homeproxy/conffiles 19 | /etc/config/homeproxy 20 | /etc/homeproxy/certs/ 21 | /etc/homeproxy/ruleset/ 22 | /etc/homeproxy/resources/direct_list.txt 23 | /etc/homeproxy/resources/proxy_list.txt 24 | /etc/homeproxy/resources/clash_dashboard.ver 25 | /etc/homeproxy/resources/*.zip 26 | /etc/homeproxy/cache.db 27 | endef 28 | 29 | PKG_UNPACK=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR) 30 | 31 | include $(TOPDIR)/feeds/luci/luci.mk 32 | 33 | # call BuildPackage - OpenWrt buildroot signature 34 | -------------------------------------------------------------------------------- /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/ruleset": { 30 | "title": "Ruleset Settings", 31 | "order": 18, 32 | "action": { 33 | "type": "view", 34 | "path": "homeproxy/ruleset" 35 | } 36 | }, 37 | "admin/services/homeproxy/server": { 38 | "title": "Server Settings", 39 | "order": 20, 40 | "action": { 41 | "type": "view", 42 | "path": "homeproxy/server" 43 | } 44 | }, 45 | "admin/services/homeproxy/status": { 46 | "title": "Service Status", 47 | "order": 30, 48 | "action": { 49 | "type": "view", 50 | "path": "homeproxy/status" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /root/etc/uci-defaults/luci-homeproxy-migration: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | china_dns_server="$(uci -q get "homeproxy.config.china_dns_server")" 4 | if [ "$china_dns_server" = "wan_114" ]; then 5 | uci -q delete "homeproxy.config.china_dns_server" 6 | uci -q add_list "homeproxy.config.china_dns_server"="wan" 7 | uci -q add_list "homeproxy.config.china_dns_server"="114.114.114.114" 8 | elif echo "$china_dns_server" | grep -q ","; then 9 | uci -q delete "homeproxy.config.china_dns_server" 10 | for dns in ${china_dns_server//,/ }; do 11 | uci -q add_list "homeproxy.config.china_dns_server"="$dns" 12 | done 13 | fi 14 | # rm Subscription Name-s 15 | subscription_urls="$(uci -q get "homeproxy.subscription.subscription_url")" 16 | subscription_names="$(uci -q get "homeproxy.subscription.subscription_name")" 17 | if [ -n "$subscription_names" ]; then 18 | uci -q delete "homeproxy.subscription.subscription_url" 19 | uci -q delete "homeproxy.subscription.subscription_name" 20 | i=1 21 | for suburl in $subscription_urls; do 22 | uci -q add_list "homeproxy.subscription.subscription_url"="${suburl}#$(echo "$subscription_names" | cut -f$i -d' ')" 23 | let i++ 24 | done 25 | fi 26 | [ -z "$(uci -q changes "homeproxy")" ] || uci -q commit "homeproxy" 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /.github/workflows/rescan-translation.yml: -------------------------------------------------------------------------------- 1 | name: Rescan translation 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - 'htdocs/**' 9 | - 'root/**' 10 | 11 | jobs: 12 | scan: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - name: Checkout source tree 19 | uses: actions/checkout@v4 20 | 21 | - name: Rescan translation 22 | env: 23 | DEBIAN_FRONTEND: noninteractive 24 | run: | 25 | sudo -E apt-get -y install gettext 26 | curl -LO "https://github.com/openwrt/luci/raw/master/build/i18n-scan.pl" 27 | curl -LO "https://github.com/openwrt/luci/raw/master/build/i18n-update.pl" 28 | perl "i18n-scan.pl" . > "po/templates/homeproxy.pot" 29 | perl "i18n-update.pl" "po" 30 | find po/ -name '*.po~' -exec rm -f {} \; 31 | rm -f "i18n-scan.pl" "i18n-update.pl" 32 | [ -z "$(git status -s)" ] || echo -e "CHANGE_STAT=1" >> "$GITHUB_ENV" 33 | 34 | - name: Commit changes 35 | if: ${{ env.CHANGE_STAT }} 36 | run: | 37 | git config --local user.name "github-actions[bot]" 38 | git config --local user.email "<41898282+github-actions[bot]@users.noreply.github.com>" 39 | git add . 40 | git commit -m "chore(po): rescan translation" 41 | git push -f origin HEAD:po 42 | -------------------------------------------------------------------------------- /.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 | 15 | pull_request: 16 | branches: 17 | - 'master' 18 | - 'dev' 19 | types: 20 | - opened 21 | - synchronize 22 | - reopened 23 | paths: 24 | - 'htdocs/**' 25 | - 'root/**' 26 | - 'Makefile' 27 | - '.github/**' 28 | 29 | release: 30 | types: 31 | - published 32 | 33 | jobs: 34 | build: 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Checkout source tree 39 | uses: actions/checkout@v4 40 | 41 | - name: Build ipk file 42 | env: 43 | RELEASE_TYPE: ${{ github.event_name }} 44 | run: | 45 | pushd .github 46 | fakeroot bash build-ipk.sh 47 | echo "ASSET_NAME=$(ls *.ipk)" >> $GITHUB_ENV 48 | popd 49 | 50 | - name: Publishing to GitHub Artifacts 51 | uses: actions/upload-artifact@v4 52 | if: github.event_name != 'release' 53 | with: 54 | name: ${{ env.ASSET_NAME }} 55 | path: .github/*.ipk 56 | 57 | - name: Publishing to GitHub Releases 58 | uses: svenstaro/upload-release-action@v2 59 | if: github.event_name == 'release' 60 | with: 61 | repo_token: ${{ github.token }} 62 | file: .github/*.ipk 63 | tag: ${{ github.ref }} 64 | file_glob: true 65 | -------------------------------------------------------------------------------- /docs/Ruleset-URI-Scheme.md: -------------------------------------------------------------------------------- 1 | # Import rule-set links format 2 | 3 | ## Structure 4 | 5 | **remote:** `http[s]://[auth@]?file=[&key=value][#label]` 6 | **local:** `file://[host]?file=[&key=value][#label]` 7 | **inline:** `inline://[#label]` 8 | 9 | ## Components 10 | 11 | ### Scheme 12 | 13 | Can be `http` or `https` or `file` or `inline`. 14 | 15 | ### Auth 16 | 17 | Add it only if required by the target host. 18 | 19 | ### Host 20 | 21 | The format is `hostname[:port]`. 22 | `hostname` can be **Domain** or **IP Address**. 23 | `:port` is optional, add it only if required by the target host. 24 | 25 | ### Path 26 | 27 | The shortest format is `/`. 28 | 29 | ### Base64edJsonStr 30 | 31 | Generation steps: 32 | 33 | 1. Base64 encode **Headless Rule** `.rules`. 34 | 2. Replace all `+` with `-` and all `/` with `_` in base64 string. 35 | 3. Remove all `=` from the EOF the base64 string. 36 | 37 | ### QueryParameters 38 | 39 | + `file`: Required. Available values ​​refer to **Rulefmt**. 40 | + `rawquery`: Optional. Available values ​​refer to **rawQuery**. 41 | 42 | #### Rulefmt 43 | 44 | Can be `json` or `srs`. Rule file format. 45 | 46 | #### rawQuery 47 | 48 | This parameter is required if the original link contains a url query. 49 | Encrypt the part `key1=value1&key2=value2` after `?` in the original link with `encodeURIComponent` and use it as the payload of this parameter. 50 | 51 | ### URIFragment 52 | 53 | Ruleset label. Empty strings are not recommended. 54 | Need encoded by `encodeURIComponent`. 55 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/firewall_pre.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl -S 2 | 3 | {%- 4 | import { cursor } from 'uci'; 5 | 6 | const cfgname = 'homeproxy'; 7 | const uci = cursor(); 8 | uci.load(cfgname); 9 | 10 | const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china', 11 | proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy'; 12 | 13 | let outbound_node, tun_name; 14 | if (match(proxy_mode, /tun/)) { 15 | if (routing_mode === 'custom') 16 | outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil'; 17 | else 18 | outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil'; 19 | 20 | if (outbound_node !== 'nil') 21 | tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0'; 22 | } 23 | 24 | const server_enabled = uci.get(cfgname, 'server', 'enabled'); 25 | let auto_firewall = '0'; 26 | if (server_enabled === '1') 27 | auto_firewall = uci.get(cfgname, 'server', 'auto_firewall') || '0'; 28 | 29 | -%} 30 | 31 | {% if (tun_name): %} 32 | chain forward { 33 | oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward" 34 | } 35 | {% endif %} 36 | 37 | {% if (tun_name || auto_firewall === '1'): %} 38 | chain input { 39 | {% if (tun_name): %} 40 | iifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun input" 41 | {% endif %} 42 | {% 43 | if (auto_firewall === '1') 44 | uci.foreach(cfgname, 'server', (s) => { 45 | if (s.enabled !== '1') 46 | return; 47 | 48 | let proto = s.network || '{ tcp, udp }'; 49 | printf(' meta l4proto %s th dport %s counter accept comment "!%s: accept server %s"\n', 50 | proto, s.port, cfgname, s['.name']); 51 | }); 52 | %} 53 | } 54 | {% endif %} 55 | -------------------------------------------------------------------------------- /.prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PKG_NAME="$1" 3 | CURDIR="$2" 4 | PKG_BUILD_DIR="$3" 5 | PKG_BUILD_BIN="$PKG_BUILD_DIR/bin" 6 | export PATH="$PATH:$PKG_BUILD_BIN" 7 | OS=linux 8 | ARCH=amd64 9 | JQVERSION=1.7.1 10 | DOCNAME=Ruleset-URI-Scheme 11 | 12 | mkdir -p "$PKG_BUILD_BIN" 13 | curl -L "https://github.com/jqlang/jq/releases/download/jq-${JQVERSION}/jq-${OS}-${ARCH}" -o "$PKG_BUILD_BIN"/jq 14 | chmod +x "$PKG_BUILD_BIN"/jq 15 | latest="$(curl -L https://api.github.com/repos/kpym/gm/releases/latest | jq -rc '.tag_name' 2>/dev/null)" 16 | curl -L "https://github.com/kpym/gm/releases/download/${latest}/gm_${latest#v}_Linux_64bit.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN" 17 | latest="$(curl -L https://api.github.com/repos/tdewolff/minify/releases/latest | jq -rc '.tag_name' 2>/dev/null)" 18 | curl -L "https://github.com/tdewolff/minify/releases/download/${latest}/minify_${OS}_${ARCH}.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN" 19 | chmod -R +x "$PKG_BUILD_BIN" 20 | 21 | cp "$CURDIR"/docs/$DOCNAME.md "$PKG_BUILD_DIR" 22 | pushd "$PKG_BUILD_DIR" 23 | gm $DOCNAME.md 24 | p=$(sed -n '/github.min.css/=' $DOCNAME.html) 25 | { 26 | head -n$(( $p -1 )) $DOCNAME.html 27 | echo '' 30 | tail -n +$(( $p +1 )) $DOCNAME.html 31 | } > buildin.html 32 | popd 33 | minify "$PKG_BUILD_DIR"/buildin.html | base64 | tr -d '\n' > "$PKG_BUILD_DIR"/base64 34 | sed -i "s|'cmxzdHBsYWNlaG9sZGVy'|'$(cat "$PKG_BUILD_DIR"/base64)'|" "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/homeproxy/ruleset.js 35 | 36 | if [ -d "$CURDIR/.git" ]; then 37 | config="$CURDIR/.git/config" 38 | else 39 | config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")" 40 | fi 41 | [ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && { 42 | for d in luasrc ucode htdocs root src; do 43 | rm -rf "$PKG_BUILD_DIR"/$d 44 | done 45 | mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view 46 | touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js 47 | mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d 48 | touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json 49 | } 50 | exit 0 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,853,873,993,995,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 china_dns_port '5334' 10 | option udp_timeout '' 11 | option tun_name 'singtun0' 12 | option tun_addr4 '172.19.0.1/30' 13 | option tun_addr6 'fdfe:dcba:9876::1/126' 14 | option tun_mtu '9000' 15 | option tun_gso '0' 16 | option table_mark '100' 17 | option self_mark '100' 18 | option tproxy_mark '101' 19 | option tun_mark '102' 20 | 21 | config homeproxy 'config' 22 | option main_node 'nil' 23 | option main_udp_node 'same' 24 | option dns_server '8.8.8.8' 25 | option routing_mode 'bypass_mainland_china' 26 | option routing_port 'common' 27 | option proxy_mode 'redirect_tproxy' 28 | option ipv6_support '1' 29 | 30 | config homeproxy 'experimental' 31 | option clash_api_port '9090' 32 | 33 | config homeproxy 'control' 34 | option lan_proxy_mode 'disabled' 35 | list wan_proxy_ipv4_ips '91.105.192.0/23' 36 | list wan_proxy_ipv4_ips '91.108.4.0/22' 37 | list wan_proxy_ipv4_ips '91.108.8.0/22' 38 | list wan_proxy_ipv4_ips '91.108.16.0/22' 39 | list wan_proxy_ipv4_ips '91.108.12.0/22' 40 | list wan_proxy_ipv4_ips '91.108.20.0/22' 41 | list wan_proxy_ipv4_ips '91.108.56.0/22' 42 | list wan_proxy_ipv4_ips '149.154.160.0/20' 43 | list wan_proxy_ipv4_ips '185.76.151.0/24' 44 | list wan_proxy_ipv6_ips '2001:67c:4e8::/48' 45 | list wan_proxy_ipv6_ips '2001:b28:f23c::/48' 46 | list wan_proxy_ipv6_ips '2001:b28:f23d::/48' 47 | list wan_proxy_ipv6_ips '2001:b28:f23f::/48' 48 | list wan_proxy_ipv6_ips '2a0a:f280::/32' 49 | 50 | config homeproxy 'routing' 51 | option sniff_override '1' 52 | option default_outbound 'direct-out' 53 | 54 | config homeproxy 'dns' 55 | option dns_strategy 'prefer_ipv4' 56 | option default_server 'local-dns' 57 | option disable_cache '0' 58 | option disable_cache_expire '0' 59 | 60 | config homeproxy 'subscription' 61 | option auto_update '0' 62 | option allow_insecure '0' 63 | option packet_encoding 'xudp' 64 | option update_via_proxy '0' 65 | option filter_nodes 'blacklist' 66 | list filter_keywords '重置|到期|过期|剩余|套餐' 67 | list filter_keywords 'Expiration|Remaining' 68 | 69 | config homeproxy 'server' 70 | option enabled '0' 71 | option auto_firewall '0' 72 | 73 | config dns_rule 'nodes_domain' 74 | option label 'NodesDomain' 75 | option enabled '1' 76 | option mode 'default' 77 | list outbound 'any-out' 78 | option server 'default-dns' 79 | 80 | -------------------------------------------------------------------------------- /.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 | if grep -q '^darwin*' <<< "$OSTYPE"; then 10 | export PATH="$(brew --prefix)/opt/coreutils/libexec/gnubin:$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$(brew --prefix)/opt/findutils/libexec/gnubin:$(brew --prefix)/opt/gnu-tar/libexec/gnubin:$PATH" 11 | fi 12 | 13 | export PKG_SOURCE_DATE_EPOCH="$(date "+%s")" 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="dev-$PKG_SOURCE_DATE_EPOCH-$(git rev-parse --short HEAD)" 27 | fi 28 | 29 | TEMP_DIR="$(mktemp -d -p $BASE_DIR 2>/dev/null || mktemp -d)" 30 | TEMP_PKG_DIR="$TEMP_DIR/$PKG_NAME" 31 | mkdir -p "$TEMP_PKG_DIR/CONTROL/" 32 | mkdir -p "$TEMP_PKG_DIR/lib/upgrade/keep.d/" 33 | mkdir -p "$TEMP_PKG_DIR/usr/lib/lua/luci/i18n/" 34 | mkdir -p "$TEMP_PKG_DIR/www/" 35 | 36 | cp -fpR "$PKG_DIR/htdocs"/* "$TEMP_PKG_DIR/www/" 37 | cp -fpR "$PKG_DIR/root"/* "$TEMP_PKG_DIR/" 38 | 39 | echo -e "/etc/config/homeproxy" > "$TEMP_PKG_DIR/CONTROL/conffiles" 40 | cat > "$TEMP_PKG_DIR/lib/upgrade/keep.d/$PKG_NAME" <<-EOF 41 | /etc/homeproxy/certs/ 42 | /etc/homeproxy/ruleset/ 43 | /etc/homeproxy/resources/direct_list.txt 44 | /etc/homeproxy/resources/proxy_list.txt 45 | EOF 46 | 47 | cat > "$TEMP_PKG_DIR/CONTROL/control" <<-EOF 48 | Package: $PKG_NAME 49 | Version: $PKG_VERSION 50 | Depends: libc, sing-box, chinadns-ng, firewall4, kmod-nft-tproxy, unzip 51 | Source: https://github.com/immortalwrt/homeproxy 52 | SourceName: $PKG_NAME 53 | Section: luci 54 | SourceDateEpoch: $PKG_SOURCE_DATE_EPOCH 55 | Maintainer: Tianling Shen 56 | Architecture: all 57 | Installed-Size: TO-BE-FILLED-BY-IPKG-BUILD 58 | Description: The modern ImmortalWrt proxy platform for ARM64/AMD64 59 | EOF 60 | 61 | echo -e '#!/bin/sh 62 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 63 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 64 | . ${IPKG_INSTROOT}/lib/functions.sh 65 | default_postinst $0 $@' > "$TEMP_PKG_DIR/CONTROL/postinst" 66 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/postinst" 67 | 68 | echo -e "[ -n "\${IPKG_INSTROOT}" ] || { 69 | (. /etc/uci-defaults/$PKG_NAME) && rm -f /etc/uci-defaults/$PKG_NAME 70 | rm -f /tmp/luci-indexcache 71 | rm -rf /tmp/luci-modulecache/ 72 | exit 0 73 | }" > "$TEMP_PKG_DIR/CONTROL/postinst-pkg" 74 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/postinst-pkg" 75 | 76 | echo -e '#!/bin/sh 77 | [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 78 | . ${IPKG_INSTROOT}/lib/functions.sh 79 | default_prerm $0 $@' > "$TEMP_PKG_DIR/CONTROL/prerm" 80 | chmod 0755 "$TEMP_PKG_DIR/CONTROL/prerm" 81 | 82 | curl -fsSL "https://raw.githubusercontent.com/openwrt/openwrt/master/scripts/ipkg-build" -o "$TEMP_DIR/ipkg-build" 83 | chmod 0755 "$TEMP_DIR/ipkg-build" 84 | "$TEMP_DIR/ipkg-build" -m "" "$TEMP_PKG_DIR" "$TEMP_DIR" 85 | 86 | mv "$TEMP_DIR/${PKG_NAME}_${PKG_VERSION}_all.ipk" "$BASE_DIR/${PKG_NAME}_${PKG_VERSION}_all.ipk" 87 | rm -rf "$TEMP_DIR" 88 | -------------------------------------------------------------------------------- /docs/css/ClearnessDark.css: -------------------------------------------------------------------------------- 1 | h1, 2 | h2, 3 | h3, 4 | h4, 5 | h5, 6 | h6, 7 | p, 8 | blockquote { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | body { 13 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; 14 | font-size: 13px; 15 | line-height: 18px; 16 | color: #fff; 17 | background-color: #282a36; 18 | margin: 10px 13px 10px 13px; 19 | } 20 | a { 21 | color: #59acf3; 22 | } 23 | a:hover { 24 | color: #a7d8ff; 25 | text-decoration: none; 26 | } 27 | a img { 28 | border: none; 29 | } 30 | p { 31 | margin-bottom: 9px; 32 | } 33 | h1, 34 | h2, 35 | h3, 36 | h4, 37 | h5, 38 | h6 { 39 | color: #fff; 40 | line-height: 36px; 41 | } 42 | h1 { 43 | margin-bottom: 18px; 44 | font-size: 30px; 45 | } 46 | h2 { 47 | font-size: 24px; 48 | } 49 | h3 { 50 | font-size: 18px; 51 | } 52 | h4 { 53 | font-size: 16px; 54 | } 55 | h5 { 56 | font-size: 14px; 57 | } 58 | h6 { 59 | font-size: 13px; 60 | } 61 | hr { 62 | margin: 0 0 19px; 63 | border: 0; 64 | border-bottom: 1px solid #ccc; 65 | } 66 | blockquote { 67 | padding: 13px 13px 21px 15px; 68 | margin-bottom: 18px; 69 | font-family:georgia,serif; 70 | font-style: italic; 71 | } 72 | blockquote:before { 73 | content:"\201C"; 74 | font-size:40px; 75 | margin-left:-10px; 76 | font-family:georgia,serif; 77 | color:#eee; 78 | } 79 | blockquote p { 80 | font-size: 14px; 81 | font-weight: 300; 82 | line-height: 18px; 83 | margin-bottom: 0; 84 | font-style: italic; 85 | } 86 | code, pre { 87 | font-family: Monaco, Andale Mono, Courier New, monospace; 88 | } 89 | code { 90 | color: #ff4a14; 91 | padding: 1px 3px; 92 | font-size: 12px; 93 | -webkit-border-radius: 3px; 94 | -moz-border-radius: 3px; 95 | border-radius: 3px; 96 | } 97 | pre { 98 | display: block; 99 | padding: 14px; 100 | margin: 0 0 18px; 101 | line-height: 16px; 102 | font-size: 11px; 103 | border: 1px solid #bf370f; 104 | white-space: pre; 105 | white-space: pre-wrap; 106 | word-wrap: break-word; 107 | } 108 | pre code { 109 | background-color: #282a36; 110 | color: #ff4a14; 111 | font-size: 11px; 112 | padding: 0; 113 | } 114 | @media screen and (min-width: 768px) { 115 | body { 116 | width: 748px; 117 | margin:10px auto; 118 | } 119 | } 120 | 121 | /** 122 | * obsidian.css 123 | * Obsidian style 124 | * ported by Alexander Marenin (http://github.com/ioncreature) 125 | */ 126 | 127 | .hljs { 128 | display: block; 129 | overflow-x: auto; 130 | padding: 0.5em; 131 | background: #282b2e; 132 | } 133 | 134 | .hljs-keyword, 135 | .hljs-selector-tag, 136 | .hljs-literal, 137 | .hljs-selector-id { 138 | color: #93c763; 139 | } 140 | 141 | .hljs-number { 142 | color: #ffcd22; 143 | } 144 | 145 | .hljs { 146 | color: #e0e2e4; 147 | } 148 | 149 | .hljs-attribute { 150 | color: #668bb0; 151 | } 152 | 153 | .hljs-code, 154 | .hljs-class .hljs-title, 155 | .hljs-section { 156 | color: white; 157 | } 158 | 159 | .hljs-regexp, 160 | .hljs-link { 161 | color: #d39745; 162 | } 163 | 164 | .hljs-meta { 165 | color: #557182; 166 | } 167 | 168 | .hljs-tag, 169 | .hljs-name, 170 | .hljs-bullet, 171 | .hljs-subst, 172 | .hljs-emphasis, 173 | .hljs-type, 174 | .hljs-built_in, 175 | .hljs-selector-attr, 176 | .hljs-selector-pseudo, 177 | .hljs-addition, 178 | .hljs-variable, 179 | .hljs-template-tag, 180 | .hljs-template-variable { 181 | color: #8cbbad; 182 | } 183 | 184 | .hljs-string, 185 | .hljs-symbol { 186 | color: #ec7600; 187 | } 188 | 189 | .hljs-comment, 190 | .hljs-quote, 191 | .hljs-deletion { 192 | color: #818e96; 193 | } 194 | 195 | .hljs-selector-class { 196 | color: #A082BD 197 | } 198 | 199 | .hljs-keyword, 200 | .hljs-selector-tag, 201 | .hljs-literal, 202 | .hljs-doctag, 203 | .hljs-title, 204 | .hljs-section, 205 | .hljs-type, 206 | .hljs-name, 207 | .hljs-strong { 208 | font-weight: bold; 209 | } 210 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/update_resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # 4 | # Copyright (C) 2022-2023 ImmortalWrt.org 5 | 6 | . /usr/share/libubox/jshn.sh 7 | 8 | NAME="homeproxy" 9 | 10 | RESOURCES_DIR="/etc/$NAME/resources" 11 | mkdir -p "$RESOURCES_DIR" 12 | 13 | RUN_DIR="/var/run/$NAME" 14 | LOG_PATH="$RUN_DIR/$NAME.log" 15 | mkdir -p "$RUN_DIR" 16 | 17 | log() { 18 | echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH" 19 | } 20 | 21 | set_lock() { 22 | local act="$1" 23 | local type="$2" 24 | 25 | local lock="$RUN_DIR/update_resources-$type.lock" 26 | if [ "$act" = "set" ]; then 27 | if [ -e "$lock" ]; then 28 | log "[$(to_upper "$type")] A task is already running." 29 | exit 2 30 | else 31 | touch "$lock" 32 | fi 33 | elif [ "$act" = "remove" ]; then 34 | rm -f "$lock" 35 | fi 36 | } 37 | 38 | to_upper() { 39 | echo -e "$1" | tr "[a-z]" "[A-Z]" 40 | } 41 | 42 | get_local_vers() { 43 | local ver_file="$1" 44 | local repoid="$2" 45 | 46 | local ver="$(eval "jsonfilter -qi \"$ver_file\" -e '@[\"$repoid\"].version'")" 47 | [ -n "$ver" ] && echo "$ver" || return 1 48 | } 49 | 50 | check_clash_dashboard_update() { 51 | local dashtype="$1" 52 | local dashrepo="$2" 53 | local dashrepoid="$(echo -n "$dashrepo" | md5sum | cut -f1 -d' ')" 54 | local wget="wget --timeout=10 -q" 55 | 56 | set_lock "set" "$dashtype" 57 | 58 | local dashdata_ver="$($wget -O- "https://api.github.com/repos/$dashrepo/releases/latest" | jsonfilter -e "@.tag_name")" 59 | [ -n "$dashdata_ver" ] || { 60 | dashdata_ver="$($wget -O- "https://api.github.com/repos/$dashrepo/tags" | jsonfilter -e "@[*].name" | head -n1)" 61 | } 62 | if [ -z "$dashdata_ver" ]; then 63 | log "[$(to_upper "$dashtype")] [$dashrepo] Failed to get the latest version, please retry later." 64 | 65 | set_lock "remove" "$dashtype" 66 | return 1 67 | fi 68 | 69 | local local_dashdata_ver="$(get_local_vers "$RESOURCES_DIR/$dashtype.ver" "$dashrepoid" || echo "NOT FOUND")" 70 | if [ "$local_dashdata_ver" = "$dashdata_ver" ]; then 71 | log "[$(to_upper "$dashtype")] [$dashrepo] Current version: $dashdata_ver." 72 | log "[$(to_upper "$dashtype")] [$dashrepo] You're already at the latest version." 73 | 74 | set_lock "remove" "$dashtype" 75 | return 3 76 | else 77 | log "[$(to_upper "$dashtype")] [$dashrepo] Local version: $local_dashdata_ver, latest version: $dashdata_ver." 78 | fi 79 | 80 | $wget "https://codeload.github.com/$dashrepo/zip/refs/heads/gh-pages" -O "$RUN_DIR/$dashtype.zip" 81 | if [ ! -s "$RUN_DIR/$dashtype.zip" ]; then 82 | rm -f "$RUN_DIR/$dashtype.zip" 83 | log "[$(to_upper "$dashtype")] [$dashrepo] Update failed." 84 | 85 | set_lock "remove" "$dashtype" 86 | return 1 87 | fi 88 | 89 | mv -f "$RUN_DIR/$dashtype.zip" "$RESOURCES_DIR/${dashrepo//\//_}.zip" 90 | touch "$RESOURCES_DIR/$dashtype.ver" 91 | json_init 92 | json_load_file "$RESOURCES_DIR/$dashtype.ver" 93 | json_select "$dashrepoid" 2>/dev/null || json_add_object "$dashrepoid" 94 | json_add_string repo "$dashrepo" 95 | json_add_string version "$dashdata_ver" 96 | json_dump > "$RESOURCES_DIR/$dashtype.ver" 97 | log "[$(to_upper "$dashtype")] [$dashrepo] Successfully updated." 98 | 99 | set_lock "remove" "$dashtype" 100 | return 0 101 | } 102 | 103 | check_list_update() { 104 | local listtype="$1" 105 | local listrepo="$2" 106 | local listref="$3" 107 | local listname="$4" 108 | local wget="wget --timeout=10 -q" 109 | 110 | set_lock "set" "$listtype" 111 | 112 | local list_info="$($wget -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname")" 113 | local list_sha="$(echo -e "$list_info" | jsonfilter -e "@[0].sha")" 114 | local list_ver="$(echo -e "$list_info" | jsonfilter -e "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')" 115 | if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then 116 | log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later." 117 | 118 | set_lock "remove" "$listtype" 119 | return 1 120 | fi 121 | 122 | local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")" 123 | if [ "$local_list_ver" = "$list_ver" ]; then 124 | log "[$(to_upper "$listtype")] Current version: $list_ver." 125 | log "[$(to_upper "$listtype")] You're already at the latest version." 126 | 127 | set_lock "remove" "$listtype" 128 | return 3 129 | else 130 | log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver." 131 | fi 132 | 133 | $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" 134 | if [ ! -s "$RUN_DIR/$listname" ]; then 135 | rm -f "$RUN_DIR/$listname" 136 | log "[$(to_upper "$listtype")] Update failed." 137 | 138 | set_lock "remove" "$listtype" 139 | return 1 140 | fi 141 | 142 | mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}" 143 | echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver" 144 | log "[$(to_upper "$listtype")] Successfully updated." 145 | 146 | set_lock "remove" "$listtype" 147 | return 0 148 | } 149 | 150 | case "$1" in 151 | "clash_dashboard") 152 | check_clash_dashboard_update "$1" "$2" 153 | ;; 154 | "china_ip4") 155 | check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt" 156 | ;; 157 | "china_ip6") 158 | check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt" 159 | ;; 160 | "gfw_list") 161 | check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt" 162 | ;; 163 | "china_list") 164 | check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \ 165 | sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt" 166 | ;; 167 | *) 168 | echo -e "Usage: $0 " 169 | exit 1 170 | ;; 171 | esac 172 | -------------------------------------------------------------------------------- /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 { readfile, writefile } from 'fs'; 11 | import { cursor } from 'uci'; 12 | 13 | import { 14 | executeCommand, isEmpty, strToBool, strToInt, 15 | removeBlankAttrs, validateHostname, validation, 16 | HP_DIR, RUN_DIR 17 | } from 'homeproxy'; 18 | 19 | /* UCI config start */ 20 | const uci = cursor(); 21 | 22 | const uciconfig = 'homeproxy'; 23 | uci.load(uciconfig); 24 | 25 | const uciserver = 'server'; 26 | 27 | const config = {}; 28 | 29 | /* Log */ 30 | config.log = { 31 | disabled: false, 32 | level: 'warn', 33 | output: RUN_DIR + '/sing-box-s.log', 34 | timestamp: true 35 | }; 36 | 37 | config.inbounds = []; 38 | 39 | uci.foreach(uciconfig, uciserver, (cfg) => { 40 | if (cfg.enabled !== '1') 41 | return; 42 | 43 | push(config.inbounds, { 44 | type: cfg.type, 45 | tag: 'cfg-' + cfg['.name'] + '-in', 46 | 47 | listen: cfg.address || '::', 48 | listen_port: strToInt(cfg.port), 49 | tcp_fast_open: strToBool(cfg.tcp_fast_open), 50 | tcp_multi_path: strToBool(cfg.tcp_multi_path), 51 | udp_fragment: strToBool(cfg.udp_fragment), 52 | udp_timeout: cfg.udp_timeout ? (cfg.udp_timeout + 's') : null, 53 | sniff: true, 54 | sniff_override_destination: (cfg.sniff_override === '1'), 55 | domain_strategy: cfg.domain_strategy, 56 | network: cfg.network, 57 | 58 | /* Hysteria */ 59 | up_mbps: strToInt(cfg.hysteria_up_mbps), 60 | down_mbps: strToInt(cfg.hysteria_down_mbps), 61 | obfs: cfg.hysteria_obfs_type ? { 62 | type: cfg.hysteria_obfs_type, 63 | password: cfg.hysteria_obfs_password 64 | } : cfg.hysteria_obfs_password, 65 | recv_window_conn: strToInt(cfg.hysteria_recv_window_conn), 66 | recv_window_client: strToInt(cfg.hysteria_revc_window_client), 67 | max_conn_client: strToInt(cfg.hysteria_max_conn_client), 68 | disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery), 69 | ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth), 70 | masquerade: cfg.hysteria_masquerade, 71 | 72 | /* Shadowsocks */ 73 | method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null, 74 | password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null, 75 | 76 | /* Tuic */ 77 | congestion_control: cfg.tuic_congestion_control, 78 | auth_timeout: cfg.tuic_auth_timeout ? (cfg.tuic_auth_timeout + 's') : null, 79 | zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt), 80 | heartbeat: cfg.tuic_heartbeat ? (cfg.tuic_heartbeat + 's') : null, 81 | 82 | /* HTTP / Hysteria (2) / Socks / Trojan / Tuic / VLESS / VMess */ 83 | users: (cfg.type !== 'shadowsocks') ? [ 84 | { 85 | name: !(cfg.type in ['http', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null, 86 | username: cfg.username, 87 | password: cfg.password, 88 | 89 | /* Hysteria */ 90 | auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null, 91 | auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null, 92 | 93 | /* Tuic */ 94 | uuid: cfg.uuid, 95 | 96 | /* VLESS / VMess */ 97 | flow: cfg.vless_flow, 98 | alterId: strToInt(cfg.vmess_alterid) 99 | } 100 | ] : null, 101 | 102 | multiplex: (cfg.multiplex === '1') ? { 103 | enabled: true, 104 | padding: (cfg.multiplex_padding === '1'), 105 | brutal: (cfg.multiplex_brutal === '1') ? { 106 | enabled: true, 107 | up_mbps: strToInt(cfg.multiplex_brutal_up), 108 | down_mbps: strToInt(cfg.multiplex_brutal_down) 109 | } : null 110 | } : null, 111 | 112 | tls: (cfg.tls === '1') ? { 113 | enabled: true, 114 | server_name: cfg.tls_sni, 115 | alpn: cfg.tls_alpn, 116 | min_version: cfg.tls_min_version, 117 | max_version: cfg.tls_max_version, 118 | cipher_suites: cfg.tls_cipher_suites, 119 | certificate_path: cfg.tls_cert_path, 120 | key_path: cfg.tls_key_path, 121 | acme: (cfg.tls_acme === '1') ? { 122 | domain: cfg.tls_acme_domains, 123 | data_directory: HP_DIR + '/certs', 124 | default_server_name: cfg.tls_acme_dsn, 125 | email: cfg.tls_acme_email, 126 | provider: cfg.tls_acme_provider, 127 | disable_http_challenge: (cfg.tls_acme_dhc === '1'), 128 | disable_tls_alpn_challenge: (cfg.tls_acme_dtac === '1'), 129 | alternative_http_port: strToInt(cfg.tls_acme_ahp), 130 | alternative_tls_port: strToInt(cfg.tls_acme_atp), 131 | external_account: (cfg.tls_acme_external_account === '1') ? { 132 | key_id: cfg.tls_acme_ea_keyid, 133 | mac_key: cfg.tls_acme_ea_mackey 134 | } : null, 135 | dns01_challenge: (cfg.tls_dns01_challenge === '1') ? { 136 | provider: cfg.tls_dns01_provider, 137 | access_key_id: cfg.tls_dns01_ali_akid, 138 | access_key_secret: cfg.tls_dns01_ali_aksec, 139 | region_id: cfg.tls_dns01_ali_rid, 140 | api_token: cfg.tls_dns01_cf_api_token 141 | } : null 142 | } : null, 143 | reality: (cfg.tls_reality === '1') ? { 144 | enabled: true, 145 | private_key: cfg.tls_reality_private_key, 146 | short_id: cfg.tls_reality_short_id, 147 | max_time_difference: cfg.tls_reality_max_time_difference ? (cfg.max_time_difference + 's') : null, 148 | handshake: { 149 | server: cfg.tls_reality_server_addr, 150 | server_port: strToInt(cfg.tls_reality_server_port) 151 | } 152 | } : null 153 | } : null, 154 | 155 | transport: !isEmpty(cfg.transport) ? { 156 | type: cfg.transport, 157 | host: cfg.http_host || cfg.httpupgrade_host, 158 | path: cfg.http_path || cfg.ws_path, 159 | headers: cfg.ws_host ? { 160 | Host: cfg.ws_host 161 | } : null, 162 | method: cfg.http_method, 163 | max_early_data: strToInt(cfg.websocket_early_data), 164 | early_data_header_name: cfg.websocket_early_data_header, 165 | service_name: cfg.grpc_servicename, 166 | idle_timeout: cfg.http_idle_timeout ? (cfg.http_idle_timeout + 's') : null, 167 | ping_timeout: cfg.http_ping_timeout ? (cfg.http_ping_timeout + 's') : null 168 | } : null 169 | }); 170 | }); 171 | 172 | if (length(config.inbounds) === 0) 173 | exit(1); 174 | 175 | system('mkdir -p ' + RUN_DIR); 176 | writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config))); 177 | -------------------------------------------------------------------------------- /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 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { access, error, lstat, mkstemp, 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 = function(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_get_features: { 158 | call: function() { 159 | let features = {}; 160 | 161 | const fd = popen('/usr/bin/sing-box version'); 162 | if (fd) { 163 | for (let line = fd.read('line'); length(line); line = fd.read('line')) { 164 | let tags = match(trim(line), /Tags: (.*)/); 165 | if (!tags) 166 | continue; 167 | 168 | for (let i in split(tags[1], ',')) 169 | features[i] = true; 170 | } 171 | 172 | fd.close(); 173 | } 174 | 175 | features.hp_has_chinadns_ng = access('/usr/bin/chinadns-ng'); 176 | if (features.hp_has_chinadns_ng) 177 | features.hp_has_chinadns_ng_v2 = (system('/usr/bin/chinadns-ng --version | grep -q "target:"') === 0); 178 | features.hp_has_ip_full = access('/usr/libexec/ip-full'); 179 | features.hp_has_tcp_brutal = hasKernelModule('brutal.ko'); 180 | features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy'); 181 | features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun'); 182 | features.hp_has_nginx = access('/usr/sbin/nginx'); 183 | 184 | return features; 185 | } 186 | }, 187 | 188 | resources_get_version: { 189 | args: { type: 'type', repo: 'repo' }, 190 | call: function(req) { 191 | const versions = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`)); 192 | if (req.args?.repo && versions) { 193 | const vers_arr = values(json(versions)); 194 | for (obj in vers_arr) { 195 | if (obj.repo === req.args?.repo) 196 | return { version: obj.version, error: 0 }; 197 | } 198 | return { version: '', error: 1 }; 199 | } else 200 | return { version: versions, error: error() }; 201 | } 202 | }, 203 | resources_update: { 204 | args: { type: 'type', repo: 'repo' }, 205 | call: function(req) { 206 | if (req.args?.type) { 207 | const type = shellquote(req.args?.type), 208 | repo = shellquote(req.args?.repo); 209 | const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type} ${repo}`); 210 | return { status: exit_code }; 211 | } else 212 | return { status: 255, error: 'illegal type' }; 213 | } 214 | }, 215 | 216 | clash_api_get_secret: { 217 | call: function() { 218 | const client_json = json(trim(readfile(`${RUN_DIR}/sing-box-c.json`))); 219 | return { secret: client_json.experimental.clash_api.secret }; 220 | } 221 | } 222 | }; 223 | 224 | return { 'luci.homeproxy': methods }; 225 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/homeproxy/status.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-2.0-only 2 | * 3 | * Copyright (C) 2022-2023 ImmortalWrt.org 4 | */ 5 | 6 | 'use strict'; 7 | 'require dom'; 8 | 'require form'; 9 | 'require fs'; 10 | 'require poll'; 11 | 'require rpc'; 12 | 'require uci'; 13 | 'require ui'; 14 | 'require view'; 15 | 16 | /* Thanks to luci-app-aria2 */ 17 | var css = ' \ 18 | #log_textarea { \ 19 | padding: 10px; \ 20 | text-align: left; \ 21 | } \ 22 | #log_textarea pre { \ 23 | padding: .5rem; \ 24 | word-break: break-all; \ 25 | margin: 0; \ 26 | } \ 27 | .description { \ 28 | background-color: #33ccff; \ 29 | }'; 30 | 31 | var hp_dir = '/var/run/homeproxy'; 32 | 33 | function getConnStat(self, site) { 34 | var callConnStat = rpc.declare({ 35 | object: 'luci.homeproxy', 36 | method: 'connection_check', 37 | params: ['site'], 38 | expect: { '': {} } 39 | }); 40 | 41 | self.default = E('div', { 'style': 'cbi-value-field' }, [ 42 | E('button', { 43 | 'class': 'btn cbi-button cbi-button-action', 44 | 'click': ui.createHandlerFn(this, function() { 45 | return L.resolveDefault(callConnStat(site), {}).then((ret) => { 46 | var ele = self.default.firstElementChild.nextElementSibling; 47 | if (ret.result) { 48 | ele.style.setProperty('color', 'green'); 49 | ele.innerHTML = _('passed'); 50 | } else { 51 | ele.style.setProperty('color', 'red'); 52 | ele.innerHTML = _('failed'); 53 | } 54 | }); 55 | }) 56 | }, [ _('Check') ]), 57 | ' ', 58 | E('strong', { 'style': 'color:gray' }, _('unchecked')), 59 | ]); 60 | } 61 | 62 | function getResVersion(self, type, repo) { 63 | var callResVersion = rpc.declare({ 64 | object: 'luci.homeproxy', 65 | method: 'resources_get_version', 66 | params: ['type', 'repo'], 67 | expect: { '': {} } 68 | }); 69 | 70 | var callResUpdate = rpc.declare({ 71 | object: 'luci.homeproxy', 72 | method: 'resources_update', 73 | params: ['type', 'repo'], 74 | expect: { '': {} } 75 | }); 76 | 77 | return L.resolveDefault(callResVersion(type, repo), {}).then((res) => { 78 | var spanTemp = E('div', { 'style': 'cbi-value-field' }, [ 79 | E('button', { 80 | 'class': 'btn cbi-button cbi-button-action', 81 | 'click': ui.createHandlerFn(this, function() { 82 | return L.resolveDefault(callResUpdate(type, repo), {}).then((res) => { 83 | switch (res.status) { 84 | case 0: 85 | self.description = _('Successfully updated.'); 86 | break; 87 | case 1: 88 | self.description = _('Update failed.'); 89 | break; 90 | case 2: 91 | self.description = _('Already in updating.'); 92 | break; 93 | case 3: 94 | self.description = _('Already at the latest version.'); 95 | break; 96 | default: 97 | self.description = _('Unknown error.'); 98 | break; 99 | } 100 | 101 | return self.map.reset(); 102 | }); 103 | }) 104 | }, [ _('Check update') ]), 105 | ' ', 106 | E('strong', { 'style': (res.error ? 'color:red' : 'color:green') }, 107 | [ res.error ? 'not found' : res.version ] 108 | ), 109 | ]); 110 | 111 | self.default = spanTemp; 112 | }); 113 | } 114 | 115 | function getRuntimeLog(name, filename) { 116 | var callLogClean = rpc.declare({ 117 | object: 'luci.homeproxy', 118 | method: 'log_clean', 119 | params: ['type'], 120 | expect: { '': {} } 121 | }); 122 | 123 | var log_textarea = E('div', { 'id': 'log_textarea' }, 124 | E('img', { 125 | 'src': L.resource(['icons/loading.gif']), 126 | 'alt': _('Loading'), 127 | 'style': 'vertical-align:middle' 128 | }, _('Collecting data...')) 129 | ); 130 | 131 | var log; 132 | poll.add(L.bind(function() { 133 | return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text') 134 | .then(function(res) { 135 | log = E('pre', { 'wrap': 'pre' }, [ 136 | res.trim() || _('Log is empty.') 137 | ]); 138 | 139 | dom.content(log_textarea, log); 140 | }).catch(function(err) { 141 | if (err.toString().includes('NotFoundError')) 142 | log = E('pre', { 'wrap': 'pre' }, [ 143 | _('Log file does not exist.') 144 | ]); 145 | else 146 | log = E('pre', { 'wrap': 'pre' }, [ 147 | _('Unknown error: %s').format(err) 148 | ]); 149 | 150 | dom.content(log_textarea, log); 151 | }); 152 | })); 153 | 154 | return E([ 155 | E('style', [ css ]), 156 | E('div', {'class': 'cbi-map'}, [ 157 | E('h3', {'name': 'content'}, [ 158 | _('%s log').format(name), 159 | ' ', 160 | E('button', { 161 | 'class': 'btn cbi-button cbi-button-action', 162 | 'click': ui.createHandlerFn(this, function() { 163 | return L.resolveDefault(callLogClean(filename), {}); 164 | }) 165 | }, [ _('Clean log') ]) 166 | ]), 167 | E('div', {'class': 'cbi-section'}, [ 168 | log_textarea, 169 | E('div', {'style': 'text-align:right'}, 170 | E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)) 171 | ) 172 | ]) 173 | ]) 174 | ]); 175 | } 176 | 177 | return view.extend({ 178 | load: function() { 179 | return Promise.all([ 180 | uci.load('homeproxy') 181 | ]); 182 | }, 183 | 184 | render: function(data) { 185 | var m, s, o; 186 | var routing_mode = uci.get(data[0], 'config', 'routing_mode') || 'bypass_mainland_china', 187 | dashboard_repo = uci.get(data[0], 'experimental', 'dashboard_repo') || ''; 188 | 189 | m = new form.Map('homeproxy'); 190 | 191 | s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check')); 192 | s.anonymous = true; 193 | 194 | o = s.option(form.DummyValue, '_check_baidu', _('BaiDu')); 195 | o.cfgvalue = function() { return getConnStat(this, 'baidu') }; 196 | 197 | o = s.option(form.DummyValue, '_check_google', _('Google')); 198 | o.cfgvalue = function() { return getConnStat(this, 'google') }; 199 | 200 | 201 | s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management')); 202 | s.anonymous = true; 203 | 204 | if (routing_mode === 'custom' && dashboard_repo !== '') { 205 | o = s.option(form.DummyValue, '_clash_dashboard_version', _('Clash dashboard version')); 206 | o.cfgvalue = function() { return getResVersion(this, 'clash_dashboard', dashboard_repo) }; 207 | o.rawhtml = true; 208 | } 209 | 210 | o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version')); 211 | o.cfgvalue = function() { return getResVersion(this, 'china_ip4') }; 212 | o.rawhtml = true; 213 | 214 | o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version')); 215 | o.cfgvalue = function() { return getResVersion(this, 'china_ip6') }; 216 | o.rawhtml = true; 217 | 218 | o = s.option(form.DummyValue, '_china_list_version', _('China list version')); 219 | o.cfgvalue = function() { return getResVersion(this, 'china_list') }; 220 | o.rawhtml = true; 221 | 222 | o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version')); 223 | o.cfgvalue = function() { return getResVersion(this, 'gfw_list') }; 224 | o.rawhtml = true; 225 | 226 | s = m.section(form.NamedSection, 'config', 'homeproxy'); 227 | s.anonymous = true; 228 | 229 | o = s.option(form.DummyValue, '_homeproxy_logview'); 230 | o.render = L.bind(getRuntimeLog, this, _('HomeProxy'), 'homeproxy'); 231 | 232 | o = s.option(form.DummyValue, '_sing-box-c_logview'); 233 | o.render = L.bind(getRuntimeLog, this, _('sing-box client'), 'sing-box-c'); 234 | 235 | o = s.option(form.DummyValue, '_sing-box-s_logview'); 236 | o.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s'); 237 | 238 | return m.render(); 239 | }, 240 | 241 | handleSaveApply: null, 242 | handleSave: null, 243 | handleReset: null 244 | }); 245 | -------------------------------------------------------------------------------- /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, 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 hexencArray(str) { 56 | if (!str || type(str) !== 'string') 57 | return null; 58 | 59 | const hexstr = hexenc(str); 60 | let arr = []; 61 | 62 | for (let i = 0; i < length(hexstr) / 2; i++) 63 | push(arr, hex('0x' + substr(hexstr, i * 2, 2))); 64 | return arr; 65 | }; 66 | 67 | export function calcStringCRC8(str) { 68 | if (!str || type(str) !== 'string') 69 | return null; 70 | 71 | const crc8Table = [ 72 | 0, 7, 14, 9, 28, 27, 18, 21, 56, 63, 54, 49, 36, 35, 42, 45, 73 | 112, 119, 126, 121, 108, 107, 98, 101, 72, 79, 70, 65, 84, 83, 90, 93, 74 | 224, 231, 238, 233, 252, 251, 242, 245, 216, 223, 214, 209, 196, 195, 202, 205, 75 | 144, 151, 158, 153, 140, 139, 130, 133, 168, 175, 166, 161, 180, 179, 186, 189, 76 | 199, 192, 201, 206, 219, 220, 213, 210, 255, 248, 241, 246, 227, 228, 237, 234, 77 | 183, 176, 185, 190, 171, 172, 165, 162, 143, 136, 129, 134, 147, 148, 157, 154, 78 | 39, 32, 41, 46, 59, 60, 53, 50, 31, 24, 17, 22, 3, 4, 13, 10, 79 | 87, 80, 89, 94, 75, 76, 69, 66, 111, 104, 97, 102, 115, 116, 125, 122, 80 | 137, 142, 135, 128, 149, 146, 155, 156, 177, 182, 191, 184, 173, 170, 163, 164, 81 | 249, 254, 247, 240, 229, 226, 235, 236, 193, 198, 207, 200, 221, 218, 211, 212, 82 | 105, 110, 103, 96, 117, 114, 123, 124, 81, 86, 95, 88, 77, 74, 67, 68, 83 | 25, 30, 23, 16, 5, 2, 11, 12, 33, 38, 47, 40, 61, 58, 51, 52, 84 | 78, 73, 64, 71, 82, 85, 92, 91, 118, 113, 120, 127, 106, 109, 100, 99, 85 | 62, 57, 48, 55, 34, 37, 44, 43, 6, 1, 8, 15, 26, 29, 20, 19, 86 | 174, 169, 160, 167, 178, 181, 188, 187, 150, 145, 152, 159, 138, 141, 132, 131, 87 | 222, 217, 208, 215, 194, 197, 204, 203, 230, 225, 232, 239, 250, 253, 244, 243 88 | ]; 89 | const strArray = hexencArray(str); 90 | let crc8 = 0; 91 | 92 | for (let i = 0; i < length(strArray); i++) 93 | crc8 = crc8Table[(crc8 ^ strArray[i]) & 255]; 94 | return substr('00' + sprintf("%X", crc8), -2); 95 | }; 96 | 97 | export function calcStringMD5(str) { 98 | if (!str || type(str) !== 'string') 99 | return null; 100 | 101 | const output = executeCommand(`/bin/echo -n ${shellQuote(str)} | /usr/bin/md5sum | /usr/bin/awk '{print $1}'`) || {}; 102 | return trim(output.stdout); 103 | }; 104 | 105 | export function getTime(epoch) { 106 | const local_time = localtime(epoch); 107 | return replace(replace(sprintf( 108 | '%d-%2d-%2d@%2d:%2d:%2d', 109 | local_time.year, 110 | local_time.mon, 111 | local_time.mday, 112 | local_time.hour, 113 | local_time.min, 114 | local_time.sec 115 | ), ' ', '0'), '@', ' '); 116 | 117 | }; 118 | 119 | export function wGET(url) { 120 | if (!url || type(url) !== 'string') 121 | return null; 122 | 123 | const output = executeCommand(`/usr/bin/wget -qO- --user-agent 'Wget/1.21 (HomeProxy, like v2rayN)' --timeout=10 ${shellQuote(url)}`) || {}; 124 | return trim(output.stdout); 125 | }; 126 | /* Utilities end */ 127 | 128 | /* String helper start */ 129 | export function isEmpty(res) { 130 | return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0); 131 | }; 132 | 133 | export function strToBool(str) { 134 | return (str === '1') || null; 135 | }; 136 | 137 | export function strToInt(str) { 138 | return !isEmpty(str) ? (int(str) || null) : null; 139 | }; 140 | 141 | export function removeBlankAttrs(res) { 142 | let content; 143 | 144 | if (type(res) === 'object') { 145 | content = {}; 146 | map(keys(res), (k) => { 147 | if (type(res[k]) in ['array', 'object']) 148 | content[k] = removeBlankAttrs(res[k]); 149 | else if (res[k] !== null && res[k] !== '') 150 | content[k] = res[k]; 151 | }); 152 | } else if (type(res) === 'array') { 153 | content = []; 154 | map(res, (k, i) => { 155 | if (type(k) in ['array', 'object']) 156 | push(content, removeBlankAttrs(k)); 157 | else if (k !== null && k !== '') 158 | push(content, k); 159 | }); 160 | } else 161 | return res; 162 | 163 | return content; 164 | }; 165 | 166 | export function validateHostname(hostname) { 167 | return (match(hostname, /^[a-zA-Z0-9_]+$/) != null || 168 | (match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) && 169 | match(hostname, /[^0-9.]/))); 170 | }; 171 | 172 | export function validation(datatype, data) { 173 | if (!datatype || !data) 174 | return null; 175 | 176 | const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`); 177 | return (ret === 0); 178 | }; 179 | 180 | export function filterCheck(name, filter_mode, filter_keywords) { 181 | if (isEmpty(name) || isEmpty(filter_mode) || isEmpty(filter_keywords)) 182 | return false; 183 | 184 | let ret = false; 185 | for (let i in filter_keywords) { 186 | const patten = regexp(i); 187 | if (match(name, patten)) 188 | ret = true; 189 | } 190 | if (filter_mode === 'whitelist') 191 | ret = !ret; 192 | 193 | return ret; 194 | }; 195 | /* String helper end */ 196 | 197 | /* String parser start */ 198 | export function decodeBase64Str(str) { 199 | if (isEmpty(str)) 200 | return null; 201 | 202 | str = trim(str); 203 | str = replace(str, '_', '/'); 204 | str = replace(str, '-', '+'); 205 | 206 | const padding = length(str) % 4; 207 | if (padding) 208 | str = str + substr('====', padding); 209 | 210 | return b64dec(str); 211 | }; 212 | 213 | export function parseURL(url) { 214 | if (type(url) !== 'string') 215 | return null; 216 | 217 | const services = { 218 | http: '80', 219 | https: '443' 220 | }; 221 | 222 | const objurl = {}; 223 | 224 | objurl.href = url; 225 | 226 | url = replace(url, /#(.+)$/, (_, val) => { 227 | objurl.hash = val; 228 | return ''; 229 | }); 230 | 231 | url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => { 232 | objurl.protocol = val; 233 | return ''; 234 | }); 235 | 236 | url = replace(url, /\?(.+)/, (_, val) => { 237 | objurl.search = val; 238 | objurl.searchParams = urldecode_params(val); 239 | return ''; 240 | }); 241 | 242 | url = replace(url, /^\/\/([^\/]+)/, (_, val) => { 243 | val = replace(val, /^([^@]+)@/, (_, val) => { 244 | objurl.userinfo = val; 245 | return ''; 246 | }); 247 | 248 | val = replace(val, /:(\d+)$/, (_, val) => { 249 | objurl.port = val; 250 | return ''; 251 | }); 252 | 253 | if (validation('ip4addr', val) || 254 | validation('ip6addr', replace(val, /\[|\]/g, '')) || 255 | validation('hostname', val)) 256 | objurl.hostname = val; 257 | 258 | return ''; 259 | }); 260 | 261 | objurl.pathname = url || '/'; 262 | 263 | if (!objurl.protocol || !objurl.hostname) 264 | return null; 265 | 266 | if (objurl.userinfo) { 267 | objurl.userinfo = replace(objurl.userinfo, /:([^:]+)$/, (_, val) => { 268 | objurl.password = val; 269 | return ''; 270 | }); 271 | 272 | if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) { 273 | objurl.username = objurl.userinfo; 274 | delete objurl.userinfo; 275 | } else { 276 | delete objurl.userinfo; 277 | delete objurl.password; 278 | } 279 | }; 280 | 281 | if (!objurl.port) 282 | objurl.port = services[objurl.protocol]; 283 | 284 | objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : ''); 285 | objurl.origin = `${objurl.protocol}://${objurl.host}`; 286 | 287 | return objurl; 288 | }; 289 | /* String parser end */ 290 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/homeproxy/ruleset.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-3.0-only 3 | * 4 | * Copyright (C) 2023 ImmortalWrt.org 5 | */ 6 | 7 | 'use strict'; 8 | 'require form'; 9 | 'require fs'; 10 | 'require uci'; 11 | 'require ui'; 12 | 'require view'; 13 | 14 | 'require homeproxy as hp'; 15 | 16 | const docdata = 'base64,' + 'cmxzdHBsYWNlaG9sZGVy' 17 | 18 | function parseRulesetLink(uri) { 19 | var config, 20 | filefmt = new RegExp(/^(json|srs)$/), 21 | unuciname = new RegExp(/[^a-zA-Z0-9_]+/, "g"); 22 | 23 | uri = uri.split('://'); 24 | if (uri[0] && uri[1]) { 25 | switch (uri[0]) { 26 | case 'http': 27 | case 'https': 28 | var url = new URL('http://' + uri[1]); 29 | var file = url.searchParams.get('file'); 30 | var rawquery = url.searchParams.get('rawquery'); 31 | var name = decodeURIComponent(url.pathname.split('/').pop()) 32 | .replace(/[\s\.-]/g, '_').replace(unuciname, ''); 33 | 34 | if (filefmt.test(file)) { 35 | var fullpath = (url.username ? url.username + '@' : '') + url.host + url.pathname + (rawquery ? '?' + decodeURIComponent(rawquery) : ''); 36 | config = { 37 | label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null, 38 | type: 'remote', 39 | format: file.match(/^json$/) ? 'source' : file.match(/^srs$/) ? 'binary' : 'unknown', 40 | url: String.format('%s://%s', uri[0], fullpath), 41 | href: String.format('http://%s', fullpath) 42 | }; 43 | } 44 | 45 | break; 46 | case 'file': 47 | var url = new URL('file://' + uri[1]); 48 | var file = url.searchParams.get('file'); 49 | var name = decodeURIComponent(url.pathname.split('/').pop()) 50 | .replace(/[\s\.-]/g, '_').replace(unuciname, ''); 51 | 52 | if (filefmt.test(file)) { 53 | config = { 54 | label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null, 55 | type: 'local', 56 | format: file.match(/^json$/) ? 'source' : file.match(/^srs$/) ? 'binary' : 'unknown', 57 | path: url.pathname, 58 | href: String.format('file://%s%s', url.host, url.pathname) 59 | }; 60 | } 61 | 62 | break; 63 | } 64 | } 65 | 66 | if (config) { 67 | if (!config.type || !config.href) 68 | return null; 69 | else if (!config.label) 70 | config.label = hp.calcStringMD5(config.href); 71 | } 72 | 73 | return config; 74 | } 75 | 76 | return view.extend({ 77 | load: function() { 78 | return Promise.all([ 79 | uci.load('homeproxy') 80 | ]); 81 | }, 82 | 83 | render: function(data) { 84 | var m, s, o; 85 | 86 | m = new form.Map('homeproxy', _('Edit ruleset')); 87 | 88 | /* Rule set settings start */ 89 | var prefix = 'rule_'; 90 | s = m.section(form.GridSection, 'ruleset'); 91 | s.addremove = true; 92 | s.rowcolors = true; 93 | s.sortable = true; 94 | s.nodescriptions = true; 95 | s.modaltitle = L.bind(hp.loadModalTitle, this, _('Rule set'), _('Add a rule set'), data[0]); 96 | s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]); 97 | /* Import rule-set links start */ 98 | s.handleLinkImport = function() { 99 | var textarea = new ui.Textarea('', { 100 | 'placeholder': 'http(s)://github.com/sagernet/sing-geoip/raw/rule-set/geoip-hk.srs?file=srs&rawquery=good%3Djob#GeoIP-HK\n' + 101 | 'file:///etc/homeproxy/ruleset/example.json?file=json#Example%20file\n' 102 | }); 103 | ui.showModal(_('Import rule-set links'), [ 104 | E('p', _('Supports rule-set links of type: local, remote and format: source, binary.
') + 105 | _('Please refer to %s for link format standards.') 106 | .format('data:text/html;' + docdata, _('Ruleset-URI-Scheme'))), 107 | textarea.render(), 108 | E('div', { class: 'right' }, [ 109 | E('button', { 110 | class: 'btn', 111 | click: ui.hideModal 112 | }, [ _('Cancel') ]), 113 | '', 114 | E('button', { 115 | class: 'btn cbi-button-action', 116 | click: ui.createHandlerFn(this, function() { 117 | var input_links = textarea.getValue().trim().split('\n'); 118 | if (input_links && input_links[0]) { 119 | /* Remove duplicate lines */ 120 | input_links = input_links.reduce((pre, cur) => 121 | (!pre.includes(cur) && pre.push(cur), pre), []); 122 | 123 | var imported_ruleset = 0; 124 | input_links.forEach((l) => { 125 | var config = parseRulesetLink(l); 126 | if (config) { 127 | var hrefHash = hp.calcStringMD5(config.href); 128 | config.href = null; 129 | var sid = uci.add(data[0], 'ruleset', hrefHash); 130 | Object.keys(config).forEach((k) => { 131 | uci.set(data[0], sid, k, config[k]); 132 | }); 133 | imported_ruleset++; 134 | } 135 | }); 136 | 137 | if (imported_ruleset === 0) 138 | ui.addNotification(null, E('p', _('No valid rule-set link found.'))); 139 | else 140 | ui.addNotification(null, E('p', _('Successfully imported %s rule-set of total %s.').format( 141 | imported_ruleset, input_links.length))); 142 | 143 | return uci.save() 144 | .then(L.bind(this.map.load, this.map)) 145 | .then(L.bind(this.map.reset, this.map)) 146 | .then(L.ui.hideModal) 147 | .catch(function() {}); 148 | } else { 149 | return ui.hideModal(); 150 | } 151 | }) 152 | }, [ _('Import') ]) 153 | ]) 154 | ]) 155 | } 156 | s.renderSectionAdd = function(extra_class) { 157 | var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments), 158 | nameEl = el.querySelector('.cbi-section-create-name'); 159 | 160 | ui.addValidator(nameEl, 'uciname', true, (v) => { 161 | var button = el.querySelector('.cbi-section-create > .cbi-button-add'); 162 | var uciconfig = this.uciconfig || this.map.config; 163 | 164 | if (!v) { 165 | button.disabled = true; 166 | return true; 167 | } else if (uci.get(uciconfig, v)) { 168 | button.disabled = true; 169 | return _('Expecting: %s').format(_('unique UCI identifier')); 170 | } else if (uci.get(uciconfig, prefix + v)) { 171 | button.disabled = true; 172 | return _('Expecting: %s').format(_('unique label')); 173 | } else { 174 | button.disabled = null; 175 | return true; 176 | } 177 | }, 'blur', 'keyup'); 178 | 179 | el.appendChild(E('button', { 180 | 'class': 'cbi-button cbi-button-add', 181 | 'title': _('Import rule-set links'), 182 | 'click': ui.createHandlerFn(this, 'handleLinkImport') 183 | }, [ _('Import rule-set links') ])); 184 | 185 | return el; 186 | } 187 | s.handleAdd = function(ev, name) { 188 | return form.GridSection.prototype.handleAdd.apply(this, [ ev, prefix + name ]); 189 | } 190 | /* Import rule-set links end */ 191 | 192 | o = s.option(form.Value, 'label', _('Label')); 193 | o.load = L.bind(hp.loadDefaultLabel, this, data[0]); 194 | o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'ruleset', 'label'); 195 | o.modalonly = true; 196 | 197 | o = s.option(form.Flag, 'enabled', _('Enable')); 198 | o.default = o.enabled; 199 | o.rmempty = false; 200 | o.editable = true; 201 | 202 | o = s.option(form.ListValue, 'type', _('Type')); 203 | o.value('local', _('Local')); 204 | o.value('remote', _('Remote')); 205 | o.default = 'remote'; 206 | o.rmempty = false; 207 | 208 | o = s.option(form.ListValue, 'format', _('Format')); 209 | o.value('source', _('Source file')); 210 | o.value('binary', _('Binary file')); 211 | o.default = 'source'; 212 | o.rmempty = false; 213 | 214 | o = s.option(form.Value, 'path', _('Path')); 215 | o.datatype = 'file'; 216 | o.placeholder = '/etc/homeproxy/ruleset/example.json'; 217 | o.rmempty = false; 218 | o.depends('type', 'local'); 219 | o.modalonly = true; 220 | 221 | o = s.option(form.Value, 'url', _('Rule set URL')); 222 | o.validate = function(section_id, value) { 223 | if (section_id) { 224 | if (!value) 225 | return _('Expecting: %s').format(_('non-empty value')); 226 | 227 | try { 228 | var url = new URL(value); 229 | if (!url.hostname) 230 | return _('Expecting: %s').format(_('valid URL')); 231 | } 232 | catch(e) { 233 | return _('Expecting: %s').format(_('valid URL')); 234 | } 235 | } 236 | 237 | return true; 238 | } 239 | o.rmempty = false; 240 | o.depends('type', 'remote'); 241 | o.modalonly = true; 242 | 243 | o = s.option(form.ListValue, 'outbound', _('Outbound'), 244 | _('Tag of the outbound to download rule set.')); 245 | o.load = function(section_id) { 246 | delete this.keylist; 247 | delete this.vallist; 248 | 249 | this.value('direct-out', _('Direct')); 250 | uci.sections(data[0], 'routing_node', (res) => { 251 | if (res.enabled === '1') 252 | this.value(res['.name'], res.label); 253 | }); 254 | 255 | return this.super('load', section_id); 256 | } 257 | o.default = 'direct-out'; 258 | o.rmempty = false; 259 | //o.editable = true; 260 | o.textvalue = function(section_id) { 261 | var cval = this.cfgvalue(section_id) || this.default; 262 | var remote = L.bind(function() { 263 | let cval = this.cfgvalue(section_id) || this.default; 264 | return (cval === 'remote') ? true : false; 265 | }, s.getOption('type')) 266 | return remote() ? cval : _('none'); 267 | }; 268 | o.depends('type', 'remote'); 269 | 270 | o = s.option(form.Value, 'update_interval', _('Update interval'), 271 | _('Update interval of rule set.
1d will be used if empty.')); 272 | o.depends('type', 'remote'); 273 | /* Rule set settings end */ 274 | 275 | return m.render(); 276 | } 277 | }); 278 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/homeproxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-2.0-only 3 | * 4 | * Copyright (C) 2022-2023 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 | 'require validation'; 15 | 16 | return baseclass.extend({ 17 | dns_strategy: { 18 | '': _('Default'), 19 | 'prefer_ipv4': _('Prefer IPv4'), 20 | 'prefer_ipv6': _('Prefer IPv6'), 21 | 'ipv4_only': _('IPv4 only'), 22 | 'ipv6_only': _('IPv6 only') 23 | }, 24 | 25 | shadowsocks_encrypt_methods: [ 26 | /* Stream */ 27 | 'none', 28 | /* AEAD */ 29 | 'aes-128-gcm', 30 | 'aes-192-gcm', 31 | 'aes-256-gcm', 32 | 'chacha20-ietf-poly1305', 33 | 'xchacha20-ietf-poly1305', 34 | /* AEAD 2022 */ 35 | '2022-blake3-aes-128-gcm', 36 | '2022-blake3-aes-256-gcm', 37 | '2022-blake3-chacha20-poly1305' 38 | ], 39 | 40 | tls_cipher_suites: [ 41 | 'TLS_RSA_WITH_AES_128_CBC_SHA', 42 | 'TLS_RSA_WITH_AES_256_CBC_SHA', 43 | 'TLS_RSA_WITH_AES_128_GCM_SHA256', 44 | 'TLS_RSA_WITH_AES_256_GCM_SHA384', 45 | 'TLS_AES_128_GCM_SHA256', 46 | 'TLS_AES_256_GCM_SHA384', 47 | 'TLS_CHACHA20_POLY1305_SHA256', 48 | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA', 49 | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA', 50 | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA', 51 | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA', 52 | 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256', 53 | 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', 54 | 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', 55 | 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 56 | 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256', 57 | 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256' 58 | ], 59 | 60 | tls_versions: [ 61 | '1.0', 62 | '1.1', 63 | '1.2', 64 | '1.3' 65 | ], 66 | 67 | calcStringMD5: function(e) { 68 | /* Thanks to https://stackoverflow.com/a/41602636 */ 69 | function h(a, b) { 70 | var c, d, e, f, g; 71 | e = a & 2147483648; 72 | f = b & 2147483648; 73 | c = a & 1073741824; 74 | d = b & 1073741824; 75 | g = (a & 1073741823) + (b & 1073741823); 76 | return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f; 77 | } 78 | function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); } 79 | function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); } 80 | function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); } 81 | function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); } 82 | function p(a) { 83 | var b = '', d = ''; 84 | for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2); 85 | return b; 86 | } 87 | 88 | var f = [], q, r, s, t, a, b, c, d; 89 | e = function(a) { 90 | a = a.replace(/\r\n/g, '\n'); 91 | for (var b = '', d = 0; d < a.length; d++) { 92 | var c = a.charCodeAt(d); 93 | 128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) : 94 | (b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)), 95 | b += String.fromCharCode(c & 63 | 128)) 96 | } 97 | return b; 98 | }(e); 99 | f = function(b) { 100 | var c = b.length, a = c + 8; 101 | for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;) 102 | a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++; 103 | a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29; 104 | return e; 105 | }(e); 106 | a = 1732584193; 107 | b = 4023233417; 108 | c = 2562383102; 109 | d = 271733878; 110 | 111 | for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d, 112 | a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), 113 | c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), 114 | a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), 115 | c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), 116 | a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), 117 | c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), 118 | a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), 119 | c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), 120 | a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), 121 | c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), 122 | a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), 123 | c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), 124 | a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), 125 | c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), 126 | a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), 127 | c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), 128 | a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), 129 | c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), 130 | a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), 131 | c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), 132 | a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), 133 | c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), 134 | a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), 135 | c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), 136 | a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), 137 | c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), 138 | a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), 139 | c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), 140 | a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), 141 | c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), 142 | a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), 143 | c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), 144 | a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t); 145 | return (p(a) + p(b) + p(c) + p(d)).toLowerCase(); 146 | }, 147 | 148 | decodeBase64Str: function(str) { 149 | if (!str) 150 | return null; 151 | 152 | /* Thanks to luci-app-ssr-plus */ 153 | str = str.replace(/-/g, '+').replace(/_/g, '/'); 154 | var padding = (4 - str.length % 4) % 4; 155 | if (padding) 156 | str = str + Array(padding + 1).join('='); 157 | 158 | return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => 159 | '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) 160 | ).join('')); 161 | }, 162 | 163 | getBuiltinFeatures: function() { 164 | var callGetSingBoxFeatures = rpc.declare({ 165 | object: 'luci.homeproxy', 166 | method: 'singbox_get_features', 167 | expect: { '': {} } 168 | }); 169 | 170 | return L.resolveDefault(callGetSingBoxFeatures(), {}); 171 | }, 172 | 173 | generateRand: function(type, length) { 174 | var byteArr; 175 | if (['base64', 'hex'].includes(type)) 176 | byteArr = crypto.getRandomValues(new Uint8Array(length)); 177 | switch (type) { 178 | case 'base64': 179 | /* Thanks to https://stackoverflow.com/questions/9267899 */ 180 | return btoa(String.fromCharCode.apply(null, byteArr)); 181 | case 'hex': 182 | return Array.from(byteArr, (byte) => 183 | (byte & 255).toString(16).padStart(2, '0') 184 | ).join(''); 185 | case 'uuid': 186 | /* Thanks to https://stackoverflow.com/a/2117523 */ 187 | return (location.protocol === 'https:') ? crypto.randomUUID() : 188 | ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => 189 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 190 | ); 191 | default: 192 | return null; 193 | }; 194 | }, 195 | 196 | loadDefaultLabel: function(uciconfig, ucisection) { 197 | var label = uci.get(uciconfig, ucisection, 'label'); 198 | if (label) { 199 | return label; 200 | } else { 201 | uci.set(uciconfig, ucisection, 'label', ucisection); 202 | return ucisection; 203 | } 204 | }, 205 | 206 | loadModalTitle: function(title, addtitle, uciconfig, ucisection) { 207 | var label = uci.get(uciconfig, ucisection, 'label'); 208 | return label ? title + ' » ' + label : addtitle; 209 | }, 210 | 211 | loadSubscriptionInfo: function(uciconfig) { 212 | var subs = {}; 213 | for (var suburl of (uci.get(uciconfig, 'subscription', 'subscription_url') || [])) { 214 | const url = new URL(suburl); 215 | const urlhash = this.calcStringMD5(suburl.replace(/#.*$/, '')); 216 | subs[urlhash] = { 217 | "url": suburl.replace(/#.*$/, ''), 218 | "name": url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname 219 | }; 220 | } 221 | return subs; 222 | }, 223 | 224 | loadNodesList: function(uciconfig, subinfo) { 225 | var nodelist = {}; 226 | uci.sections(uciconfig, 'node', (res) => { 227 | var nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '', 228 | nodeport = ((res.type === 'direct') ? res.override_port : res.port) || ''; 229 | 230 | nodelist[res['.name']] = 231 | String.format('%s [%s] %s', res.grouphash ? 232 | String.format('[%s]', subinfo[res.grouphash]?.name || res.grouphash) : '', 233 | res.type, res.label || ((validation.parseIPv6(nodeaddr) ? 234 | String.format('[%s]', nodeaddr) : nodeaddr) + ':' + nodeport)); 235 | }); 236 | return nodelist; 237 | }, 238 | 239 | renderSectionAdd: function(section, prefmt, LC, extra_class) { 240 | var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]), 241 | nameEl = el.querySelector('.cbi-section-create-name'); 242 | ui.addValidator(nameEl, 'uciname', true, (v) => { 243 | var button = el.querySelector('.cbi-section-create > .cbi-button-add'); 244 | var uciconfig = section.uciconfig || section.map.config; 245 | var prefix = prefmt?.prefix ? prefmt.prefix : '', 246 | suffix = prefmt?.suffix ? prefmt.suffix : ''; 247 | 248 | if (!v) { 249 | button.disabled = true; 250 | return true; 251 | } else if (LC && (v !== v.toLowerCase())) { 252 | button.disabled = true; 253 | return _('Expecting: %s').format(_('Lowercase only')); 254 | } else if (uci.get(uciconfig, v)) { 255 | button.disabled = true; 256 | return _('Expecting: %s').format(_('unique UCI identifier')); 257 | } else if (uci.get(uciconfig, prefix + v + suffix)) { 258 | button.disabled = true; 259 | return _('Expecting: %s').format(_('unique label')); 260 | } else { 261 | button.disabled = null; 262 | return true; 263 | } 264 | }, 'blur', 'keyup'); 265 | 266 | return el; 267 | }, 268 | 269 | handleAdd: function(section, prefmt, ev, name) { 270 | var prefix = prefmt?.prefix ? prefmt.prefix : '', 271 | suffix = prefmt?.suffix ? prefmt.suffix : ''; 272 | 273 | return form.GridSection.prototype.handleAdd.apply(section, [ ev, prefix + name + suffix ]); 274 | }, 275 | 276 | uploadCertificate: function(option, type, filename, ev) { 277 | var callWriteCertificate = rpc.declare({ 278 | object: 'luci.homeproxy', 279 | method: 'certificate_write', 280 | params: ['filename'], 281 | expect: { '': {} } 282 | }); 283 | 284 | return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target) 285 | .then(L.bind((btn, res) => { 286 | return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => { 287 | if (ret.result === true) 288 | ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size))); 289 | else 290 | ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error))); 291 | }); 292 | }, this, ev.target)) 293 | .catch((e) => { ui.addNotification(null, E('p', e.message)) }); 294 | }, 295 | 296 | validateBase64Key: function(length, section_id, value) { 297 | /* Thanks to luci-proto-wireguard */ 298 | if (section_id && value) 299 | if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') 300 | return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length)); 301 | 302 | return true; 303 | }, 304 | 305 | validateUniqueValue: function(uciconfig, ucisection, ucioption, section_id, value) { 306 | if (section_id) { 307 | if (!value) 308 | return _('Expecting: %s').format(_('non-empty value')); 309 | 310 | var 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: function(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 | -------------------------------------------------------------------------------- /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 | DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d" 18 | APILOCATION_PATH="/etc/nginx/conf.d/homeproxy.locations" 19 | 20 | log() { 21 | echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH" 22 | } 23 | 24 | start_service() { 25 | config_load "$CONF" 26 | 27 | local routing_mode proxy_mode 28 | config_get routing_mode "config" "routing_mode" "bypass_mainland_china" 29 | config_get proxy_mode "config" "proxy_mode" "redirect_tproxy" 30 | 31 | local outbound_node 32 | if [ "$routing_mode" != "custom" ]; then 33 | config_get outbound_node "config" "main_node" "nil" 34 | else 35 | config_get outbound_node "routing" "default_outbound" "nil" 36 | fi 37 | 38 | local server_enabled 39 | config_get_bool server_enabled "server" "enabled" "0" 40 | 41 | if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then 42 | return 1 43 | fi 44 | 45 | mkdir -p "$RUN_DIR" 46 | 47 | if [ "$outbound_node" != "nil" ]; then 48 | # Generate/Validate client config 49 | ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH" 50 | 51 | if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then 52 | log "Error: failed to generate client configuration." 53 | return 1 54 | elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then 55 | log "Error: wrong client configuration detected." 56 | return 1 57 | fi 58 | 59 | # Auto update 60 | local auto_update auto_update_expr 61 | config_get_bool auto_update "subscription" "auto_update" "0" 62 | if [ "$auto_update" = "1" ]; then 63 | config_get auto_update_expr "subscription" "auto_update_expr" "0 2 * * *" 64 | echo -e "$auto_update_expr $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root" 65 | /etc/init.d/cron restart 66 | fi 67 | 68 | # Clash API uses Nginx reverse proxy 69 | local clash_api_enabled clash_api_port nginx_support 70 | config_get_bool clash_api_enabled "experimental" "clash_api_enabled" "0" 71 | config_get_bool nginx_support "experimental" "nginx_support" "0" 72 | if [ "$clash_api_enabled" = "1" -a "$nginx_support" = "1" ]; then 73 | config_get clash_api_port "experimental" "clash_api_port" "9090" 74 | [ "$(sed -En "s|^\s*proxy_pass\s+https?://[^:]+:(\d+).*|\1|p" "$APILOCATION_PATH")" = "$clash_api_port" ] || sed -Ei "/\bproxy_pass\b/{s|(proxy_pass\s+https?://[^:]+:)(\d+)(.*)|\1$clash_api_port\3|}" "$APILOCATION_PATH" 75 | /etc/init.d/nginx reload 76 | fi 77 | 78 | # DNSMasq rules 79 | local ipv6_support 80 | config_get_bool ipv6_support "config" "ipv6_support" "0" 81 | local dns_port china_dns_server china_dns_port 82 | config_get dns_port "infra" "dns_port" "5333" 83 | mkdir -p "$DNSMASQ_DIR" 84 | echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" 85 | case "$routing_mode" in 86 | "gfwlist") 87 | [ "$ipv6_support" -eq "0" ] || local gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6" 88 | 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" \ 89 | "$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf" 90 | ;; 91 | "bypass_mainland_china") 92 | config_get china_dns_server "config" "china_dns_server" 93 | config_get china_dns_port "infra" "china_dns_port" "5334" 94 | 95 | if [ -e "/usr/bin/chinadns-ng" ] && [ -n "$china_dns_server" ]; then 96 | cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf" 97 | no-poll 98 | no-resolv 99 | server=127.0.0.1#$china_dns_port 100 | EOF 101 | else 102 | china_dns_server="" 103 | sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \ 104 | "$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf" 105 | fi 106 | ;; 107 | "proxy_mainland_china") 108 | sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \ 109 | "$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf" 110 | ;; 111 | "custom"|"global") 112 | cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf" 113 | no-poll 114 | no-resolv 115 | server=127.0.0.1#$dns_port 116 | EOF 117 | ;; 118 | esac 119 | 120 | if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then 121 | [ "$ipv6_support" -eq "0" ] || local wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6" 122 | 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" \ 123 | "$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf" 124 | fi 125 | /etc/init.d/dnsmasq restart >"/dev/null" 2>&1 126 | 127 | # Setup routing table 128 | local table_mark 129 | config_get table_mark "infra" "table_mark" "100" 130 | case "$proxy_mode" in 131 | "redirect_tproxy") 132 | local outbound_udp_node 133 | config_get outbound_udp_node "config" "main_udp_node" "nil" 134 | if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then 135 | local tproxy_mark 136 | config_get tproxy_mark "infra" "tproxy_mark" "101" 137 | 138 | ip rule add fwmark "$tproxy_mark" table "$table_mark" 139 | ip route add local 0.0.0.0/0 dev lo table "$table_mark" 140 | 141 | if [ "$ipv6_support" -eq "1" ]; then 142 | ip -6 rule add fwmark "$tproxy_mark" table "$table_mark" 143 | ip -6 route add local ::/0 dev lo table "$table_mark" 144 | fi 145 | fi 146 | ;; 147 | "redirect_tun"|"tun") 148 | local tun_name tun_mark 149 | config_get tun_name "infra" "tun_name" "singtun0" 150 | config_get tun_mark "infra" "tun_mark" "102" 151 | 152 | ip tuntap add mode tun user root name "$tun_name" 153 | sleep 1s 154 | ip link set "$tun_name" up 155 | 156 | ip route replace default dev "$tun_name" table "$table_mark" 157 | ip rule add fwmark "$tun_mark" lookup "$table_mark" 158 | 159 | ip -6 route replace default dev "$tun_name" table "$table_mark" 160 | ip -6 rule add fwmark "$tun_mark" lookup "$table_mark" 161 | ;; 162 | esac 163 | 164 | # sing-box (client) 165 | procd_open_instance "sing-box-c" 166 | 167 | procd_set_param command "$PROG" 168 | procd_append_param command run --config "$RUN_DIR/sing-box-c.json" 169 | 170 | if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then 171 | procd_add_jail "sing-box-c" log procfs 172 | procd_add_jail_mount "$RUN_DIR/sing-box-c.json" 173 | procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log" 174 | procd_add_jail_mount "$HP_DIR/certs/" 175 | procd_add_jail_mount "/etc/ssl/" 176 | procd_add_jail_mount "/etc/localtime" 177 | procd_add_jail_mount "/etc/TZ" 178 | procd_set_param capabilities "/etc/capabilities/homeproxy.json" 179 | procd_set_param no_new_privs 1 180 | procd_set_param user sing-box 181 | procd_set_param group sing-box 182 | fi 183 | 184 | procd_set_param limits core="unlimited" 185 | procd_set_param limits nofile="1000000 1000000" 186 | procd_set_param stderr 1 187 | procd_set_param respawn 188 | 189 | procd_close_instance 190 | 191 | # chinadns-ng 192 | if [ -n "$china_dns_server" ]; then 193 | local wandns="$(ifstatus wan | jsonfilter -e '@["dns-server"][0]' || echo "119.29.29.29")" 194 | china_dns_server="${china_dns_server/wan/$wandns}" 195 | china_dns_server="${china_dns_server// /,}" 196 | 197 | for i in $(seq 1 "$(grep -c "processor" "/proc/cpuinfo")"); do 198 | procd_open_instance "chinadns-ng-$i" 199 | 200 | procd_set_param command "/usr/bin/chinadns-ng" 201 | procd_append_param command --bind-port "$china_dns_port" 202 | procd_append_param command --china-dns "$china_dns_server" 203 | procd_append_param command --trust-dns "127.0.0.1#$dns_port" 204 | procd_append_param command --ipset-name4 "inet@fw4@homeproxy_mainland_addr_v4" 205 | procd_append_param command --ipset-name6 "inet@fw4@homeproxy_mainland_addr_v6" 206 | procd_append_param command --chnlist-file "$HP_DIR/resources/china_list.txt" 207 | procd_append_param command --gfwlist-file "$HP_DIR/resources/gfw_list.txt" 208 | procd_append_param command --reuse-port 209 | 210 | if chinadns-ng --version | grep -q "target:"; then 211 | procd_append_param command --cache 10000 212 | procd_append_param command --cache-stale 3600 213 | procd_append_param command --verdict-cache 10000 214 | [ "$ipv6_support" -eq "1" ] || procd_append_param command --no-ipv6=ip:non_china 215 | else 216 | [ "$ipv6_support" -eq "1" ] || procd_append_param command --no-ipv6=tC 217 | fi 218 | 219 | if [ -x "/sbin/ujail" ]; then 220 | procd_add_jail "chinadns-ng" log 221 | procd_add_jail_mount "$HP_DIR/resources/china_list.txt" 222 | procd_add_jail_mount "$HP_DIR/resources/gfw_list.txt" 223 | procd_set_param capabilities "/etc/capabilities/homeproxy.json" 224 | procd_set_param no_new_privs 1 225 | procd_set_param user sing-box 226 | procd_set_param group sing-box 227 | fi 228 | 229 | procd_set_param limits core="unlimited" 230 | procd_set_param limits nofile="1000000 1000000" 231 | procd_set_param stderr 1 232 | procd_set_param respawn 233 | 234 | procd_close_instance 235 | done 236 | fi 237 | fi 238 | 239 | if [ "$server_enabled" = "1" ]; then 240 | # Generate/Validate server config 241 | ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH" 242 | 243 | if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then 244 | log "Error: failed to generate server configuration." 245 | return 1 246 | elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then 247 | log "Error: wrong server configuration detected." 248 | return 1 249 | fi 250 | 251 | # sing-box (server) 252 | procd_open_instance "sing-box-s" 253 | 254 | procd_set_param command "$PROG" 255 | procd_append_param command run --config "$RUN_DIR/sing-box-s.json" 256 | 257 | if [ -x "/sbin/ujail" ]; then 258 | procd_add_jail "sing-box-s" log procfs 259 | procd_add_jail_mount "$RUN_DIR/sing-box-s.json" 260 | procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log" 261 | procd_add_jail_mount "$HP_DIR/certs/" 262 | procd_add_jail_mount "/etc/localtime" 263 | procd_add_jail_mount "/etc/TZ" 264 | procd_set_param capabilities "/etc/capabilities/homeproxy.json" 265 | procd_set_param no_new_privs 1 266 | procd_set_param user sing-box 267 | procd_set_param group sing-box 268 | fi 269 | 270 | procd_set_param limits core="unlimited" 271 | procd_set_param limits nofile="1000000 1000000" 272 | procd_set_param stderr 1 273 | procd_set_param respawn 274 | 275 | procd_close_instance 276 | fi 277 | 278 | # log-cleaner 279 | procd_open_instance "log-cleaner" 280 | procd_set_param command "$HP_DIR/scripts/clean_log.sh" 281 | procd_set_param respawn 282 | procd_close_instance 283 | 284 | # Prepare ruleset directory for custom routing mode 285 | if [ "$routing_mode" = "custom" ]; then 286 | [ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset" 287 | fi 288 | 289 | # Update permissions for ujail 290 | if [ "$outbound_node" != "nil" ]; then 291 | echo > "$RUN_DIR/sing-box-c.log" 292 | chown sing-box:sing-box "$RUN_DIR/sing-box-c.log" 293 | chown sing-box:sing-box "$RUN_DIR/sing-box-c.json" 294 | chmod 0644 "$HP_DIR/resources/gfw_list.txt" 295 | fi 296 | if [ "$server_enabled" = "1" ]; then 297 | echo > "$RUN_DIR/sing-box-s.log" 298 | chown sing-box:sing-box "$RUN_DIR/sing-box-s.log" 299 | chown sing-box:sing-box "$RUN_DIR/sing-box-s.json" 300 | fi 301 | 302 | # Setup firewall 303 | utpl -S "$HP_DIR/scripts/firewall_pre.ut" > "$RUN_DIR/fw4_pre.nft" 304 | [ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft" 305 | fw4 reload >"/dev/null" 2>&1 306 | 307 | log "$(sing-box version | awk 'NR==1{print $1,$3}') started." 308 | } 309 | 310 | stop_service() { 311 | sed -i "/$CONF/d" "/etc/crontabs/root" 2>"/dev/null" 312 | /etc/init.d/cron restart >"/dev/null" 2>&1 313 | 314 | # Setup firewall 315 | # Load config 316 | config_load "$CONF" 317 | local table_mark tproxy_mark tun_mark tun_name 318 | config_get table_mark "infra" "table_mark" "100" 319 | config_get tproxy_mark "infra" "tproxy_mark" "101" 320 | config_get tun_mark "infra" "tun_mark" "102" 321 | config_get tun_name "infra" "tun_name" "singtun0" 322 | 323 | # Tproxy 324 | ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" 325 | ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null" 326 | ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" 327 | ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null" 328 | 329 | # TUN 330 | ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null" 331 | ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null" 332 | 333 | ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null" 334 | ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null" 335 | 336 | # Nftables rules 337 | for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \ 338 | "homeproxy_redirect" "homeproxy_redirect_proxy" \ 339 | "homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \ 340 | "homeproxy_mangle_prerouting" "homeproxy_mangle_output" \ 341 | "homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \ 342 | "homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \ 343 | "homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do 344 | nft flush chain inet fw4 "$i" 345 | nft delete chain inet fw4 "$i" 346 | done 2>"/dev/null" 347 | for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \ 348 | "homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \ 349 | "homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \ 350 | "homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \ 351 | "homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \ 352 | "homeproxy_routing_port"; do 353 | nft flush set inet fw4 "$i" 354 | nft delete set inet fw4 "$i" 355 | done 2>"/dev/null" 356 | echo > "$RUN_DIR/fw4_pre.nft" 2>"/dev/null" 357 | echo > "$RUN_DIR/fw4_post.nft" 2>"/dev/null" 358 | fw4 reload >"/dev/null" 2>&1 359 | 360 | # Remove DNS hijack 361 | rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR" 362 | /etc/init.d/dnsmasq restart >"/dev/null" 2>&1 363 | 364 | rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \ 365 | "$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log" 366 | 367 | log "Service stopped." 368 | } 369 | 370 | service_stopped() { 371 | # Load config 372 | config_load "$CONF" 373 | local tun_name 374 | config_get tun_name "infra" "tun_name" "singtun0" 375 | 376 | # TUN 377 | ip link set "$tun_name" down 2>"/dev/null" 378 | ip tuntap del mode tun name "$tun_name" 2>"/dev/null" 379 | } 380 | 381 | reload_service() { 382 | log "Reloading service..." 383 | 384 | stop 385 | start 386 | } 387 | 388 | service_triggers() { 389 | procd_add_reload_trigger "$CONF" 390 | procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload 391 | } 392 | -------------------------------------------------------------------------------- /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 { open } from 'fs'; 11 | import { connect } from 'ubus'; 12 | import { cursor } from 'uci'; 13 | 14 | import { urldecode, urlencode, urldecode_params } from 'luci.http'; 15 | import { init_action } from 'luci.sys'; 16 | 17 | import { 18 | calcStringMD5, wGET, executeCommand, decodeBase64Str, 19 | getTime, isEmpty, parseURL, validation, filterCheck, 20 | 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') || 'nil', 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 | via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0'; 39 | 40 | const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china'; 41 | let main_node, main_udp_node; 42 | if (routing_mode !== 'custom') { 43 | main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil'; 44 | main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil'; 45 | } 46 | /* UCI config end */ 47 | 48 | /* String helper start */ 49 | /* String helper end */ 50 | 51 | /* Common var start */ 52 | const node_cache = {}, 53 | node_result = []; 54 | 55 | const ubus = connect(); 56 | const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {}; 57 | /* Common var end */ 58 | 59 | /* Log */ 60 | system(`mkdir -p ${RUN_DIR}`); 61 | function log(...args) { 62 | const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a'); 63 | logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`); 64 | logfile.close(); 65 | } 66 | 67 | function parse_uri(uri) { 68 | let config, url, params; 69 | 70 | if (type(uri) === 'object') { 71 | if (uri.nodetype === 'sip008') { 72 | /* https://shadowsocks.org/guide/sip008.html */ 73 | config = { 74 | label: uri.remarks, 75 | type: 'shadowsocks', 76 | address: uri.server, 77 | port: uri.server_port, 78 | shadowsocks_encrypt_method: uri.method, 79 | password: uri.password, 80 | shadowsocks_plugin: uri.plugin, 81 | shadowsocks_plugin_opts: uri.plugin_opts 82 | }; 83 | } 84 | } else if (type(uri) === 'string') { 85 | uri = split(trim(uri), '://'); 86 | 87 | switch (uri[0]) { 88 | case 'http': 89 | case 'https': 90 | url = parseURL('http://' + uri[1]); 91 | 92 | config = { 93 | label: url.hash ? urldecode(url.hash) : null, 94 | type: 'http', 95 | address: url.hostname, 96 | port: url.port, 97 | username: url.username ? urldecode(url.username) : null, 98 | password: url.password ? urldecode(url.password) : null, 99 | tls: (uri[0] === 'https') ? '1' : '0' 100 | }; 101 | 102 | break; 103 | case 'hysteria': 104 | /* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */ 105 | url = parseURL('http://' + uri[1]); 106 | params = url.searchParams; 107 | 108 | if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) { 109 | log(sprintf('Skipping unsupported %s node: %s.', 'hysteria', urldecode(url.hash) || url.hostname)); 110 | if (!sing_features.with_quic) 111 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 112 | 113 | return null; 114 | } 115 | 116 | config = { 117 | label: url.hash ? urldecode(url.hash) : null, 118 | type: 'hysteria', 119 | address: url.hostname, 120 | port: url.port, 121 | hysteria_protocol: params.protocol || 'udp', 122 | hysteria_auth_type: params.auth ? 'string' : null, 123 | hysteria_auth_payload: params.auth, 124 | hysteria_obfs_password: params.obfsParam, 125 | hysteria_down_mbps: params.downmbps, 126 | hysteria_up_mbps: params.upmbps, 127 | tls: '1', 128 | tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0', 129 | tls_sni: params.peer, 130 | tls_alpn: params.alpn 131 | }; 132 | 133 | break; 134 | case 'hysteria2': 135 | case 'hy2': 136 | /* https://v2.hysteria.network/docs/developers/URI-Scheme/ */ 137 | url = parseURL('http://' + uri[1]); 138 | params = url.searchParams; 139 | 140 | if (!sing_features.with_quic) { 141 | log(sprintf('Skipping unsupported %s node: %s.', 'hysteria2', urldecode(url.hash) || url.hostname)); 142 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 143 | return null; 144 | } 145 | 146 | config = { 147 | label: url.hash ? urldecode(url.hash) : null, 148 | type: 'hysteria2', 149 | address: url.hostname, 150 | port: url.port, 151 | password: url.username ? ( 152 | urldecode(url.username + (url.password ? (':' + url.password) : '')) 153 | ) : null, 154 | hysteria_obfs_type: params.obfs, 155 | hysteria_obfs_password: params['obfs-password'], 156 | tls: '1', 157 | tls_insecure: params.insecure ? '1' : '0', 158 | tls_sni: params.sni 159 | }; 160 | 161 | break; 162 | case 'socks': 163 | case 'socks4': 164 | case 'socks4a': 165 | case 'socsk5': 166 | case 'socks5h': 167 | url = parseURL('http://' + uri[1]); 168 | 169 | config = { 170 | label: url.hash ? urldecode(url.hash) : null, 171 | type: 'socks', 172 | address: url.hostname, 173 | port: url.port, 174 | username: url.username ? urldecode(url.username) : null, 175 | password: url.password ? urldecode(url.password) : null, 176 | socks_version: (match(uri[0], /4/)) ? '4' : '5' 177 | }; 178 | 179 | break; 180 | case 'ss': 181 | /* "Lovely" Shadowrocket format */ 182 | const ss_suri = split(uri[1], '#'); 183 | let ss_slabel = ''; 184 | if (length(ss_suri) <= 2) { 185 | if (length(ss_suri) === 2) 186 | ss_slabel = '#' + urlencode(ss_suri[1]); 187 | if (decodeBase64Str(ss_suri[0])) 188 | uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel; 189 | } 190 | 191 | /* Legacy format is not supported, it should be never appeared in modern subscriptions */ 192 | /* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */ 193 | 194 | /* SIP002 format https://shadowsocks.org/guide/sip002.html */ 195 | url = parseURL('http://' + uri[1]); 196 | 197 | let ss_userinfo = {}; 198 | if (url.username && url.password) 199 | /* User info encoded with URIComponent */ 200 | ss_userinfo = [url.username, urldecode(url.password)]; 201 | else if (url.username) 202 | /* User info encoded with base64 */ 203 | ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':'); 204 | 205 | let ss_plugin, ss_plugin_opts; 206 | if (url.search && url.searchParams.plugin) { 207 | const ss_plugin_info = split(url.searchParams.plugin, ';'); 208 | ss_plugin = ss_plugin_info[0]; 209 | if (ss_plugin === 'simple-obfs') 210 | /* Fix non-standard plugin name */ 211 | ss_plugin = 'obfs-local'; 212 | ss_plugin_opts = slice(ss_plugin_info, 1) ? join(';', slice(ss_plugin_info, 1)) : null; 213 | } 214 | 215 | config = { 216 | label: url.hash ? urldecode(url.hash) : null, 217 | type: 'shadowsocks', 218 | address: url.hostname, 219 | port: url.port, 220 | shadowsocks_encrypt_method: ss_userinfo[0], 221 | password: ss_userinfo[1], 222 | shadowsocks_plugin: ss_plugin, 223 | shadowsocks_plugin_opts: ss_plugin_opts 224 | }; 225 | 226 | break; 227 | case 'trojan': 228 | /* https://p4gefau1t.github.io/trojan-go/developer/url/ */ 229 | url = parseURL('http://' + uri[1]); 230 | params = url.searchParams || {}; 231 | 232 | config = { 233 | label: url.hash ? urldecode(url.hash) : null, 234 | type: 'trojan', 235 | address: url.hostname, 236 | port: url.port, 237 | password: urldecode(url.username), 238 | transport: (params.type !== 'tcp') ? params.type : null, 239 | tls: '1', 240 | tls_sni: params.sni 241 | }; 242 | switch(params.type) { 243 | case 'grpc': 244 | config.grpc_servicename = params.serviceName; 245 | break; 246 | case 'ws': 247 | config.ws_host = params.host ? urldecode(params.host) : null; 248 | config.ws_path = params.path ? urldecode(params.path) : null; 249 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 250 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 251 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 252 | config.ws_path = split(config.ws_path, '?ed=')[0]; 253 | } 254 | break; 255 | } 256 | 257 | break; 258 | case 'tuic': 259 | /* https://github.com/daeuniverse/dae/discussions/182 */ 260 | url = parseURL('http://' + uri[1]); 261 | params = url.searchParams || {}; 262 | 263 | if (!sing_features.with_quic) { 264 | log(sprintf('Skipping unsupported %s node: %s.', 'TUIC', urldecode(url.hash) || url.hostname)); 265 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 266 | 267 | return null; 268 | } 269 | 270 | config = { 271 | label: url.hash ? urldecode(url.hash) : null, 272 | type: 'tuic', 273 | address: url.hostname, 274 | port: url.port, 275 | uuid: url.username, 276 | password: url.password ? urldecode(url.password) : null, 277 | tuic_congestion_control: params.congestion_control, 278 | tuic_udp_relay_mode: params.udp_relay_mode, 279 | tls: '1', 280 | tls_sni: params.sni, 281 | tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null, 282 | }; 283 | 284 | break; 285 | case 'vless': 286 | /* https://github.com/XTLS/Xray-core/discussions/716 */ 287 | url = parseURL('http://' + uri[1]); 288 | params = url.searchParams; 289 | 290 | /* Unsupported protocol */ 291 | if (params.type === 'kcp') { 292 | log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname)); 293 | return null; 294 | } else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) { 295 | log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname)); 296 | if (!sing_features.with_quic) 297 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 298 | 299 | return null; 300 | } 301 | 302 | config = { 303 | label: url.hash ? urldecode(url.hash) : null, 304 | type: 'vless', 305 | address: url.hostname, 306 | port: url.port, 307 | uuid: url.username, 308 | transport: (params.type !== 'tcp') ? params.type : null, 309 | tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0', 310 | tls_sni: params.sni, 311 | tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null, 312 | tls_reality: (params.security === 'reality') ? '1' : '0', 313 | tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null, 314 | tls_reality_short_id: params.sid, 315 | tls_utls: sing_features.with_utls ? params.fp : null, 316 | vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null 317 | }; 318 | switch(params.type) { 319 | case 'grpc': 320 | config.grpc_servicename = params.serviceName; 321 | break; 322 | case 'http': 323 | case 'tcp': 324 | if (params.type === 'http' || params.headerType === 'http') { 325 | config.http_host = params.host ? split(urldecode(params.host), ',') : null; 326 | config.http_path = params.path ? urldecode(params.path) : null; 327 | } 328 | break; 329 | case 'ws': 330 | config.ws_host = params.host ? urldecode(params.host) : null; 331 | config.ws_path = params.path ? urldecode(params.path) : null; 332 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 333 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 334 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 335 | config.ws_path = split(config.ws_path, '?ed=')[0]; 336 | } 337 | break; 338 | } 339 | 340 | break; 341 | case 'vmess': 342 | /* "Lovely" shadowrocket format */ 343 | if (match(uri, /&/)) { 344 | log(sprintf('Skipping unsupported %s format.', 'VMess')); 345 | return null; 346 | } 347 | 348 | /* https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) */ 349 | try { 350 | uri = json(decodeBase64Str(uri[1])); 351 | } catch(e) { 352 | log(sprintf('Skipping unsupported %s format.', 'VMess')); 353 | return null; 354 | } 355 | 356 | if (uri.v != '2') { 357 | log(sprintf('Skipping unsupported %s format.', 'VMess')); 358 | return null; 359 | /* Unsupported protocol */ 360 | } else if (uri.net === 'kcp') { 361 | log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add)); 362 | return null; 363 | } else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) { 364 | log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add)); 365 | if (!sing_features.with_quic) 366 | log(sprintf('Please rebuild sing-box with %s support!', 'QUIC')); 367 | 368 | return null; 369 | } 370 | /* 371 | * 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 372 | * else if (uri.aid && int(uri.aid) !== 0) { 373 | * log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add)); 374 | * return null; 375 | * } 376 | */ 377 | 378 | config = { 379 | label: uri.ps ? urldecode(uri.ps) : null, 380 | type: 'vmess', 381 | address: uri.add, 382 | port: uri.port, 383 | uuid: uri.id, 384 | vmess_alterid: uri.aid, 385 | vmess_encrypt: uri.scy || 'auto', 386 | vmess_global_padding: '1', 387 | transport: (uri.net !== 'tcp') ? uri.net : null, 388 | tls: (uri.tls === 'tls') ? '1' : '0', 389 | tls_sni: uri.sni || uri.host, 390 | tls_alpn: uri.alpn ? split(uri.alpn, ',') : null 391 | }; 392 | switch (uri.net) { 393 | case 'grpc': 394 | config.grpc_servicename = uri.path; 395 | break; 396 | case 'h2': 397 | case 'tcp': 398 | if (uri.net === 'h2' || uri.type === 'http') { 399 | config.transport = 'http'; 400 | config.http_host = uri.host ? uri.host.split(',') : null; 401 | config.http_path = uri.path; 402 | } 403 | break; 404 | case 'ws': 405 | config.ws_host = uri.host; 406 | config.ws_path = uri.path; 407 | if (config.ws_path && match(config.ws_path, /\?ed=/)) { 408 | config.websocket_early_data_header = 'Sec-WebSocket-Protocol'; 409 | config.websocket_early_data = split(config.ws_path, '?ed=')[1]; 410 | config.ws_path = split(config.ws_path, '?ed=')[0]; 411 | } 412 | break; 413 | } 414 | 415 | break; 416 | } 417 | } 418 | 419 | if (!isEmpty(config)) { 420 | if (config.address) 421 | config.address = replace(config.address, /\[|\]/g, ''); 422 | 423 | if (!validation('host', config.address) || !validation('port', config.port)) { 424 | log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL')); 425 | return null; 426 | } else if (!config.label) 427 | config.label = (validation('ip6addr', config.address) ? 428 | `[${config.address}]` : config.address) + ':' + config.port; 429 | } 430 | 431 | return config; 432 | } 433 | 434 | function main() { 435 | if (via_proxy !== '1') { 436 | log('Stopping service...'); 437 | init_action('homeproxy', 'stop'); 438 | } 439 | 440 | for (let url in subscription_urls) { 441 | url = replace(url, /#.*$/, ''); 442 | const groupHash = calcStringMD5(url); 443 | node_cache[groupHash] = {}; 444 | 445 | const res = wGET(url); 446 | if (isEmpty(res)) { 447 | log(sprintf('Failed to fetch resources from %s.', url)); 448 | continue; 449 | } 450 | 451 | let nodes; 452 | try { 453 | nodes = json(res).servers || json(res); 454 | 455 | /* Shadowsocks SIP008 format */ 456 | if (nodes[0].server && nodes[0].method) 457 | map(nodes, (_, i) => nodes[i].nodetype = 'sip008'); 458 | } catch(e) { 459 | nodes = decodeBase64Str(res); 460 | nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : {}; 461 | } 462 | 463 | let count = 0; 464 | for (let node in nodes) { 465 | let config; 466 | if (!isEmpty(node)) 467 | config = parse_uri(node); 468 | if (isEmpty(config)) 469 | continue; 470 | 471 | const label = config.label; 472 | config.label = null; 473 | const confHash = calcStringMD5(sprintf('%J', config)), 474 | nameHash = calcStringMD5(label); 475 | config.label = label; 476 | 477 | if (filterCheck(config.label, filter_mode, filter_keywords)) 478 | log(sprintf('Skipping blacklist node: %s.', config.label)); 479 | else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash]) 480 | log(sprintf('Skipping duplicate node: %s.', config.label)); 481 | else { 482 | if (config.tls === '1' && allow_insecure === '1') 483 | config.tls_insecure = '1'; 484 | if (config.type in ['vless', 'vmess']) 485 | config.packet_encoding = packet_encoding; 486 | 487 | config.grouphash = groupHash; 488 | push(node_result, []); 489 | push(node_result[length(node_result)-1], config); 490 | node_cache[groupHash][confHash] = config; 491 | node_cache[groupHash][nameHash] = config; 492 | 493 | count++; 494 | } 495 | } 496 | 497 | if (count == 0) 498 | log(sprintf('No valid node found in %s.', url)); 499 | else 500 | log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url)); 501 | } 502 | 503 | if (isEmpty(node_result)) { 504 | log('Failed to update subscriptions: no valid node found.'); 505 | 506 | if (via_proxy !== '1') { 507 | log('Starting service...'); 508 | init_action('homeproxy', 'start'); 509 | } 510 | 511 | return false; 512 | } 513 | 514 | let added = 0, removed = 0; 515 | uci.foreach(uciconfig, ucinode, (cfg) => { 516 | /* Nodes created by the user */ 517 | if (!cfg.grouphash) 518 | return null; 519 | 520 | /* Empty object - failed to fetch nodes */ 521 | if (length(node_cache[cfg.grouphash]) === 0) 522 | return null; 523 | 524 | if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) { 525 | uci.delete(uciconfig, cfg['.name']); 526 | removed++; 527 | 528 | log(sprintf('Removing node: %s.', cfg.label || cfg['name'])); 529 | } else { 530 | map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => { 531 | uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]); 532 | }); 533 | node_cache[cfg.grouphash][cfg['.name']].isExisting = true; 534 | } 535 | }); 536 | for (let nodes in node_result) 537 | map(nodes, (node) => { 538 | if (node.isExisting) 539 | return null; 540 | 541 | const nameHash = calcStringMD5(node.label); 542 | uci.set(uciconfig, nameHash, 'node'); 543 | map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v])); 544 | 545 | added++; 546 | log(sprintf('Adding node: %s.', node.label)); 547 | }); 548 | uci.commit(uciconfig); 549 | 550 | let need_restart = (via_proxy !== '1'); 551 | if (!isEmpty(main_node)) { 552 | const first_server = uci.get_first(uciconfig, ucinode); 553 | if (first_server) { 554 | if (!uci.get(uciconfig, main_node)) { 555 | uci.set(uciconfig, ucimain, 'main_node', first_server); 556 | uci.commit(uciconfig); 557 | need_restart = true; 558 | 559 | log('Main node is gone, switching to the first node.'); 560 | } 561 | 562 | if (!isEmpty(main_udp_node) && main_udp_node !== 'same') { 563 | if (!uci.get(uciconfig, main_udp_node)) { 564 | uci.set(uciconfig, ucimain, 'main_udp_node', first_server); 565 | uci.commit(uciconfig); 566 | need_restart = true; 567 | 568 | log('Main UDP node is gone, switching to the first node.'); 569 | } 570 | } 571 | } else { 572 | uci.set(uciconfig, ucimain, 'main_node', 'nil'); 573 | uci.set(uciconfig, ucimain, 'main_udp_node', 'nil'); 574 | uci.commit(uciconfig); 575 | need_restart = true; 576 | 577 | log('No available node, disable tproxy.'); 578 | } 579 | } 580 | 581 | if (need_restart) { 582 | log('Restarting service...'); 583 | init_action('homeproxy', 'stop'); 584 | init_action('homeproxy', 'start'); 585 | } 586 | 587 | log(sprintf('%s nodes added, %s removed.', added, removed)); 588 | log('Successfully updated subscriptions.'); 589 | } 590 | 591 | if (!isEmpty(subscription_urls)) 592 | try { 593 | call(main); 594 | } catch(e) { 595 | log('[FATAL ERROR] An error occurred during updating subscriptions:'); 596 | log(sprintf('%s: %s', e.type, e.message)); 597 | log(e.stacktrace[0].context); 598 | 599 | log('Restarting service...'); 600 | init_action('homeproxy', 'stop'); 601 | init_action('homeproxy', 'start'); 602 | } 603 | -------------------------------------------------------------------------------- /root/etc/homeproxy/scripts/firewall_post.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl 2 | 3 | {%- 4 | 'use strict'; 5 | 6 | import { readfile } from 'fs'; 7 | import { cursor } from 'uci'; 8 | import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc'; 9 | 10 | const fw4 = require('fw4'); 11 | 12 | function array_to_nftarr(array) { 13 | if (type(array) !== 'array') 14 | return null; 15 | 16 | return `{ ${join(', ', uniq(array))} }`; 17 | } 18 | 19 | function resolve_ipv6(str) { 20 | if (isEmpty(str)) 21 | return null; 22 | 23 | let ipv6 = fw4.parse_subnet(str)?.[0]; 24 | if (!ipv6 || ipv6.family !== 6) 25 | return null; 26 | 27 | if (ipv6.bits > -1) 28 | return `${ipv6.addr}/${ipv6.bits}`; 29 | else 30 | return `& ${ipv6.mask} == ${ipv6.addr}`; 31 | } 32 | 33 | /* Misc config */ 34 | const resources_dir = '/etc/homeproxy/resources'; 35 | 36 | /* UCI config start */ 37 | const cfgname = 'homeproxy'; 38 | const uci = cursor(); 39 | uci.load(cfgname); 40 | 41 | const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china'; 42 | let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic; 43 | 44 | if (routing_mode !== 'custom') { 45 | outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil'; 46 | outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil'; 47 | china_dns_server = uci.get(cfgname, 'config', 'china_dns_server'); 48 | } else { 49 | outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil'; 50 | bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0'; 51 | } 52 | 53 | let routing_port = uci.get(cfgname, 'config', 'routing_port') || 'common'; 54 | if (routing_port === 'common') 55 | routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418'; 56 | 57 | const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy', 58 | ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0'; 59 | 60 | let self_mark, redirect_port, 61 | tproxy_port, tproxy_mark, 62 | tun_name, tun_mark; 63 | 64 | if (match(proxy_mode, /redirect/)) { 65 | self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100'; 66 | redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331'; 67 | } 68 | if (match(proxy_mode, /tproxy/)) 69 | if (outbound_udp_node !== 'nil' || routing_mode === 'custom') { 70 | tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332'; 71 | tproxy_mark = uci.get(cfgname, 'infra', 'tproxy_mark') || '101'; 72 | } 73 | if (match(proxy_mode, /tun/)) { 74 | tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0'; 75 | tun_mark = uci.get(cfgname, 'infra', 'tun_mark') || '102'; 76 | } 77 | 78 | const control_options = [ 79 | "listen_interfaces", "lan_proxy_mode", 80 | "lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips", 81 | "lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips", 82 | "lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips", 83 | "lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips", 84 | "wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips", 85 | "wan_direct_ipv4_ips", "wan_direct_ipv6_ips" 86 | ]; 87 | const control_info = {}; 88 | 89 | for (let i in control_options) 90 | control_info[i] = uci.get(cfgname, 'control', i); 91 | /* UCI config end */ 92 | -%} 93 | 94 | {# Reserved addresses -#} 95 | set homeproxy_local_addr_v4 { 96 | type ipv4_addr 97 | flags interval 98 | auto-merge 99 | elements = { 100 | 0.0.0.0/8, 101 | 10.0.0.0/8, 102 | 100.64.0.0/10, 103 | 127.0.0.0/8, 104 | 169.254.0.0/16, 105 | 172.16.0.0/12, 106 | 192.0.0.0/24, 107 | 192.0.2.0/24, 108 | 192.31.196.0/24, 109 | 192.52.193.0/24, 110 | 192.88.99.0/24, 111 | 192.168.0.0/16, 112 | 192.175.48.0/24, 113 | 198.18.0.0/15, 114 | 198.51.100.0/24, 115 | 203.0.113.0/24, 116 | 224.0.0.0/4, 117 | 240.0.0.0/4 118 | } 119 | } 120 | {% if (ipv6_support === '1'): %} 121 | set homeproxy_local_addr_v6 { 122 | type ipv6_addr 123 | flags interval 124 | auto-merge 125 | elements = { 126 | ::/128, 127 | ::1/128, 128 | ::ffff:0:0/96, 129 | 100::/64, 130 | 64:ff9b::/96, 131 | 2001::/32, 132 | 2001:10::/28, 133 | 2001:20::/28, 134 | 2001:db8::/28, 135 | 2002::/16, 136 | fc00::/7, 137 | fe80::/10, 138 | ff00::/8 139 | } 140 | } 141 | {% endif %} 142 | 143 | {% if (routing_mode === 'gfwlist'): %} 144 | set homeproxy_gfw_list_v4 { 145 | type ipv4_addr 146 | flags interval 147 | auto-merge 148 | } 149 | {% if (ipv6_support === '1'): %} 150 | set homeproxy_gfw_list_v6 { 151 | type ipv6_addr 152 | flags interval 153 | auto-merge 154 | } 155 | {% endif /* ipv6_support */ %} 156 | {% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %} 157 | set homeproxy_mainland_addr_v4 { 158 | type ipv4_addr 159 | flags interval 160 | auto-merge 161 | elements = { 162 | {% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %} 163 | {{ cnip4 }}, 164 | {% endfor %} 165 | } 166 | } 167 | {% if ((ipv6_support === '1') || china_dns_server): %} 168 | set homeproxy_mainland_addr_v6 { 169 | type ipv6_addr 170 | flags interval 171 | auto-merge 172 | elements = { 173 | {% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %} 174 | {{ cnip6 }}, 175 | {% endfor %} 176 | } 177 | } 178 | {% endif /* ipv6_support */ %} 179 | {% endif /* routing_mode */ %} 180 | 181 | {# WAN ACL addresses #} 182 | set homeproxy_wan_proxy_addr_v4 { 183 | type ipv4_addr 184 | flags interval 185 | auto-merge 186 | {% if (control_info.wan_proxy_ipv4_ips): %} 187 | elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} } 188 | {% endif %} 189 | } 190 | 191 | {% if (ipv6_support === '1'): %} 192 | set homeproxy_wan_proxy_addr_v6 { 193 | type ipv6_addr 194 | flags interval 195 | auto-merge 196 | {% if (control_info.wan_proxy_ipv6_ips): %} 197 | elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} } 198 | {% endif /* wan_proxy_ipv6_ips*/ %} 199 | } 200 | {% endif /* ipv6_support */ %} 201 | 202 | set homeproxy_wan_direct_addr_v4 { 203 | type ipv4_addr 204 | flags interval 205 | auto-merge 206 | {% if (control_info.wan_direct_ipv4_ips): %} 207 | elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} } 208 | {% endif %} 209 | } 210 | 211 | {% if (ipv6_support === '1'): %} 212 | set homeproxy_wan_direct_addr_v6 { 213 | type ipv6_addr 214 | flags interval 215 | auto-merge 216 | {% if (control_info.wan_direct_ipv6_ips): %} 217 | elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} } 218 | {% endif /* wan_direct_ipv6_ips */ %} 219 | } 220 | {% endif /* ipv6_support */ %} 221 | 222 | {% if (routing_port !== 'all'): %} 223 | set homeproxy_routing_port { 224 | type inet_service 225 | flags interval 226 | auto-merge 227 | elements = { {{ join(', ', split(routing_port, ',')) }} } 228 | } 229 | {% endif %} 230 | 231 | {# TCP redirect #} 232 | {% if (match(proxy_mode, /redirect/)): %} 233 | chain homeproxy_redirect_proxy { 234 | meta l4proto tcp counter redirect to :{{ redirect_port }} 235 | } 236 | 237 | chain homeproxy_redirect_proxy_port { 238 | {% if (routing_port !== 'all'): %} 239 | tcp dport != @homeproxy_routing_port counter return 240 | {% endif %} 241 | goto homeproxy_redirect_proxy 242 | } 243 | 244 | chain homeproxy_redirect_lanac { 245 | {% if (control_info.listen_interfaces): %} 246 | meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return 247 | {% endif %} 248 | meta mark {{ self_mark }} counter return 249 | 250 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 251 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 252 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect 253 | {% endif /* lan_proxy_ipv4_ips */ %} 254 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 255 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect 256 | {% endfor /* lan_proxy_ipv6_ips */ %} 257 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 258 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect 259 | {% endif /* lan_proxy_mac_addrs */ %} 260 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 261 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 262 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 263 | {% endif /* lan_direct_ipv4_ips */ %} 264 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 265 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 266 | {% endfor /* lan_direct_ipv6_ips */ %} 267 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 268 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 269 | {% endif /* lan_direct_mac_addrs */ %} 270 | {% endif /* lan_proxy_mode */ %} 271 | 272 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 273 | counter goto homeproxy_redirect 274 | {% endif %} 275 | } 276 | 277 | chain homeproxy_redirect { 278 | meta mark {{ self_mark }} counter return 279 | 280 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port 281 | {% if (ipv6_support === '1'): %} 282 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port 283 | {% endif %} 284 | 285 | ip daddr @homeproxy_local_addr_v4 counter return 286 | {% if (ipv6_support === '1'): %} 287 | ip6 daddr @homeproxy_local_addr_v6 counter return 288 | {% endif %} 289 | 290 | {% if (routing_mode !== 'custom'): %} 291 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 292 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port 293 | {% endif /* lan_global_proxy_ipv4_ips */ %} 294 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 295 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port 296 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 297 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 298 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port 299 | {% endif /* lan_global_proxy_mac_addrs */ %} 300 | {% endif /* routing_mode */ %} 301 | 302 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 303 | {% if (ipv6_support === '1'): %} 304 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 305 | {% endif /* ipv6_support */ %} 306 | 307 | {% if (routing_mode === 'gfwlist'): %} 308 | ip daddr != @homeproxy_gfw_list_v4 counter return 309 | {% if (ipv6_support === '1'): %} 310 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 311 | {% endif /* ipv6_support */ %} 312 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 313 | ip daddr @homeproxy_mainland_addr_v4 counter return 314 | {% if (ipv6_support === '1'): %} 315 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 316 | {% endif /* ipv6_support */ %} 317 | {% elif (routing_mode === 'proxy_mainland_china'): %} 318 | ip daddr != @homeproxy_mainland_addr_v4 counter return 319 | {% if (ipv6_support === '1'): %} 320 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 321 | {% endif /* ipv6_support */ %} 322 | {% endif /* routing_mode */ %} 323 | 324 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 325 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy 326 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 327 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 328 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy 329 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 330 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 331 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy 332 | {% endif /* lan_gaming_mode_mac_addrs */ %} 333 | 334 | counter goto homeproxy_redirect_proxy_port 335 | } 336 | 337 | chain homeproxy_output_redir { 338 | type nat hook output priority filter -105; policy accept 339 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect 340 | } 341 | 342 | chain dstnat { 343 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac 344 | } 345 | {% endif %} 346 | 347 | {# UDP tproxy #} 348 | {% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %} 349 | chain homeproxy_mangle_tproxy { 350 | meta l4proto udp mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept 351 | {% if (ipv6_support === '1'): %} 352 | meta l4proto udp mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept 353 | {% endif %} 354 | } 355 | 356 | chain homeproxy_mangle_tproxy_port { 357 | {% if (routing_port !== 'all'): %} 358 | udp dport != @homeproxy_routing_port counter return 359 | {% endif %} 360 | goto homeproxy_mangle_tproxy 361 | } 362 | 363 | chain homeproxy_mangle_mark { 364 | {% if (routing_port !== 'all'): %} 365 | udp dport != @homeproxy_routing_port counter return 366 | {% endif %} 367 | meta l4proto udp mark set {{ tproxy_mark }} counter accept 368 | } 369 | 370 | chain homeproxy_mangle_lanac { 371 | {% if (control_info.listen_interfaces): %} 372 | meta iifname != {{ array_to_nftarr(split(join(' ', control_info.listen_interfaces) + ' lo', ' ')) }} counter return 373 | {% endif %} 374 | meta mark {{ self_mark }} counter return 375 | 376 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 377 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 378 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting 379 | {% endif /* lan_proxy_ipv4_ips */ %} 380 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 381 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting 382 | {% endfor /* lan_proxy_ipv6_ips */ %} 383 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 384 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting 385 | {% endif /* lan_proxy_mac_addrs */ %} 386 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 387 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 388 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 389 | {% endif /* lan_direct_ipv4_ips */ %} 390 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 391 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 392 | {% endfor /* lan_direct_ipv6_ips */ %} 393 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 394 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 395 | {% endif /* lan_direct_mac_addrs */ %} 396 | {% endif /* lan_proxy_mode */ %} 397 | 398 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 399 | counter goto homeproxy_mangle_prerouting 400 | {% endif %} 401 | } 402 | 403 | chain homeproxy_mangle_prerouting { 404 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port 405 | {% if (ipv6_support === '1'): %} 406 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port 407 | {% endif %} 408 | 409 | ip daddr @homeproxy_local_addr_v4 counter return 410 | {% if (ipv6_support === '1'): %} 411 | ip6 daddr @homeproxy_local_addr_v6 counter return 412 | {% endif %} 413 | 414 | {% if (routing_mode !== 'custom'): %} 415 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 416 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port 417 | {% endif /* lan_global_proxy_ipv4_ips */ %} 418 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 419 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port 420 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 421 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 422 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port 423 | {% endif /* lan_global_proxy_mac_addrs */ %} 424 | {% endif /* routing_mode */ %} 425 | 426 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 427 | {% if (ipv6_support === '1'): %} 428 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 429 | {% endif /* ipv6_support */ %} 430 | 431 | {% if (routing_mode === 'gfwlist'): %} 432 | ip daddr != @homeproxy_gfw_list_v4 counter return 433 | {% if (ipv6_support === '1'): %} 434 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 435 | {% endif /* ipv6_support */ %} 436 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 437 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 438 | ip daddr @homeproxy_mainland_addr_v4 counter return 439 | {% if (ipv6_support === '1'): %} 440 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 441 | {% endif /* ipv6_support */ %} 442 | {% if (routing_mode !== 'custom'): %} 443 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 444 | {% endif /* routing_mode */ %} 445 | {% elif (routing_mode === 'proxy_mainland_china'): %} 446 | ip daddr != @homeproxy_mainland_addr_v4 counter return 447 | {% if (ipv6_support === '1'): %} 448 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 449 | {% endif /* ipv6_support */ %} 450 | {% endif /* routing_mode */ %} 451 | 452 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 453 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy 454 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 455 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 456 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy 457 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 458 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 459 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy 460 | {% endif /* lan_gaming_mode_mac_addrs */ %} 461 | 462 | counter goto homeproxy_mangle_tproxy_port 463 | } 464 | 465 | chain homeproxy_mangle_output { 466 | meta mark {{ self_mark }} counter return 467 | 468 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark 469 | {% if (ipv6_support === '1'): %} 470 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark 471 | {% endif %} 472 | 473 | ip daddr @homeproxy_local_addr_v4 counter return 474 | {% if (ipv6_support === '1'): %} 475 | ip6 daddr @homeproxy_local_addr_v6 counter return 476 | {% endif %} 477 | 478 | ip daddr @homeproxy_wan_direct_addr_v4 counter return 479 | {% if (ipv6_support === '1'): %} 480 | ip6 daddr @homeproxy_wan_direct_addr_v6 counter return 481 | {% endif /* ipv6_support */ %} 482 | 483 | {% if (routing_mode === 'gfwlist'): %} 484 | ip daddr != @homeproxy_gfw_list_v4 counter return 485 | {% if (ipv6_support === '1'): %} 486 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 487 | {% endif /* ipv6_support */ %} 488 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 489 | ip daddr @homeproxy_mainland_addr_v4 counter return 490 | {% if (ipv6_support === '1'): %} 491 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 492 | {% endif /* ipv6_support */ %} 493 | {% elif (routing_mode === 'proxy_mainland_china'): %} 494 | ip daddr != @homeproxy_mainland_addr_v4 counter return 495 | {% if (ipv6_support === '1'): %} 496 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 497 | {% endif /* ipv6_support */ %} 498 | {% endif /* routing_mode */ %} 499 | 500 | counter goto homeproxy_mangle_mark 501 | } 502 | 503 | chain mangle_prerouting { 504 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac 505 | } 506 | 507 | chain mangle_output { 508 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output 509 | } 510 | {% endif %} 511 | 512 | {# TUN #} 513 | {% if (match(proxy_mode, /tun/)): %} 514 | chain homeproxy_mangle_lanac { 515 | iifname {{ tun_name }} counter return 516 | 517 | {% if (control_info.listen_interfaces): %} 518 | meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return 519 | {% endif %} 520 | 521 | {% if (control_info.lan_proxy_mode === 'listed_only'): %} 522 | {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} 523 | ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun 524 | {% endif /* lan_proxy_ipv4_ips */ %} 525 | {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} 526 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun 527 | {% endfor /* lan_proxy_ipv6_ips */ %} 528 | {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} 529 | ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun 530 | {% endif /* lan_proxy_mac_addrs */ %} 531 | {% elif (control_info.lan_proxy_mode === 'except_listed'): %} 532 | {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} 533 | ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return 534 | {% endif /* lan_direct_ipv4_ips */ %} 535 | {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} 536 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter return 537 | {% endfor /* lan_direct_ipv6_ips */ %} 538 | {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} 539 | ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return 540 | {% endif /* lan_direct_mac_addrs */ %} 541 | {% endif /* lan_proxy_mode */ %} 542 | 543 | {% if (control_info.lan_proxy_mode !== 'listed_only'): %} 544 | counter goto homeproxy_mangle_tun 545 | {% endif %} 546 | } 547 | 548 | chain homeproxy_mangle_tun_mark { 549 | {% if (routing_port !== 'all'): %} 550 | {% if (proxy_mode === 'tun'): %} 551 | tcp dport != @homeproxy_routing_port counter return 552 | {% endif /* proxy_mode */ %} 553 | udp dport != @homeproxy_routing_port counter return 554 | {% endif /* routing_port */ %} 555 | 556 | counter mark set {{ tun_mark }} 557 | } 558 | 559 | chain homeproxy_mangle_tun { 560 | iifname {{ tun_name }} counter return 561 | 562 | ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark 563 | {% if (ipv6_support === '1'): %} 564 | ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark 565 | {% endif %} 566 | 567 | ip daddr @homeproxy_local_addr_v4 counter return 568 | {% if (ipv6_support === '1'): %} 569 | ip6 daddr @homeproxy_local_addr_v6 counter return 570 | {% endif %} 571 | 572 | {% if (routing_mode !== 'custom'): %} 573 | {% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %} 574 | ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark 575 | {% endif /* lan_global_proxy_ipv4_ips */ %} 576 | {% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %} 577 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark 578 | {% endfor /* lan_global_proxy_ipv6_ips */ %} 579 | {% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %} 580 | ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark 581 | {% endif /* lan_global_proxy_mac_addrs */ %} 582 | {% endif /* routing_mode */ %} 583 | 584 | {% if (control_info.wan_direct_ipv4_ips): %} 585 | ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return 586 | {% endif /* wan_direct_ipv4_ips */ %} 587 | {% if (control_info.wan_direct_ipv6_ips): %} 588 | ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return 589 | {% endif /* wan_direct_ipv6_ips */ %} 590 | 591 | {% if (routing_mode === 'gfwlist'): %} 592 | ip daddr != @homeproxy_gfw_list_v4 counter return 593 | {% if (ipv6_support === '1'): %} 594 | ip6 daddr != @homeproxy_gfw_list_v6 counter return 595 | {% endif /* ipv6_support */ %} 596 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 597 | {% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %} 598 | ip daddr @homeproxy_mainland_addr_v4 counter return 599 | {% if (ipv6_support === '1'): %} 600 | ip6 daddr @homeproxy_mainland_addr_v6 counter return 601 | {% endif /* ipv6_support */ %} 602 | {% if (routing_mode !== 'custom'): %} 603 | udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC" 604 | {% endif /* routing_mode */ %} 605 | {% elif (routing_mode === 'proxy_mainland_china'): %} 606 | ip daddr != @homeproxy_mainland_addr_v4 counter return 607 | {% if (ipv6_support === '1'): %} 608 | ip6 daddr != @homeproxy_mainland_addr_v6 counter return 609 | {% endif /* ipv6_support */ %} 610 | {% endif /* routing_mode */ %} 611 | 612 | {% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %} 613 | ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter mark set {{ tun_mark }} 614 | {% endif /* lan_gaming_mode_ipv4_ips */ %} 615 | {% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %} 616 | ip6 saddr {{ resolve_ipv6(ipv6) }} counter mark set {{ tun_mark }} 617 | {% endfor /* lan_gaming_mode_ipv6_ips */ %} 618 | {% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %} 619 | ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter mark set {{ tun_mark }} 620 | {% endif /* lan_gaming_mode_mac_addrs */ %} 621 | 622 | counter goto homeproxy_mangle_tun_mark 623 | } 624 | 625 | chain mangle_prerouting { 626 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac 627 | } 628 | 629 | chain mangle_output { 630 | meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun 631 | } 632 | {% endif %} 633 | -------------------------------------------------------------------------------- /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 ImmortalWrt.org 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { readfile, writefile } from 'fs'; 11 | import { isnan } from 'math'; 12 | import { cursor } from 'uci'; 13 | 14 | import { urldecode } from 'luci.http'; 15 | 16 | import { 17 | executeCommand, shellQuote, calcStringCRC8, calcStringMD5, isEmpty, strToBool, strToInt, 18 | removeBlankAttrs, parseURL, validateHostname, validation, filterCheck, 19 | HP_DIR, RUN_DIR 20 | } from 'homeproxy'; 21 | 22 | /* UCI config start */ 23 | const uci = cursor(); 24 | 25 | const uciconfig = 'homeproxy'; 26 | uci.load(uciconfig); 27 | 28 | const uciinfra = 'infra', 29 | ucimain = 'config', 30 | ucisub = 'subscription', 31 | uciexp = 'experimental', 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 = executeCommand('ifstatus wan | jsonfilter -e \'@["dns-server"][0]\''); 48 | if (wan_dns.exitcode === 0 && trim(wan_dns.stdout)) 49 | wan_dns = trim(wan_dns.stdout); 50 | else 51 | wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '208.67.222.222' : '114.114.114.114'; 52 | 53 | const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333'; 54 | 55 | let main_node, main_udp_node, dedicated_udp_node, default_outbound, sniff_override = '1', 56 | dns_server, dns_default_strategy, dns_default_server, dns_disable_cache, dns_disable_cache_expire, 57 | dns_independent_cache, dns_client_subnet, direct_domain_list, proxy_domain_list; 58 | 59 | if (routing_mode !== 'custom') { 60 | main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil'; 61 | main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil'; 62 | dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]); 63 | 64 | dns_server = uci.get(uciconfig, ucimain, 'dns_server'); 65 | if (isEmpty(dns_server) || dns_server === 'wan') 66 | dns_server = wan_dns; 67 | 68 | direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt')); 69 | if (direct_domain_list) 70 | direct_domain_list = split(direct_domain_list, /[\r\n]/); 71 | 72 | proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt')); 73 | if (proxy_domain_list) 74 | proxy_domain_list = split(proxy_domain_list, /[\r\n]/); 75 | } else { 76 | /* DNS settings */ 77 | dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy'); 78 | dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server'); 79 | dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache'); 80 | dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire'); 81 | dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache'); 82 | dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet'); 83 | 84 | /* Routing settings */ 85 | default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil'; 86 | sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override'); 87 | } 88 | 89 | const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy', 90 | ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0', 91 | default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface'); 92 | 93 | const cache_file_store_rdrc = uci.get(uciconfig, uciexp, 'cache_file_store_rdrc'), 94 | cache_file_rdrc_timeout = uci.get(uciconfig, uciexp, 'cache_file_rdrc_timeout'); 95 | 96 | const clash_api_enabled = uci.get(uciconfig, uciexp, 'clash_api_enabled'), 97 | nginx_support = uci.get(uciconfig, uciexp, 'nginx_support'), 98 | clash_api_log_level = uci.get(uciconfig, uciexp, 'clash_api_log_level') || 'warn', 99 | dashboard_repo = uci.get(uciconfig, uciexp, 'dashboard_repo'), 100 | clash_api_port = uci.get(uciconfig, uciexp, 'clash_api_port') || '9090', 101 | clash_api_secret = uci.get(uciconfig, uciexp, 'clash_api_secret') || trim(readfile('/proc/sys/kernel/random/uuid')); 102 | 103 | const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330'; 104 | let self_mark, redirect_port, tproxy_port, 105 | tun_name, tun_addr4, tun_addr6, tun_mtu, tun_gso, 106 | tcpip_stack, endpoint_independent_nat, udp_timeout; 107 | udp_timeout = uci.get(uciconfig, 'infra', 'udp_timeout'); 108 | if (routing_mode === 'custom') 109 | udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout'); 110 | if (match(proxy_mode, /redirect/)) { 111 | self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100'; 112 | redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331'; 113 | } 114 | if (match(proxy_mode), /tproxy/) 115 | if (main_udp_node !== 'nil' || routing_mode === 'custom') 116 | tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332'; 117 | if (match(proxy_mode), /tun/) { 118 | tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0'; 119 | tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30'; 120 | tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126'; 121 | tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000'; 122 | tun_gso = uci.get(uciconfig, uciinfra, 'tun_gso') || '0'; 123 | tcpip_stack = 'system'; 124 | if (routing_mode === 'custom') { 125 | tun_gso = uci.get(uciconfig, uciroutingsetting, 'tun_gso') || '0'; 126 | tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system'; 127 | endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat'); 128 | } 129 | } 130 | 131 | let subs_info = {}; 132 | { 133 | const suburls = uci.get(uciconfig, ucisub, 'subscription_url') || []; 134 | for (let i = 0; i < length(suburls); i++) { 135 | const url = parseURL(suburls[i]); 136 | const urlhash = calcStringMD5(replace(suburls[i], /#.*$/, '')); 137 | subs_info[urlhash] = { 138 | "url": replace(suburls[i], /#.*$/, ''), 139 | "name": url.hash ? urldecode(url.hash) : url.hostname 140 | }; 141 | } 142 | } 143 | 144 | let checkedout_nodes = [], 145 | nodes_tobe_checkedout = [], 146 | checkedout_groups = [], 147 | groups_tobe_checkedout = []; 148 | /* UCI config end */ 149 | 150 | /* Config helper start */ 151 | function parse_port(strport) { 152 | if (type(strport) !== 'array' || isEmpty(strport)) 153 | return null; 154 | 155 | let ports = []; 156 | for (let i in strport) 157 | push(ports, int(i)); 158 | 159 | return ports; 160 | 161 | } 162 | 163 | function parse_dnsquery(strquery) { 164 | if (type(strquery) !== 'array' || isEmpty(strquery)) 165 | return null; 166 | 167 | let querys = []; 168 | for (let i in strquery) 169 | isnan(int(i)) ? push(querys, i) : push(querys, int(i)); 170 | 171 | return querys; 172 | 173 | } 174 | 175 | function get_tag(cfg, failback_tag, filterable) { 176 | if (isEmpty(cfg)) 177 | return null; 178 | 179 | let node = {}; 180 | if (type(cfg) === 'object') 181 | node = cfg; 182 | else { 183 | if (cfg in ['direct-out', 'block-out']) 184 | return cfg; 185 | else 186 | node = uci.get_all(uciconfig, cfg); 187 | } 188 | 189 | //filter check 190 | if (!isEmpty(filterable)) 191 | if (filterCheck(node.label, filterable.filter_nodes, filterable.filter_keywords)) 192 | return null; 193 | 194 | const sub_info = subs_info[node.grouphash]; 195 | return node.label ? sprintf("%s%s", node.grouphash ? 196 | sprintf("[%s] ", sub_info ? sub_info.name : calcStringCRC8(node.grouphash)) : '', 197 | node.label) : 198 | (failback_tag || null); 199 | } 200 | 201 | function generate_outbound(node) { 202 | if (type(node) !== 'object' || isEmpty(node)) 203 | return null; 204 | 205 | push(checkedout_nodes, node['.name']); 206 | 207 | if (node.type in ['selector', 'urltest']) { 208 | let outbounds = []; 209 | for (let grouphash in node.group) { 210 | if (!isEmpty(grouphash)) { 211 | const output = executeCommand(`/sbin/uci -q show ${shellQuote(uciconfig)} | /bin/grep "\.grouphash='*${shellQuote(grouphash)}'*" | /usr/bin/cut -f2 -d'.'`) || {}; 212 | if (!isEmpty(trim(output.stdout))) 213 | for (let order in split(trim(output.stdout), /\n/)) 214 | push(outbounds, get_tag(order, 'cfg-' + order + '-out', { "filter_nodes": node.filter_nodes, "filter_keywords": node.filter_keywords })); 215 | if (!(grouphash in groups_tobe_checkedout)) 216 | push(groups_tobe_checkedout, grouphash); 217 | } 218 | } 219 | for (let order in node.order) { 220 | push(outbounds, get_tag(order, 'cfg-' + order + '-out', { "filter_nodes": node.filter_nodes, "filter_keywords": node.filter_keywords })); 221 | if (!(order in ['direct-out', 'block-out']) && !(order in nodes_tobe_checkedout)) 222 | push(nodes_tobe_checkedout, order); 223 | } 224 | if (length(outbounds) === 0) 225 | push(outbounds, 'direct-out', 'block-out'); 226 | return { 227 | type: node.type, 228 | tag: get_tag(node, 'cfg-' + node['.name'] + '-out'), 229 | /* Selector */ 230 | outbounds: outbounds, 231 | default: node.default_selected ? (get_tag(node.default_selected, 'cfg-' + node.default_selected + '-out')) : null, 232 | /* URLTest */ 233 | url: node.test_url, 234 | interval: node.interval, 235 | tolerance: strToInt(node.tolerance), 236 | idle_timeout: node.idle_timeout, 237 | interrupt_exist_connections: strToBool(node.interrupt_exist_connections) 238 | }; 239 | } 240 | 241 | const outbound = { 242 | type: node.type, 243 | tag: get_tag(node, 'cfg-' + node['.name'] + '-out'), 244 | routing_mark: strToInt(self_mark), 245 | 246 | server: node.address, 247 | server_port: strToInt(node.port), 248 | 249 | username: (node.type !== 'ssh') ? node.username : null, 250 | user: (node.type === 'ssh') ? node.username : null, 251 | password: node.password, 252 | 253 | /* Direct */ 254 | override_address: node.override_address, 255 | override_port: strToInt(node.override_port), 256 | proxy_protocol: (node.proxy_protocol === '1') ? { 257 | enabled: true, 258 | version: strToInt(node.proxy_protocol_version) 259 | } : null, 260 | /* Hysteria (2) */ 261 | up_mbps: strToInt(node.hysteria_up_mbps), 262 | down_mbps: strToInt(node.hysteria_down_mbps), 263 | obfs: node.hysteria_obfs_type ? { 264 | type: node.hysteria_obfs_type, 265 | password: node.hysteria_obfs_password 266 | } : node.hysteria_obfs_password, 267 | auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null, 268 | auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null, 269 | recv_window_conn: strToInt(node.hysteria_recv_window_conn), 270 | recv_window: strToInt(node.hysteria_revc_window), 271 | disable_mtu_discovery: strToBool(node.hysteria_disable_mtu_discovery), 272 | /* Shadowsocks */ 273 | method: node.shadowsocks_encrypt_method, 274 | plugin: node.shadowsocks_plugin, 275 | plugin_opts: node.shadowsocks_plugin_opts, 276 | /* ShadowTLS / Socks */ 277 | version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null), 278 | /* SSH */ 279 | client_version: node.ssh_client_version, 280 | host_key: node.ssh_host_key, 281 | host_key_algorithms: node.ssh_host_key_algo, 282 | private_key: node.ssh_priv_key, 283 | private_key_passphrase: node.ssh_priv_key_pp, 284 | /* Tuic */ 285 | uuid: node.uuid, 286 | congestion_control: node.tuic_congestion_control, 287 | udp_relay_mode: node.tuic_udp_relay_mode, 288 | udp_over_stream: strToBool(node.tuic_udp_over_stream), 289 | zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt), 290 | heartbeat: node.tuic_heartbeat ? (node.tuic_heartbeat + 's') : null, 291 | /* VLESS / VMess */ 292 | flow: node.vless_flow, 293 | alter_id: strToInt(node.vmess_alterid), 294 | security: node.vmess_encrypt, 295 | global_padding: node.vmess_global_padding ? (node.vmess_global_padding === '1') : null, 296 | authenticated_length: node.vmess_authenticated_length ? (node.vmess_authenticated_length === '1') : null, 297 | packet_encoding: node.packet_encoding, 298 | /* WireGuard */ 299 | system_interface: (node.type === 'wireguard') || null, 300 | gso: (node.wireguard_gso === '1') || null, 301 | interface_name: (node.type === 'wireguard') ? 'wg-' + node['.name'] + '-out' : null, 302 | local_address: node.wireguard_local_address, 303 | private_key: node.wireguard_private_key, 304 | peer_public_key: node.wireguard_peer_public_key, 305 | pre_shared_key: node.wireguard_pre_shared_key, 306 | reserved: parse_port(node.wireguard_reserved), 307 | mtu: strToInt(node.wireguard_mtu), 308 | 309 | multiplex: (node.multiplex === '1') ? { 310 | enabled: true, 311 | protocol: node.multiplex_protocol, 312 | max_connections: strToInt(node.multiplex_max_connections), 313 | min_streams: strToInt(node.multiplex_min_streams), 314 | max_streams: strToInt(node.multiplex_max_streams), 315 | padding: (node.multiplex_padding === '1'), 316 | brutal: (node.multiplex_brutal === '1') ? { 317 | enabled: true, 318 | up_mbps: strToInt(node.multiplex_brutal_up), 319 | down_mbps: strToInt(node.multiplex_brutal_down) 320 | } : null 321 | } : null, 322 | tls: (node.tls === '1') ? { 323 | enabled: true, 324 | server_name: node.tls_sni, 325 | insecure: (node.tls_insecure === '1'), 326 | alpn: node.tls_alpn, 327 | min_version: node.tls_min_version, 328 | max_version: node.tls_max_version, 329 | cipher_suites: node.tls_cipher_suites, 330 | certificate_path: node.tls_cert_path, 331 | ech: (node.tls_ech === '1') ? { 332 | enabled: true, 333 | dynamic_record_sizing_disabled: (node.tls_ech_tls_disable_drs === '1'), 334 | pq_signature_schemes_enabled: (node.tls_ech_enable_pqss === '1'), 335 | config: node.tls_ech_config 336 | } : null, 337 | utls: !isEmpty(node.tls_utls) ? { 338 | enabled: true, 339 | fingerprint: node.tls_utls 340 | } : null, 341 | reality: (node.tls_reality === '1') ? { 342 | enabled: true, 343 | public_key: node.tls_reality_public_key, 344 | short_id: node.tls_reality_short_id 345 | } : null 346 | } : null, 347 | transport: !isEmpty(node.transport) ? { 348 | type: node.transport, 349 | host: node.http_host || node.httpupgrade_host, 350 | path: node.http_path || node.ws_path, 351 | headers: node.ws_host ? { 352 | Host: node.ws_host 353 | } : null, 354 | method: node.http_method, 355 | max_early_data: strToInt(node.websocket_early_data), 356 | early_data_header_name: node.websocket_early_data_header, 357 | service_name: node.grpc_servicename, 358 | idle_timeout: node.http_idle_timeout ? (node.http_idle_timeout + 's') : null, 359 | ping_timeout: node.http_ping_timeout ? (node.http_ping_timeout + 's') : null, 360 | permit_without_stream: strToBool(node.grpc_permit_without_stream) 361 | } : null, 362 | udp_over_tcp: (node.udp_over_tcp === '1') ? { 363 | enabled: true, 364 | version: strToInt(node.udp_over_tcp_version) 365 | } : null, 366 | tcp_fast_open: strToBool(node.tcp_fast_open), 367 | tcp_multi_path: strToBool(node.tcp_multi_path), 368 | udp_fragment: strToBool(node.udp_fragment) 369 | }; 370 | 371 | return outbound; 372 | } 373 | 374 | function get_outbound(cfg) { 375 | if (isEmpty(cfg)) 376 | return null; 377 | 378 | if (type(cfg) === 'array') { 379 | if ('any-out' in cfg) 380 | return 'any'; 381 | 382 | let outbounds = []; 383 | for (let i in cfg) 384 | push(outbounds, get_outbound(i)); 385 | return outbounds; 386 | } else { 387 | if (cfg in ['direct-out', 'block-out']) { 388 | return cfg; 389 | } else { 390 | const node = uci.get(uciconfig, cfg, 'node'); 391 | if (isEmpty(node)) 392 | die(sprintf("%s's node is missing, please check your configuration.", cfg)); 393 | else 394 | return get_tag(node, 'cfg-' + node + '-out'); 395 | } 396 | } 397 | } 398 | 399 | function get_resolver(cfg) { 400 | if (isEmpty(cfg)) 401 | return null; 402 | 403 | if (cfg in ['default-dns', 'system-dns', 'block-dns']) 404 | return cfg; 405 | else 406 | return 'cfg-' + cfg + '-dns'; 407 | } 408 | 409 | function get_ruleset(cfg) { 410 | if (isEmpty(cfg)) 411 | return null; 412 | 413 | let rules = []; 414 | for (let i in cfg) 415 | push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule'); 416 | return rules; 417 | } 418 | /* Config helper end */ 419 | 420 | const config = {}; 421 | 422 | /* Log */ 423 | config.log = { 424 | disabled: false, 425 | level: (clash_api_enabled === '1') ? clash_api_log_level : 'warn', 426 | output: RUN_DIR + '/sing-box-c.log', 427 | timestamp: true 428 | }; 429 | 430 | /* DNS start */ 431 | /* Default settings */ 432 | config.dns = { 433 | servers: [ 434 | { 435 | tag: 'default-dns', 436 | address: wan_dns, 437 | detour: 'direct-out' 438 | }, 439 | { 440 | tag: 'system-dns', 441 | address: 'local', 442 | detour: 'direct-out' 443 | }, 444 | { 445 | tag: 'block-dns', 446 | address: 'rcode://name_error' 447 | } 448 | ], 449 | rules: [], 450 | strategy: dns_default_strategy, 451 | disable_cache: (dns_disable_cache === '1'), 452 | disable_expire: (dns_disable_cache_expire === '1'), 453 | independent_cache: (dns_independent_cache === '1'), 454 | client_subnet: dns_client_subnet 455 | }; 456 | 457 | if (!isEmpty(main_node)) { 458 | /* Avoid DNS loop */ 459 | const main_node_addr = uci.get(uciconfig, main_node, 'address'); 460 | if (validateHostname(main_node_addr)) 461 | push(config.dns.rules, { 462 | domain: main_node_addr, 463 | server: 'default-dns' 464 | }); 465 | 466 | if (dedicated_udp_node) { 467 | const main_udp_node_addr = uci.get(uciconfig, main_udp_node, 'address'); 468 | if (validateHostname(main_udp_node_addr)) 469 | push(config.dns.rules, { 470 | domain: main_udp_node_addr, 471 | server: 'default-dns' 472 | }); 473 | } 474 | 475 | if (direct_domain_list) 476 | push(config.dns.rules, { 477 | domain_keyword: direct_domain_list, 478 | server: 'default-dns' 479 | }); 480 | 481 | /* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */ 482 | if (routing_mode === 'gfwlist' || proxy_domain_list) 483 | push(config.dns.rules, { 484 | domain_keyword: (routing_mode !== 'gfwlist') ? proxy_domain_list : null, 485 | query_type: [64, 65], 486 | server: 'block-dns' 487 | }); 488 | 489 | if (isEmpty(config.dns.rules)) 490 | config.dns.rules = null; 491 | 492 | let default_final_dns = 'default-dns'; 493 | /* Main DNS */ 494 | if (dns_server !== wan_dns) { 495 | push(config.dns.servers, { 496 | tag: 'main-dns', 497 | address: 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server), 498 | strategy: (ipv6_support !== '1') ? 'ipv4_only' : null, 499 | detour: 'main-out' 500 | }); 501 | 502 | default_final_dns = 'main-dns'; 503 | } 504 | 505 | config.dns.final = default_final_dns; 506 | } else if (!isEmpty(default_outbound)) { 507 | /* DNS servers */ 508 | uci.foreach(uciconfig, ucidnsserver, (cfg) => { 509 | if (cfg.enabled !== '1') 510 | return; 511 | 512 | push(config.dns.servers, { 513 | tag: 'cfg-' + cfg['.name'] + '-dns', 514 | address: cfg.address, 515 | address: cfg.address, 516 | address_resolver: get_resolver(cfg.address_resolver), 517 | address_strategy: cfg.address_strategy, 518 | strategy: cfg.resolve_strategy, 519 | detour: get_outbound(cfg.outbound), 520 | client_subnet: cfg.client_subnet 521 | }); 522 | }); 523 | 524 | /* DNS rules */ 525 | uci.foreach(uciconfig, ucidnsrule, (cfg) => { 526 | if (cfg.enabled !== '1') 527 | return; 528 | 529 | push(config.dns.rules, { 530 | ip_version: strToInt(cfg.ip_version), 531 | query_type: parse_dnsquery(cfg.query_type), 532 | network: cfg.network, 533 | protocol: cfg.protocol, 534 | domain: cfg.domain, 535 | domain_suffix: cfg.domain_suffix, 536 | domain_keyword: cfg.domain_keyword, 537 | domain_regex: cfg.domain_regex, 538 | port: parse_port(cfg.port), 539 | port_range: cfg.port_range, 540 | source_ip_cidr: cfg.source_ip_cidr, 541 | source_ip_is_private: (cfg.source_ip_is_private === '1') || null, 542 | ip_cidr: cfg.ip_cidr, 543 | ip_is_private: (cfg.ip_is_private === '1') || null, 544 | source_port: parse_port(cfg.source_port), 545 | source_port_range: cfg.source_port_range, 546 | process_name: cfg.process_name, 547 | process_path: cfg.process_path, 548 | user: cfg.user, 549 | clash_mode: cfg.clash_mode, 550 | rule_set: get_ruleset(cfg.rule_set), 551 | rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null, 552 | invert: (cfg.invert === '1') || null, 553 | outbound: get_outbound(cfg.outbound), 554 | server: get_resolver(cfg.server), 555 | disable_cache: (cfg.dns_disable_cache === '1') || null, 556 | rewrite_ttl: strToInt(cfg.rewrite_ttl), 557 | client_subnet: cfg.client_subnet 558 | }); 559 | }); 560 | 561 | if (isEmpty(config.dns.rules)) 562 | config.dns.rules = null; 563 | 564 | config.dns.final = get_resolver(dns_default_server); 565 | } 566 | /* DNS end */ 567 | 568 | /* Inbound start */ 569 | config.inbounds = []; 570 | 571 | push(config.inbounds, { 572 | type: 'direct', 573 | tag: 'dns-in', 574 | listen: '::', 575 | listen_port: int(dns_port) 576 | }); 577 | 578 | push(config.inbounds, { 579 | type: 'mixed', 580 | tag: 'mixed-in', 581 | listen: '::', 582 | listen_port: int(mixed_port), 583 | udp_timeout: udp_timeout ? (udp_timeout + 's') : null, 584 | sniff: true, 585 | sniff_override_destination: (sniff_override === '1'), 586 | set_system_proxy: false 587 | }); 588 | 589 | if (match(proxy_mode, /redirect/)) 590 | push(config.inbounds, { 591 | type: 'redirect', 592 | tag: 'redirect-in', 593 | 594 | listen: '::', 595 | listen_port: int(redirect_port), 596 | sniff: true, 597 | sniff_override_destination: (sniff_override === '1') 598 | }); 599 | if (match(proxy_mode, /tproxy/)) 600 | push(config.inbounds, { 601 | type: 'tproxy', 602 | tag: 'tproxy-in', 603 | 604 | listen: '::', 605 | listen_port: int(tproxy_port), 606 | network: 'udp', 607 | udp_timeout: udp_timeout ? (udp_timeout + 's') : null, 608 | sniff: true, 609 | sniff_override_destination: (sniff_override === '1') 610 | }); 611 | if (match(proxy_mode, /tun/)) 612 | push(config.inbounds, { 613 | type: 'tun', 614 | tag: 'tun-in', 615 | 616 | interface_name: tun_name, 617 | inet4_address: tun_addr4, 618 | inet6_address: (ipv6_support === '1') ? tun_addr6 : null, 619 | mtu: strToInt(tun_mtu), 620 | gso: (tun_gso === '1'), 621 | auto_route: false, 622 | endpoint_independent_nat: strToBool(endpoint_independent_nat), 623 | udp_timeout: udp_timeout ? (udp_timeout + 's') : null, 624 | stack: tcpip_stack, 625 | sniff: true, 626 | sniff_override_destination: (sniff_override === '1'), 627 | }); 628 | /* Inbound end */ 629 | 630 | /* Outbound start */ 631 | /* Default outbounds */ 632 | config.outbounds = [ 633 | { 634 | type: 'direct', 635 | tag: 'direct-out', 636 | routing_mark: strToInt(self_mark) 637 | }, 638 | { 639 | type: 'block', 640 | tag: 'block-out' 641 | }, 642 | { 643 | type: 'dns', 644 | tag: 'dns-out' 645 | } 646 | ]; 647 | 648 | /* Main outbounds */ 649 | if (!isEmpty(main_node)) { 650 | const main_node_cfg = uci.get_all(uciconfig, main_node) || {}; 651 | push(config.outbounds, generate_outbound(main_node_cfg)); 652 | config.outbounds[length(config.outbounds)-1].tag = 'main-out'; 653 | 654 | if (dedicated_udp_node) { 655 | const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {}; 656 | push(config.outbounds, generate_outbound(main_udp_node_cfg)); 657 | config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out'; 658 | } 659 | } else if (!isEmpty(default_outbound)) 660 | uci.foreach(uciconfig, uciroutingnode, (cfg) => { 661 | if (cfg.enabled !== '1') 662 | return; 663 | 664 | const outbound = uci.get_all(uciconfig, cfg.node) || {}; 665 | push(config.outbounds, generate_outbound(outbound)); 666 | const type = config.outbounds[length(config.outbounds)-1].type; 667 | if (!(type in ['selector', 'urltest'])) { 668 | config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy; 669 | config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface; 670 | config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound); 671 | } 672 | }); 673 | /* Second level outbounds */ 674 | while (length(nodes_tobe_checkedout) > 0) { 675 | const oldarr = uniq(nodes_tobe_checkedout); 676 | 677 | nodes_tobe_checkedout = []; 678 | map(oldarr, (k) => { 679 | if (!(k in checkedout_nodes)) { 680 | const outbound = uci.get_all(uciconfig, k) || {}; 681 | push(config.outbounds, generate_outbound(outbound)); 682 | push(checkedout_nodes, k); 683 | } 684 | }); 685 | } 686 | while (length(groups_tobe_checkedout) > 0) { 687 | const oldarr = uniq(groups_tobe_checkedout); 688 | let newarr = []; 689 | 690 | groups_tobe_checkedout = []; 691 | map(oldarr, (k) => { 692 | if (!(k in checkedout_groups)) { 693 | push(newarr, k); 694 | push(checkedout_groups, k); 695 | } 696 | }); 697 | const hashexp = regexp('^' + replace(replace(replace(sprintf("%J", newarr), /^\[(.*)\]$/g, "($1)"), /[" ]/g, ''), ',', '|') + '$', 'is'); 698 | uci.foreach(uciconfig, ucinode, (cfg) => { 699 | if (!(cfg['.name'] in checkedout_nodes) && match(cfg?.grouphash, hashexp)) { 700 | push(config.outbounds, generate_outbound(cfg)); 701 | push(checkedout_nodes, cfg['.name']); 702 | } 703 | }); 704 | } 705 | /* Outbound end */ 706 | 707 | /* Routing rules start */ 708 | /* Default settings */ 709 | config.route = { 710 | rules: [ 711 | { 712 | inbound: 'dns-in', 713 | outbound: 'dns-out' 714 | }, 715 | { 716 | protocol: 'dns', 717 | outbound: 'dns-out' 718 | } 719 | ], 720 | rule_set: [], 721 | auto_detect_interface: isEmpty(default_interface) ? true : null, 722 | default_interface: default_interface 723 | }; 724 | 725 | /* Routing rules */ 726 | if (!isEmpty(main_node)) { 727 | /* Direct list */ 728 | if (length(direct_domain_list)) 729 | push(config.route.rules, { 730 | domain_keyword: direct_domain_list, 731 | outbound: 'direct-out' 732 | }); 733 | 734 | /* Main UDP out */ 735 | if (dedicated_udp_node) 736 | push(config.route.rules, { 737 | network: 'udp', 738 | outbound: 'main-udp-out' 739 | }); 740 | 741 | config.route.final = 'main-out'; 742 | } else if (!isEmpty(default_outbound)) { 743 | uci.foreach(uciconfig, uciroutingrule, (cfg) => { 744 | if (cfg.enabled !== '1') 745 | return null; 746 | 747 | push(config.route.rules, { 748 | ip_version: strToInt(cfg.ip_version), 749 | protocol: cfg.protocol, 750 | network: cfg.network, 751 | domain: cfg.domain, 752 | domain_suffix: cfg.domain_suffix, 753 | domain_keyword: cfg.domain_keyword, 754 | domain_regex: cfg.domain_regex, 755 | source_ip_cidr: cfg.source_ip_cidr, 756 | source_ip_is_private: (cfg.source_ip_is_private === '1') || null, 757 | ip_cidr: cfg.ip_cidr, 758 | ip_is_private: (cfg.ip_is_private === '1') || null, 759 | source_port: parse_port(cfg.source_port), 760 | source_port_range: cfg.source_port_range, 761 | port: parse_port(cfg.port), 762 | port_range: cfg.port_range, 763 | process_name: cfg.process_name, 764 | process_path: cfg.process_path, 765 | user: cfg.user, 766 | clash_mode: cfg.clash_mode, 767 | rule_set: get_ruleset(cfg.rule_set), 768 | rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null, 769 | invert: (cfg.invert === '1') || null, 770 | outbound: get_outbound(cfg.outbound) 771 | }); 772 | }); 773 | 774 | config.route.final = get_outbound(default_outbound); 775 | }; 776 | 777 | /* Rule set */ 778 | if (routing_mode === 'custom') { 779 | uci.foreach(uciconfig, uciruleset, (cfg) => { 780 | if (cfg.enabled !== '1') 781 | return null; 782 | 783 | push(config.route.rule_set, { 784 | type: cfg.type, 785 | tag: 'cfg-' + cfg['.name'] + '-rule', 786 | format: cfg.format, 787 | path: cfg.path, 788 | url: cfg.url, 789 | download_detour: get_outbound(cfg.outbound), 790 | update_interval: cfg.update_interval 791 | }); 792 | }); 793 | } 794 | /* Routing rules end */ 795 | 796 | /* Experimental start */ 797 | if (routing_mode === 'custom') { 798 | config.experimental = { 799 | cache_file: { 800 | enabled: true, 801 | path: HP_DIR + '/cache.db', 802 | store_rdrc: (cache_file_store_rdrc === '1') || null, 803 | rdrc_timeout: cache_file_rdrc_timeout 804 | } 805 | }; 806 | /* Clash API */ 807 | if (dashboard_repo) { 808 | system('rm -rf ' + RUN_DIR + '/ui'); 809 | const dashpkg = HP_DIR + '/resources/' + replace(dashboard_repo, '/', '_') + '.zip'; 810 | system('unzip -qo ' + dashpkg + ' -d ' + RUN_DIR + '/'); 811 | system('mv ' + RUN_DIR + '/*-gh-pages/ ' + RUN_DIR + '/ui/'); 812 | } 813 | config.experimental.clash_api = { 814 | external_controller: (clash_api_enabled === '1') ? (nginx_support ? '[::1]:' : '[::]:') + clash_api_port : null, 815 | external_ui: dashboard_repo ? RUN_DIR + '/ui' : null, 816 | secret: clash_api_secret 817 | }; 818 | } 819 | /* Experimental end */ 820 | 821 | system('mkdir -p ' + RUN_DIR); 822 | writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config))); 823 | -------------------------------------------------------------------------------- /root/etc/homeproxy/resources/china_ip6.txt: -------------------------------------------------------------------------------- 1 | 2001:250::/30 2 | 2001:254::/31 3 | 2001:256:100::/48 4 | 2001:67c:ebc::/48 5 | 2001:7fa:5::/48 6 | 2001:c68::/32 7 | 2001:cc0::/32 8 | 2001:da8::/32 9 | 2001:daa:1::/48 10 | 2001:daa:2::/47 11 | 2001:daa:4::/47 12 | 2001:daa:6::/48 13 | 2001:dc7::/32 14 | 2001:dd8:1::/48 15 | 2001:dd8:5::/48 16 | 2001:dd9::/48 17 | 2001:df0:2e00::/48 18 | 2001:df0:a640::/48 19 | 2001:df0:ac40::/48 20 | 2001:df1:6b80::/48 21 | 2001:df1:bd80::/48 22 | 2001:df3:15c0::/48 23 | 2001:df3:3a80::/48 24 | 2001:df3:8b80::/48 25 | 2001:df3:b380::/48 26 | 2001:df7:1480::/48 27 | 2400:1160::/32 28 | 2400:3200::/32 29 | 2400:5280:f803::/48 30 | 2400:5400:10::/48 31 | 2400:5a60:2::/48 32 | 2400:5a60:100::/48 33 | 2400:5f60::/32 34 | 2400:6000::/32 35 | 2400:6460::/40 36 | 2400:6600::/32 37 | 2400:6e60:1301::/48 38 | 2400:73e0::/32 39 | 2400:75aa::/32 40 | 2400:7bc0:20::/43 41 | 2400:7fc0::/40 42 | 2400:7fc0:220::/44 43 | 2400:7fc0:240::/44 44 | 2400:7fc0:2a0::/44 45 | 2400:7fc0:2c0::/44 46 | 2400:7fc0:4000::/40 47 | 2400:7fc0:8000::/36 48 | 2400:7fc0:c000::/36 49 | 2400:8200::/32 50 | 2400:87c0::/32 51 | 2400:89c0:1010::/44 52 | 2400:89c0:1020::/43 53 | 2400:89c0:1050::/46 54 | 2400:89c0:1130::/44 55 | 2400:89c0:1150::/48 56 | 2400:89c0:2100::/48 57 | 2400:89c0:2200::/48 58 | 2400:89c0:3010::/44 59 | 2400:89c0:6000::/48 60 | 2400:89c0:6100::/48 61 | 2400:9020:f010::/46 62 | 2400:9340::/32 63 | 2400:9380:8001::/48 64 | 2400:9380:8003::/48 65 | 2400:9380:8021::/48 66 | 2400:9380:8040::/48 67 | 2400:9380:8140::/48 68 | 2400:9380:8201::/48 69 | 2400:9380:8301::/48 70 | 2400:9380:9001::/48 71 | 2400:9380:9002::/48 72 | 2400:9380:9005::/48 73 | 2400:9380:9009::/48 74 | 2400:9380:900a::/48 75 | 2400:9380:9020::/47 76 | 2400:9380:9040::/47 77 | 2400:9380:9050::/47 78 | 2400:9380:9060::/48 79 | 2400:9380:9071::/48 80 | 2400:9380:9080::/47 81 | 2400:9380:90a1::/48 82 | 2400:9380:90b0::/45 83 | 2400:9380:9100::/47 84 | 2400:9380:9121::/48 85 | 2400:9380:9201::/48 86 | 2400:9380:9202::/48 87 | 2400:9380:9220::/47 88 | 2400:9380:9240::/47 89 | 2400:9380:9250::/47 90 | 2400:9380:9252::/48 91 | 2400:9380:9260::/48 92 | 2400:9380:9271::/48 93 | 2400:9380:9272::/48 94 | 2400:9380:9280::/47 95 | 2400:9380:9282::/48 96 | 2400:9380:92a1::/48 97 | 2400:9380:92b0::/45 98 | 2400:95e0::/48 99 | 2400:9600:8800::/48 100 | 2400:9620::/32 101 | 2400:a040::/32 102 | 2400:a980::/29 103 | 2400:ae00:1981::/48 104 | 2400:b200::/32 105 | 2400:b600::/32 106 | 2400:b700::/48 107 | 2400:be00::/48 108 | 2400:cb80:e2e::/47 109 | 2400:cb80:e30::/44 110 | 2400:cb80:e40::/44 111 | 2400:da00::/32 112 | 2400:dd00::/28 113 | 2400:ebc0::/32 114 | 2400:ed60::/48 115 | 2400:ee00::/32 116 | 2400:f6e0::/32 117 | 2400:f720::/32 118 | 2400:f7c0::/32 119 | 2400:fb40::/32 120 | 2400:fe00::/32 121 | 2401:20::/40 122 | 2401:800::/32 123 | 2401:ba0::/32 124 | 2401:e60:10::/44 125 | 2401:1160::/32 126 | 2401:11a0:10::/44 127 | 2401:11a0:150::/44 128 | 2401:11a0:1500::/40 129 | 2401:11a0:d150::/48 130 | 2401:11a0:d152::/48 131 | 2401:11a0:d158::/48 132 | 2401:1200::/48 133 | 2401:1320::/32 134 | 2401:1d40::/32 135 | 2401:2780::/32 136 | 2401:2e00::/32 137 | 2401:33c0::/32 138 | 2401:3480::/36 139 | 2401:3480:2000::/48 140 | 2401:3480:3000::/36 141 | 2401:34a0::/31 142 | 2401:3800::/32 143 | 2401:3880::/32 144 | 2401:3980::/32 145 | 2401:3a80::/32 146 | 2401:3b80::/32 147 | 2401:3c80::/32 148 | 2401:3d80::/32 149 | 2401:3e80::/32 150 | 2401:3f80::/32 151 | 2401:4180::/32 152 | 2401:4280::/32 153 | 2401:4380::/32 154 | 2401:4480::/32 155 | 2401:4580::/32 156 | 2401:4680::/32 157 | 2401:4780::/32 158 | 2401:4880::/32 159 | 2401:4a80::/32 160 | 2401:70e0::/32 161 | 2401:71c0::/48 162 | 2401:7700::/32 163 | 2401:7d40::/32 164 | 2401:7e00::/32 165 | 2401:8be0::/48 166 | 2401:8d00::/46 167 | 2401:8d00:7::/48 168 | 2401:8d00:8::/47 169 | 2401:8d00:b::/48 170 | 2401:8d00:c::/48 171 | 2401:8d00:f::/48 172 | 2401:8d00:10::/48 173 | 2401:8d00:12::/48 174 | 2401:8d00:14::/48 175 | 2401:8da0::/48 176 | 2401:95c0:f001::/48 177 | 2401:a140:1::/48 178 | 2401:b180::/32 179 | 2401:b400::/45 180 | 2401:b400:8::/47 181 | 2401:b400:11::/48 182 | 2401:b400:14::/46 183 | 2401:b400:20::/47 184 | 2401:b680::/32 185 | 2401:be00::/32 186 | 2401:ca00::/32 187 | 2401:cb80::/32 188 | 2401:cc00::/32 189 | 2401:ce00::/32 190 | 2401:d180::/46 191 | 2401:d180:10::/47 192 | 2401:d180:2120::/48 193 | 2401:de00::/32 194 | 2401:ec00::/32 195 | 2401:fa00:40::/43 196 | 2402:840:d000::/46 197 | 2402:840:e000::/46 198 | 2402:840:f000::/38 199 | 2402:1440::/32 200 | 2402:2000::/32 201 | 2402:3180:8000::/33 202 | 2402:33c0::/32 203 | 2402:3c00::/32 204 | 2402:3f80:1400::/40 205 | 2402:4440::/32 206 | 2402:4b80::/32 207 | 2402:4e00::/32 208 | 2402:5e40::/32 209 | 2402:5ec0::/32 210 | 2402:6e80::/32 211 | 2402:6f40::/48 212 | 2402:6f40:2::/47 213 | 2402:6fc0::/48 214 | 2402:7040::/32 215 | 2402:7740:a000::/36 216 | 2402:7d80::/32 217 | 2402:8bc0::/32 218 | 2402:8cc0::/40 219 | 2402:8cc0:200::/40 220 | 2402:92c0::/48 221 | 2402:93c0::/48 222 | 2402:93c0:20::/48 223 | 2402:93c0:100::/48 224 | 2402:9a80::/32 225 | 2402:9b80:10::/48 226 | 2402:a200::/32 227 | 2402:b8c0::/32 228 | 2402:b940::/40 229 | 2402:b940:200::/39 230 | 2402:d340::/32 231 | 2402:db40::/33 232 | 2402:dfc0::/44 233 | 2402:dfc0:50::/44 234 | 2402:e380:100::/40 235 | 2402:e380:302::/48 236 | 2402:e380:306::/48 237 | 2402:e380:308::/48 238 | 2402:e380:30c::/48 239 | 2402:e380:312::/48 240 | 2402:e480::/32 241 | 2402:e740::/32 242 | 2402:e7c0::/32 243 | 2402:ef40::/32 244 | 2402:f000::/32 245 | 2402:f8c0::/32 246 | 2403:600::/32 247 | 2403:c80::/32 248 | 2403:1a40::/32 249 | 2403:1b80::/48 250 | 2403:1ec0:1200::/48 251 | 2403:1ec0:1400::/48 252 | 2403:1ec0:1610::/48 253 | 2403:2040::/48 254 | 2403:2280::/48 255 | 2403:2b40::/32 256 | 2403:3140::/32 257 | 2403:3480::/46 258 | 2403:3480:4::/48 259 | 2403:3480:12::/47 260 | 2403:3480:14::/46 261 | 2403:3480:18::/47 262 | 2403:3480:1b::/48 263 | 2403:3480:1c::/46 264 | 2403:3480:20::/48 265 | 2403:3480:30::/47 266 | 2403:4240::/32 267 | 2403:4280::/47 268 | 2403:4b40::/32 269 | 2403:4c80::/48 270 | 2403:4c80:128::/48 271 | 2403:5c80::/48 272 | 2403:6380:14::/47 273 | 2403:6380:40::/46 274 | 2403:6a00::/32 275 | 2403:7580::/32 276 | 2403:8080:101::/48 277 | 2403:8c00::/32 278 | 2403:9b00::/32 279 | 2403:a100::/48 280 | 2403:a140:10::/48 281 | 2403:a140:100::/40 282 | 2403:a200::/32 283 | 2403:ac00::/32 284 | 2403:ad80:8008::/48 285 | 2403:b400::/32 286 | 2403:c980::/32 287 | 2403:d400::/32 288 | 2403:e7c0:1::/48 289 | 2403:f4c0::/48 290 | 2403:ffc0:1100::/40 291 | 2403:ffc0:1200::/39 292 | 2404:bc0:1::/48 293 | 2404:bc0:4000::/43 294 | 2404:bc0:4100::/43 295 | 2404:bc0:4200::/43 296 | 2404:bc0:4300::/44 297 | 2404:bc0:4400::/43 298 | 2404:bc0:4f00::/43 299 | 2404:1c80::/32 300 | 2404:2280:103::/48 301 | 2404:2280:106::/47 302 | 2404:2280:10b::/48 303 | 2404:2280:10d::/48 304 | 2404:2280:10f::/48 305 | 2404:2280:112::/47 306 | 2404:2280:115::/48 307 | 2404:2280:11a::/47 308 | 2404:2280:11c::/46 309 | 2404:2280:123::/48 310 | 2404:2280:125::/48 311 | 2404:2280:126::/47 312 | 2404:2280:12e::/48 313 | 2404:2280:130::/48 314 | 2404:2280:134::/48 315 | 2404:2280:136::/47 316 | 2404:2280:13b::/48 317 | 2404:2280:13c::/47 318 | 2404:2280:142::/48 319 | 2404:2280:147::/48 320 | 2404:2280:152::/48 321 | 2404:2280:193::/48 322 | 2404:2280:196::/48 323 | 2404:2280:199::/48 324 | 2404:2280:19a::/47 325 | 2404:2280:19c::/46 326 | 2404:2280:1a4::/47 327 | 2404:2280:1a8::/47 328 | 2404:2280:1b0::/48 329 | 2404:2280:1b2::/48 330 | 2404:2280:1b4::/46 331 | 2404:2280:1b8::/47 332 | 2404:2280:1ba::/48 333 | 2404:2280:1bf::/48 334 | 2404:2280:1c1::/48 335 | 2404:2280:1c2::/47 336 | 2404:2280:1c4::/46 337 | 2404:2280:1c8::/48 338 | 2404:2280:1cb::/48 339 | 2404:2280:1cc::/48 340 | 2404:2280:1cf::/48 341 | 2404:2280:1d0::/48 342 | 2404:2280:1d3::/48 343 | 2404:2280:1d6::/48 344 | 2404:2280:1d8::/45 345 | 2404:2280:1e0::/48 346 | 2404:2280:1e2::/47 347 | 2404:2280:1e4::/46 348 | 2404:2280:1e8::/46 349 | 2404:2280:1ec::/47 350 | 2404:2280:1ee::/48 351 | 2404:2280:1f0::/45 352 | 2404:2280:1f8::/46 353 | 2404:4dc0::/32 354 | 2404:6380::/48 355 | 2404:6380:1000::/48 356 | 2404:6380:8001::/48 357 | 2404:6500:dcb3::/48 358 | 2404:7180:a000::/48 359 | 2404:7180:a010::/48 360 | 2404:7180:a021::/48 361 | 2404:7180:aa00::/48 362 | 2404:7180:aa10::/48 363 | 2404:7180:b001::/48 364 | 2404:7180:b002::/48 365 | 2404:7180:b010::/48 366 | 2404:7180:c001::/48 367 | 2404:7180:c002::/48 368 | 2404:7180:c011::/48 369 | 2404:7180:c012::/48 370 | 2404:7180:f000::/48 371 | 2404:7180:f010::/48 372 | 2404:7240::/33 373 | 2404:7600::/32 374 | 2404:7940::/32 375 | 2404:8d02:28c8::/48 376 | 2404:8d02:4881::/48 377 | 2404:c2c0::/40 378 | 2404:c2c0:240::/44 379 | 2404:c2c0:280::/44 380 | 2404:c2c0:2c0::/44 381 | 2404:c2c0:501::/48 382 | 2404:c2c0:4000::/40 383 | 2404:c2c0:8000::/36 384 | 2404:c2c0:c000::/36 385 | 2404:c300::/32 386 | 2404:c940::/48 387 | 2404:d7c0:2::/48 388 | 2404:e280::/47 389 | 2404:e8c0::/32 390 | 2404:f4c0::/32 391 | 2405:6c0:1::/48 392 | 2405:6c0:2::/48 393 | 2405:6c0:4::/48 394 | 2405:1480:1000::/48 395 | 2405:1480:2000::/48 396 | 2405:1480:3000::/47 397 | 2405:1640:6::/48 398 | 2405:3140:11::/48 399 | 2405:3140:31::/48 400 | 2405:3140:3a::/48 401 | 2405:3bc0::/48 402 | 2405:4540:8000::/48 403 | 2405:57c0::/47 404 | 2405:57c0:100::/48 405 | 2405:66c0::/32 406 | 2405:68c0:21::/48 407 | 2405:6940::/48 408 | 2405:6f00:c101::/48 409 | 2405:6f00:c102::/48 410 | 2405:6f00:c170::/47 411 | 2405:6f00:c602::/48 412 | 2405:7040:6000::/47 413 | 2405:78c0:6e00::/43 414 | 2405:8280::/32 415 | 2405:8a40::/32 416 | 2405:a900:ffee::/48 417 | 2405:a900:fffe::/48 418 | 2405:ad00::/32 419 | 2405:b7c0::/32 420 | 2405:be80::/32 421 | 2405:d900::/32 422 | 2405:e000::/32 423 | 2405:f580::/32 424 | 2405:f940::/32 425 | 2406:280::/32 426 | 2406:840:8100::/40 427 | 2406:840:9000::/44 428 | 2406:840:e031::/48 429 | 2406:840:e033::/48 430 | 2406:840:e03f::/48 431 | 2406:840:e080::/44 432 | 2406:840:e0cf::/48 433 | 2406:840:e0e0::/47 434 | 2406:840:e0e2::/48 435 | 2406:840:e0e4::/47 436 | 2406:840:e10f::/48 437 | 2406:840:e14f::/48 438 | 2406:840:e20f::/48 439 | 2406:840:e230::/47 440 | 2406:840:e232::/48 441 | 2406:840:e234::/47 442 | 2406:840:e237::/48 443 | 2406:840:e36f::/48 444 | 2406:840:e500::/47 445 | 2406:840:e666::/47 446 | 2406:840:e720::/48 447 | 2406:840:e770::/48 448 | 2406:840:e777::/48 449 | 2406:840:e80f::/48 450 | 2406:840:e880::/44 451 | 2406:840:eab0::/46 452 | 2406:840:eab4::/48 453 | 2406:840:eab6::/48 454 | 2406:840:eb00::/46 455 | 2406:840:eb04::/47 456 | 2406:840:eb07::/48 457 | 2406:840:eb08::/48 458 | 2406:840:eb0b::/48 459 | 2406:840:eb0f::/48 460 | 2406:840:f200::/47 461 | 2406:840:f203::/48 462 | 2406:840:f380::/44 463 | 2406:840:f440::/47 464 | 2406:840:f44f::/48 465 | 2406:840:f48f::/48 466 | 2406:840:fa01::/48 467 | 2406:840:fa40::/48 468 | 2406:840:fc80::/45 469 | 2406:840:fc88::/46 470 | 2406:840:fc8c::/47 471 | 2406:840:fc8e::/48 472 | 2406:840:fcc0::/44 473 | 2406:840:fcd0::/48 474 | 2406:840:fd00::/47 475 | 2406:840:fd03::/48 476 | 2406:840:fd1f::/48 477 | 2406:840:fd40::/42 478 | 2406:840:fd80::/42 479 | 2406:840:fdc0::/44 480 | 2406:840:fdd1::/48 481 | 2406:840:fe27::/48 482 | 2406:840:fe72::/48 483 | 2406:840:fe90::/46 484 | 2406:840:fe96::/47 485 | 2406:840:fe98::/46 486 | 2406:840:fec0::/47 487 | 2406:840:fec3::/48 488 | 2406:840:fec4::/46 489 | 2406:840:fec8::/46 490 | 2406:840:fecc::/47 491 | 2406:840:fecf::/48 492 | 2406:840:fed1::/48 493 | 2406:840:fed2::/48 494 | 2406:840:fed6::/48 495 | 2406:840:fed8::/48 496 | 2406:840:fedb::/48 497 | 2406:840:fedf::/48 498 | 2406:840:fef0::/47 499 | 2406:840:fef3::/48 500 | 2406:840:fef7::/48 501 | 2406:840:fef8::/47 502 | 2406:840:fefb::/48 503 | 2406:840:fefc::/48 504 | 2406:840:feff::/48 505 | 2406:1080::/48 506 | 2406:1e40:f012::/47 507 | 2406:2700::/32 508 | 2406:3340::/32 509 | 2406:3640:1::/48 510 | 2406:3d80::/32 511 | 2406:4d00::/48 512 | 2406:52c0::/32 513 | 2406:5340:6666::/48 514 | 2406:5340:8888::/48 515 | 2406:5ac0::/32 516 | 2406:8880::/48 517 | 2406:94c0::/48 518 | 2406:b640:100::/48 519 | 2406:b640:4100::/48 520 | 2406:cf00::/48 521 | 2406:cf00:1000::/43 522 | 2406:d440:100::/44 523 | 2406:d440:200::/44 524 | 2406:d440:300::/44 525 | 2406:d440:ff00::/48 526 | 2406:d440:ffff::/48 527 | 2406:e3c0::/32 528 | 2406:e500::/33 529 | 2407:2840::/48 530 | 2407:3740::/48 531 | 2407:37c0::/32 532 | 2407:4f00::/47 533 | 2407:4f00:2::/48 534 | 2407:5380::/32 535 | 2407:6c40:1210::/48 536 | 2407:6c40:1500::/48 537 | 2407:6c40:1600::/40 538 | 2407:6c40:1810::/48 539 | 2407:8f40:2::/48 540 | 2407:9f00::/32 541 | 2407:ad80::/32 542 | 2407:ae80::/32 543 | 2407:b380::/33 544 | 2407:b380:8000::/48 545 | 2407:ba80::/32 546 | 2407:bc00::/32 547 | 2407:c080::/35 548 | 2407:c080:4000::/37 549 | 2407:c080:5000::/37 550 | 2407:c080:6000::/36 551 | 2407:c080:8000::/36 552 | 2407:d9c0::/32 553 | 2408:4000::/22 554 | 2408:8000::/48 555 | 2408:8000:2::/47 556 | 2408:8000:1000::/36 557 | 2408:8000:2000::/35 558 | 2408:8000:4000::/34 559 | 2408:8000:8000::/33 560 | 2408:8001::/32 561 | 2408:8020::/30 562 | 2408:8024::/31 563 | 2408:8026::/32 564 | 2408:802a:8000::/33 565 | 2408:802c::/32 566 | 2408:8034::/32 567 | 2408:803e::/32 568 | 2408:8056::/32 569 | 2408:805c::/30 570 | 2408:8060::/33 571 | 2408:80c2::/33 572 | 2408:80c5::/33 573 | 2408:80ca::/33 574 | 2408:80ca:8000::/34 575 | 2408:80da::/33 576 | 2408:80da:8000::/34 577 | 2408:80e0:4000::/34 578 | 2408:80e0:8000::/33 579 | 2408:80e2::/33 580 | 2408:80e3:8000::/33 581 | 2408:80e9:4000::/34 582 | 2408:80ea:4000::/34 583 | 2408:80ea:8000::/33 584 | 2408:80f0:4000::/34 585 | 2408:80f0:8000::/33 586 | 2408:80f1::/42 587 | 2408:80f1:40::/43 588 | 2408:80f1:70::/44 589 | 2408:80f1:80::/41 590 | 2408:80f1:100::/43 591 | 2408:80f1:120::/44 592 | 2408:80f1:160::/43 593 | 2408:80f1:180::/43 594 | 2408:80f1:1b0::/44 595 | 2408:80f1:1c0::/43 596 | 2408:80f1:1e0::/44 597 | 2408:80f1:200::/40 598 | 2408:80f5:4000::/34 599 | 2408:80f9:4000::/34 600 | 2408:80fa:4000::/34 601 | 2408:80fa:8000::/33 602 | 2408:8120:1::/48 603 | 2408:8120:2::/48 604 | 2408:8120:7000::/36 605 | 2408:8140:2000::/48 606 | 2408:815f:e000::/35 607 | 2408:81a2:2000::/35 608 | 2408:81a2:4000::/35 609 | 2408:81a3:6000::/35 610 | 2408:81a3:c800::/48 611 | 2408:81a3:ca66::/48 612 | 2408:8206::/31 613 | 2408:8208::/29 614 | 2408:8210::/30 615 | 2408:8214::/31 616 | 2408:821a::/31 617 | 2408:8220::/31 618 | 2408:8226::/32 619 | 2408:822a::/31 620 | 2408:822e::/31 621 | 2408:8230::/29 622 | 2408:8238::/31 623 | 2408:823c::/31 624 | 2408:8240::/32 625 | 2408:8244::/30 626 | 2408:8248::/30 627 | 2408:824c::/32 628 | 2408:824e::/31 629 | 2408:8250::/29 630 | 2408:8258::/30 631 | 2408:825c::/31 632 | 2408:8260::/32 633 | 2408:8262::/31 634 | 2408:8264::/31 635 | 2408:8266::/32 636 | 2408:826a::/32 637 | 2408:826c::/30 638 | 2408:8270::/32 639 | 2408:8274::/30 640 | 2408:8278::/31 641 | 2408:827a::/32 642 | 2408:8306::/31 643 | 2408:8308::/30 644 | 2408:8310::/30 645 | 2408:832e::/31 646 | 2408:8330::/30 647 | 2408:8338::/32 648 | 2408:8340::/32 649 | 2408:8344::/30 650 | 2408:8348::/30 651 | 2408:834e::/31 652 | 2408:8350::/29 653 | 2408:8358::/30 654 | 2408:8360::/30 655 | 2408:8364::/31 656 | 2408:836c::/30 657 | 2408:8374::/30 658 | 2408:8378::/31 659 | 2408:837a::/32 660 | 2408:8406::/40 661 | 2408:8406:100::/41 662 | 2408:8406:180::/42 663 | 2408:8406:c00::/40 664 | 2408:8406:d00::/41 665 | 2408:8406:d80::/42 666 | 2408:8406:1800::/40 667 | 2408:8406:1900::/41 668 | 2408:8406:1980::/42 669 | 2408:8406:2400::/40 670 | 2408:8406:2500::/41 671 | 2408:8406:2580::/42 672 | 2408:8406:3000::/40 673 | 2408:8406:3100::/41 674 | 2408:8406:3180::/42 675 | 2408:8406:3c00::/40 676 | 2408:8406:3d00::/41 677 | 2408:8406:3d80::/42 678 | 2408:8406:4800::/40 679 | 2408:8406:4900::/41 680 | 2408:8406:4980::/42 681 | 2408:8406:5400::/40 682 | 2408:8406:5500::/41 683 | 2408:8406:5580::/42 684 | 2408:8406:6000::/40 685 | 2408:8406:6100::/41 686 | 2408:8406:6180::/42 687 | 2408:8406:6c00::/40 688 | 2408:8406:6d00::/41 689 | 2408:8406:6d80::/42 690 | 2408:8406:7800::/40 691 | 2408:8406:7900::/41 692 | 2408:8406:7980::/42 693 | 2408:8406:8400::/40 694 | 2408:8406:8500::/41 695 | 2408:8406:8580::/42 696 | 2408:8406:9000::/40 697 | 2408:8406:9100::/41 698 | 2408:8406:9180::/42 699 | 2408:8406:9c00::/40 700 | 2408:8406:9d00::/41 701 | 2408:8406:9d80::/42 702 | 2408:8406:a800::/40 703 | 2408:8406:a900::/41 704 | 2408:8406:a980::/42 705 | 2408:8406:b400::/40 706 | 2408:8406:b500::/41 707 | 2408:8406:b580::/42 708 | 2408:8409::/40 709 | 2408:8409:120::/43 710 | 2408:8409:140::/42 711 | 2408:8409:1a0::/43 712 | 2408:8409:c00::/40 713 | 2408:8409:d00::/41 714 | 2408:8409:1800::/40 715 | 2408:8409:1900::/41 716 | 2408:8409:2400::/40 717 | 2408:8409:2500::/41 718 | 2408:8409:3000::/40 719 | 2408:8409:3100::/41 720 | 2408:8409:3c00::/40 721 | 2408:8409:3d00::/41 722 | 2408:8409:4800::/40 723 | 2408:8409:4900::/41 724 | 2408:8409:5400::/40 725 | 2408:8409:5500::/41 726 | 2408:8409:6000::/40 727 | 2408:8409:6100::/41 728 | 2408:8409:6c00::/40 729 | 2408:8409:6d00::/41 730 | 2408:8409:7800::/40 731 | 2408:8409:7900::/41 732 | 2408:8409:8400::/40 733 | 2408:8409:8500::/41 734 | 2408:8409:9000::/40 735 | 2408:8409:9100::/41 736 | 2408:8409:9c00::/40 737 | 2408:8409:9d00::/41 738 | 2408:8409:a800::/40 739 | 2408:8409:a900::/41 740 | 2408:8409:b400::/40 741 | 2408:8409:b500::/41 742 | 2408:840c::/40 743 | 2408:840c:200::/40 744 | 2408:840c:400::/40 745 | 2408:840c:d00::/40 746 | 2408:840c:f00::/40 747 | 2408:840c:1100::/40 748 | 2408:840c:1a00::/40 749 | 2408:840c:1c00::/40 750 | 2408:840c:1e00::/40 751 | 2408:840c:2700::/40 752 | 2408:840c:2900::/40 753 | 2408:840c:2b00::/40 754 | 2408:840c:3400::/40 755 | 2408:840c:3600::/40 756 | 2408:840c:3800::/40 757 | 2408:840c:4e00::/40 758 | 2408:840c:5000::/40 759 | 2408:840c:5200::/40 760 | 2408:840c:5b00::/40 761 | 2408:840c:5d00::/40 762 | 2408:840c:5f00::/40 763 | 2408:840c:6800::/40 764 | 2408:840c:6a00::/40 765 | 2408:840c:6c00::/40 766 | 2408:840c:7500::/40 767 | 2408:840c:7700::/40 768 | 2408:840c:7900::/40 769 | 2408:840c:8200::/40 770 | 2408:840c:8400::/40 771 | 2408:840c:8600::/40 772 | 2408:840c:8f00::/40 773 | 2408:840c:9100::/40 774 | 2408:840c:9300::/40 775 | 2408:840c:9c00::/40 776 | 2408:840c:9e00::/40 777 | 2408:840c:a000::/40 778 | 2408:840c:a900::/40 779 | 2408:840c:ab00::/40 780 | 2408:840c:ad00::/40 781 | 2408:840c:b600::/40 782 | 2408:840c:b800::/40 783 | 2408:840c:ba00::/40 784 | 2408:840c:c300::/40 785 | 2408:840c:c500::/40 786 | 2408:840c:c700::/40 787 | 2408:840c:d000::/40 788 | 2408:840c:d200::/40 789 | 2408:840c:d400::/40 790 | 2408:840c:dd00::/40 791 | 2408:840c:de00::/39 792 | 2408:840d::/42 793 | 2408:840d:200::/42 794 | 2408:840d:400::/42 795 | 2408:840d:600::/42 796 | 2408:840d:d00::/42 797 | 2408:840d:f00::/42 798 | 2408:840d:1100::/42 799 | 2408:840d:1300::/42 800 | 2408:840d:1a00::/42 801 | 2408:840d:1c00::/42 802 | 2408:840d:1e00::/42 803 | 2408:840d:2000::/42 804 | 2408:840d:2700::/42 805 | 2408:840d:2900::/42 806 | 2408:840d:2b00::/42 807 | 2408:840d:2d00::/42 808 | 2408:840d:3400::/42 809 | 2408:840d:3600::/42 810 | 2408:840d:3800::/42 811 | 2408:840d:3a00::/42 812 | 2408:840d:4e00::/42 813 | 2408:840d:5000::/42 814 | 2408:840d:5200::/42 815 | 2408:840d:5400::/42 816 | 2408:840d:5b00::/42 817 | 2408:840d:5d00::/42 818 | 2408:840d:5f00::/42 819 | 2408:840d:6100::/42 820 | 2408:840d:6800::/42 821 | 2408:840d:6a00::/42 822 | 2408:840d:6c00::/42 823 | 2408:840d:6e00::/42 824 | 2408:840d:7500::/42 825 | 2408:840d:7700::/42 826 | 2408:840d:7900::/42 827 | 2408:840d:7b00::/42 828 | 2408:840d:8200::/42 829 | 2408:840d:8400::/42 830 | 2408:840d:8600::/42 831 | 2408:840d:8800::/42 832 | 2408:840d:8f00::/42 833 | 2408:840d:9100::/42 834 | 2408:840d:9300::/42 835 | 2408:840d:9500::/42 836 | 2408:840d:9c00::/42 837 | 2408:840d:9e00::/42 838 | 2408:840d:a000::/42 839 | 2408:840d:a200::/42 840 | 2408:840d:a900::/42 841 | 2408:840d:ab00::/42 842 | 2408:840d:ad00::/42 843 | 2408:840d:af00::/42 844 | 2408:840d:b600::/42 845 | 2408:840d:b800::/42 846 | 2408:840d:ba00::/42 847 | 2408:840d:bc00::/42 848 | 2408:840d:c300::/42 849 | 2408:840d:c500::/42 850 | 2408:840d:c700::/42 851 | 2408:840d:c900::/42 852 | 2408:840d:d000::/42 853 | 2408:840d:d200::/42 854 | 2408:840d:d400::/42 855 | 2408:840d:d600::/42 856 | 2408:840d:dd00::/42 857 | 2408:840d:de00::/42 858 | 2408:840e:dd00::/40 859 | 2408:840e:de00::/39 860 | 2408:8410::/30 861 | 2408:8414::/31 862 | 2408:8417::/32 863 | 2408:8418::/32 864 | 2408:841a::/31 865 | 2408:841c::/31 866 | 2408:841e::/32 867 | 2408:8420::/31 868 | 2408:8422::/32 869 | 2408:8426::/31 870 | 2408:842a::/31 871 | 2408:842c::/32 872 | 2408:842e::/32 873 | 2408:8431::/32 874 | 2408:8434::/30 875 | 2408:8438::/31 876 | 2408:843c::/30 877 | 2408:8440::/31 878 | 2408:8444::/30 879 | 2408:8448::/32 880 | 2408:844b::/32 881 | 2408:844c::/30 882 | 2408:8452::/31 883 | 2408:8454::/32 884 | 2408:8456::/31 885 | 2408:8458::/30 886 | 2408:845c::/31 887 | 2408:8460::/30 888 | 2408:8464::/31 889 | 2408:8466::/32 890 | 2408:8469::/32 891 | 2408:846a::/31 892 | 2408:846c::/30 893 | 2408:8470::/31 894 | 2408:8474::/30 895 | 2408:8478::/31 896 | 2408:847a::/32 897 | 2408:84e1::/32 898 | 2408:84e2::/31 899 | 2408:84e4::/30 900 | 2408:84e9::/32 901 | 2408:84eb::/32 902 | 2408:84ec::/30 903 | 2408:84f0::/28 904 | 2408:856c::/31 905 | 2408:8606::/31 906 | 2408:8608::/29 907 | 2408:8610::/30 908 | 2408:8614::/31 909 | 2408:861a::/31 910 | 2408:861c::/32 911 | 2408:8620::/31 912 | 2408:8624::/31 913 | 2408:8626::/32 914 | 2408:862a::/31 915 | 2408:862d::/32 916 | 2408:862e::/31 917 | 2408:8630::/29 918 | 2408:8638::/31 919 | 2408:863c::/31 920 | 2408:8640::/32 921 | 2408:8642::/32 922 | 2408:8644::/30 923 | 2408:8648::/31 924 | 2408:864c::/32 925 | 2408:864e::/31 926 | 2408:8650::/30 927 | 2408:8654::/32 928 | 2408:8656::/31 929 | 2408:8658::/30 930 | 2408:865c::/31 931 | 2408:8660::/32 932 | 2408:8662::/31 933 | 2408:8664::/31 934 | 2408:8666::/32 935 | 2408:866a::/31 936 | 2408:866c::/30 937 | 2408:8670::/32 938 | 2408:8674::/30 939 | 2408:8678::/31 940 | 2408:867a::/32 941 | 2408:8706::/31 942 | 2408:8708::/29 943 | 2408:8710::/30 944 | 2408:8719::/32 945 | 2408:871a::/31 946 | 2408:8720::/30 947 | 2408:8726::/32 948 | 2408:872b::/32 949 | 2408:872f::/32 950 | 2408:8730::/30 951 | 2408:8734::/31 952 | 2408:8736::/32 953 | 2408:8738::/32 954 | 2408:873c::/31 955 | 2408:8740::/32 956 | 2408:8742::/32 957 | 2408:8744::/30 958 | 2408:8748::/29 959 | 2408:8752::/32 960 | 2408:8756::/31 961 | 2408:8758::/30 962 | 2408:875c::/32 963 | 2408:8760::/32 964 | 2408:8762::/31 965 | 2408:8764::/31 966 | 2408:8766::/32 967 | 2408:8768::/32 968 | 2408:876a::/32 969 | 2408:876c::/30 970 | 2408:8770::/32 971 | 2408:8772::/31 972 | 2408:8774::/32 973 | 2408:8776::/31 974 | 2408:8778::/31 975 | 2408:877a::/32 976 | 2408:877c::/30 977 | 2408:8806::/42 978 | 2408:8806:40::/43 979 | 2408:880c::/30 980 | 2408:8810::/30 981 | 2408:8814::/31 982 | 2408:8818::/31 983 | 2408:882c::/32 984 | 2408:883a::/32 985 | 2408:8844::/43 986 | 2408:8856::/31 987 | 2408:8858::/30 988 | 2408:8862::/31 989 | 2408:8864::/31 990 | 2408:8866::/32 991 | 2408:886e::/31 992 | 2408:8872::/32 993 | 2408:8878::/31 994 | 2408:887e::/32 995 | 2408:8906:20::/44 996 | 2408:8907:9000::/44 997 | 2408:890c::/31 998 | 2408:8912::/31 999 | 2408:8914::/30 1000 | 2408:891c::/32 1001 | 2408:8920::/32 1002 | 2408:8924::/32 1003 | 2408:892c::/32 1004 | 2408:8936::/32 1005 | 2408:893a::/32 1006 | 2408:8940::/32 1007 | 2408:8948::/32 1008 | 2408:894c::/32 1009 | 2408:894e::/32 1010 | 2408:8956::/31 1011 | 2408:8958::/30 1012 | 2408:8962::/31 1013 | 2408:8964::/31 1014 | 2408:8966::/32 1015 | 2408:896c::/30 1016 | 2408:8978::/30 1017 | 2408:897e::/32 1018 | 2408:8a00:c000::/36 1019 | 2408:8a00:d000::/37 1020 | 2408:8a00:e000::/35 1021 | 2408:8a01::/36 1022 | 2408:8a02:b110::/44 1023 | 2408:8a02:b120::/44 1024 | 2408:8a04:8000::/36 1025 | 2408:8a04:e000::/40 1026 | 2408:8a05:6000::/35 1027 | 2408:8a05:8000::/36 1028 | 2408:8a06::/47 1029 | 2408:8a06:100::/47 1030 | 2408:8a21:4000::/35 1031 | 2408:8a22:9200::/39 1032 | 2408:8a22:9400::/38 1033 | 2408:8a22:9800::/40 1034 | 2408:8a22:9a00::/39 1035 | 2408:8a22:9c00::/38 1036 | 2408:8a22:a000::/37 1037 | 2408:8a23:4000::/34 1038 | 2408:8a24:4000::/34 1039 | 2408:8a26:c000::/34 1040 | 2408:8a27:4000::/35 1041 | 2409:2000::/31 1042 | 2409:27fa::/48 1043 | 2409:27fa:f000::/48 1044 | 2409:27fb::/48 1045 | 2409:27fc::/48 1046 | 2409:27fe::/33 1047 | 2409:6100::/44 1048 | 2409:8000::/20 1049 | 240a:2000::/29 1050 | 240a:4010:8000::/33 1051 | 240a:4020:83a::/48 1052 | 240a:4020:883a::/48 1053 | 240a:4021:83a::/48 1054 | 240a:4021:883a::/48 1055 | 240a:4084:2000::/35 1056 | 240a:4088:a000::/35 1057 | 240a:408c:2000::/35 1058 | 240a:4090:50::/48 1059 | 240a:4090:120::/48 1060 | 240a:4090:250::/48 1061 | 240a:4090:1000::/39 1062 | 240a:4090:1200::/40 1063 | 240a:4090:2010::/48 1064 | 240a:4090:2041::/48 1065 | 240a:4090:2061::/48 1066 | 240a:4090:3000::/39 1067 | 240a:4090:3200::/40 1068 | 240a:4090:5000::/39 1069 | 240a:4090:5200::/40 1070 | 240a:4090:7000::/39 1071 | 240a:4090:7200::/40 1072 | 240a:4090:a000::/35 1073 | 240a:4094:2000::/35 1074 | 240a:409c:2000::/35 1075 | 240a:40a4:2000::/35 1076 | 240a:40ac:2000::/35 1077 | 240a:40b0:83a::/48 1078 | 240a:40b0:283a::/48 1079 | 240a:40b0:483a::/48 1080 | 240a:40b0:683a::/48 1081 | 240a:40c0:8200::/48 1082 | 240a:40c0:8240::/48 1083 | 240a:40c1:a000::/43 1084 | 240a:40c2:a000::/43 1085 | 240a:40c3:2000::/43 1086 | 240a:40c3:c200::/48 1087 | 240a:40c3:c240::/48 1088 | 240a:4172::/31 1089 | 240a:41b0::/34 1090 | 240a:41f2::/31 1091 | 240a:420a::/31 1092 | 240a:4242::/31 1093 | 240a:4280::/26 1094 | 240a:42c0::/27 1095 | 240a:42e0::/28 1096 | 240a:42f0::/29 1097 | 240a:42f8::/30 1098 | 240a:6001::/48 1099 | 240a:a000::/20 1100 | 240a:c000::/20 1101 | 240b:e001::/32 1102 | 240b:e002::/31 1103 | 240b:e004::/30 1104 | 240b:e008::/29 1105 | 240b:e010::/32 1106 | 240c::/28 1107 | 240c:4000::/22 1108 | 240c:c000::/20 1109 | 240d:4000::/21 1110 | 240e::/20 1111 | 2601:1d08:4000::/44 1112 | 2602:2a4:ff::/48 1113 | 2602:2c3:810::/44 1114 | 2602:2e0:ff::/48 1115 | 2602:fa4f:600::/40 1116 | 2602:fab0:11::/48 1117 | 2602:fd92:801::/48 1118 | 2602:fd92:cc0::/44 1119 | 2602:fed2:731d::/48 1120 | 2602:feda:182::/47 1121 | 2602:feda:1bf::/48 1122 | 2602:feda:1d1::/48 1123 | 2602:feda:1d2::/48 1124 | 2602:feda:2d0::/47 1125 | 2602:feda:2f0::/48 1126 | 2602:feda:d80::/48 1127 | 2602:ffe4:c5f::/48 1128 | 2602:ffe4:c60::/47 1129 | 2605:9d80:8001::/48 1130 | 2605:9d80:8011::/48 1131 | 2605:9d80:8021::/48 1132 | 2605:9d80:8031::/48 1133 | 2605:9d80:8041::/48 1134 | 2605:9d80:8081::/48 1135 | 2605:9d80:9003::/48 1136 | 2605:9d80:9013::/48 1137 | 2605:9d80:9023::/48 1138 | 2605:9d80:9033::/48 1139 | 2605:9d80:9042::/48 1140 | 2605:9d80:9052::/48 1141 | 2605:9d80:9071::/48 1142 | 2605:9d80:9092::/48 1143 | 2620:57:4004::/47 1144 | 2804:1e48::/32 1145 | 2a03:5840:11b::/48 1146 | 2a04:3e00:1002::/48 1147 | 2a04:f580:8010::/47 1148 | 2a04:f580:8090::/48 1149 | 2a04:f580:8210::/47 1150 | 2a04:f580:8290::/48 1151 | 2a04:f580:9000::/47 1152 | 2a04:f580:9002::/48 1153 | 2a04:f580:9010::/48 1154 | 2a04:f580:9012::/47 1155 | 2a04:f580:9020::/48 1156 | 2a04:f580:9030::/48 1157 | 2a04:f580:9040::/48 1158 | 2a04:f580:9050::/48 1159 | 2a04:f580:9060::/48 1160 | 2a04:f580:9070::/48 1161 | 2a04:f580:9080::/48 1162 | 2a04:f580:9090::/48 1163 | 2a04:f580:9200::/47 1164 | 2a04:f580:9202::/48 1165 | 2a04:f580:9210::/48 1166 | 2a04:f580:9212::/47 1167 | 2a04:f580:9220::/48 1168 | 2a04:f580:9230::/48 1169 | 2a04:f580:9240::/48 1170 | 2a04:f580:9250::/48 1171 | 2a04:f580:9260::/48 1172 | 2a04:f580:9270::/48 1173 | 2a04:f580:9280::/48 1174 | 2a04:f580:9290::/48 1175 | 2a05:1087::/32 1176 | 2a05:4140:66::/48 1177 | 2a06:1281:8100::/40 1178 | 2a06:1281:8200::/39 1179 | 2a06:1281:8400::/38 1180 | 2a06:1281:8800::/40 1181 | 2a06:1281:8f00::/40 1182 | 2a06:1281:b000::/40 1183 | 2a06:3600::/30 1184 | 2a06:9f43::/32 1185 | 2a06:9f81:4600::/43 1186 | 2a06:9f81:4620::/44 1187 | 2a06:9f81:4640::/43 1188 | 2a06:9f81:4660::/44 1189 | 2a06:9f81:5100::/40 1190 | 2a06:9f81:5400::/40 1191 | 2a06:9f81:6100::/40 1192 | 2a06:a005:260::/43 1193 | 2a06:a005:280::/43 1194 | 2a06:a005:2a0::/44 1195 | 2a06:a005:8d0::/47 1196 | 2a06:a005:8da::/48 1197 | 2a06:a005:9c0::/48 1198 | 2a06:a005:a13::/48 1199 | 2a06:a005:e80::/43 1200 | 2a06:a005:1c40::/44 1201 | 2a07:bb40::/29 1202 | 2a09:b280:ff80::/47 1203 | 2a09:b280:ff83::/48 1204 | 2a09:b280:ff84::/47 1205 | 2a0a:2840:20::/43 1206 | 2a0a:2845:aab8::/46 1207 | 2a0a:2d04:1::/48 1208 | 2a0a:6040:3420::/48 1209 | 2a0a:6040:3430::/48 1210 | 2a0a:6040:34ff::/48 1211 | 2a0a:6040:ec00::/40 1212 | 2a0a:6044:b800::/40 1213 | 2a0b:b87:ffb5::/48 1214 | 2a0b:2542::/48 1215 | 2a0c:9a40:9e00::/43 1216 | 2a0c:b641:571::/48 1217 | 2a0e:15c0:6::/48 1218 | 2a0e:8f02:2182::/47 1219 | 2a0e:8f02:f062::/48 1220 | 2a0e:8f02:f067::/48 1221 | 2a0e:97c0:550::/44 1222 | 2a0e:97c0:5ef::/48 1223 | 2a0e:97c0:83f::/48 1224 | 2a0e:9b00::/29 1225 | 2a0e:aa01:1fff::/48 1226 | 2a0e:aa06::/40 1227 | 2a0e:aa06:440::/48 1228 | 2a0e:aa06:490::/44 1229 | 2a0e:aa06:4e0::/44 1230 | 2a0e:aa06:500::/44 1231 | 2a0e:aa06:520::/48 1232 | 2a0e:aa06:541::/48 1233 | 2a0e:aa07:e01b::/48 1234 | 2a0e:aa07:e024::/47 1235 | 2a0e:aa07:e030::/48 1236 | 2a0e:aa07:e035::/48 1237 | 2a0e:aa07:e039::/48 1238 | 2a0e:aa07:e16a::/48 1239 | 2a0e:aa07:e1a0::/46 1240 | 2a0e:aa07:e1e1::/48 1241 | 2a0e:aa07:e200::/44 1242 | 2a0e:aa07:f000::/48 1243 | 2a0e:aa07:f004::/48 1244 | 2a0e:aa07:f008::/48 1245 | 2a0e:aa07:f0d0::/47 1246 | 2a0e:aa07:f0d2::/48 1247 | 2a0e:aa07:f0d4::/47 1248 | 2a0e:b107:12b::/48 1249 | 2a0e:b107:272::/48 1250 | 2a0e:b107:740::/44 1251 | 2a0e:b107:c10::/48 1252 | 2a0e:b107:da0::/44 1253 | 2a0e:b107:dce::/48 1254 | 2a0e:b107:14a0::/44 1255 | 2a0e:b107:16b0::/44 1256 | 2a0e:b107:16c0::/44 1257 | 2a0e:b107:178c::/48 1258 | 2a0f:5707:ac01::/48 1259 | 2a0f:7803:dd00::/41 1260 | 2a0f:7803:ddc0::/42 1261 | 2a0f:7803:f860::/44 1262 | 2a0f:7803:f8b0::/44 1263 | 2a0f:7803:f970::/44 1264 | 2a0f:7803:fa21::/48 1265 | 2a0f:7803:fa22::/47 1266 | 2a0f:7803:fa24::/46 1267 | 2a0f:7803:fe21::/48 1268 | 2a0f:7803:fe22::/48 1269 | 2a0f:7803:fe24::/48 1270 | 2a0f:7803:fe80::/47 1271 | 2a0f:7803:fe82::/48 1272 | 2a0f:7803:fe84::/48 1273 | 2a0f:85c1:816::/48 1274 | 2a0f:9400:7700::/48 1275 | 2a0f:c400::/29 1276 | 2a0f:dec0::/29 1277 | 2a0f:e540::/29 1278 | 2a10:2f00:15a::/48 1279 | 2a10:3240::/29 1280 | 2a10:3340::/29 1281 | 2a10:3440::/29 1282 | 2a10:cc40:190::/48 1283 | 2a10:cc42:120::/43 1284 | 2a12:d5c0::/29 1285 | 2a12:d6c0::/29 1286 | 2a12:f8c0:1000::/40 1287 | 2a12:f8c3::/36 1288 | 2a13:1800::/48 1289 | 2a13:1800:10::/48 1290 | 2a13:1800:80::/44 1291 | 2a13:1800:300::/44 1292 | 2a13:1801:180::/43 1293 | 2a13:18c4::/32 1294 | 2a13:4900::/29 1295 | 2a13:a5c7:1200::/40 1296 | 2a13:a5c7:1801::/48 1297 | 2a13:a5c7:1802::/47 1298 | 2a13:a5c7:1804::/47 1299 | 2a13:a5c7:1888::/48 1300 | 2a13:a5c7:2102::/47 1301 | 2a13:a5c7:2106::/48 1302 | 2a13:a5c7:2109::/48 1303 | 2a13:a5c7:2200::/40 1304 | 2a13:aac4:f000::/44 1305 | 2a14:7c0:4a01::/48 1306 | 2a14:7c0:4d00::/40 1307 | 2a14:4c41::/32 1308 | 2a14:7581:b20::/48 1309 | 2a14:7581:b40::/48 1310 | 2a14:7581:ffb::/48 1311 | 2a14:7581:ffc::/48 1312 | 2a14:7583::/32 1313 | 2c0f:f7a8:8011::/48 1314 | 2c0f:f7a8:8050::/48 1315 | 2c0f:f7a8:805f::/48 1316 | 2c0f:f7a8:8150::/48 1317 | 2c0f:f7a8:815f::/48 1318 | 2c0f:f7a8:8211::/48 1319 | 2c0f:f7a8:9010::/47 1320 | 2c0f:f7a8:9020::/48 1321 | 2c0f:f7a8:9041::/48 1322 | 2c0f:f7a8:9210::/47 1323 | 2c0f:f7a8:9220::/48 1324 | --------------------------------------------------------------------------------