mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 08:25:13 +08:00
🎁 Sync 2026-07-31 00:21:49
This commit is contained in:
1 parent
f6c3fbbbbe
commit
c71b32c9fb
204 files changed
+187145
-10038
No files matched your search
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
log_max_size="50" #KB
|
||||
main_log_file="/var/run/$NAME/$NAME.log"
|
||||
hiddifyc_log_file="/var/run/$NAME/hiddify-c.log"
|
||||
|
||||
while true; do
|
||||
sleep 180
|
||||
for i in "$main_log_file" "$hiddifyc_log_file"; do
|
||||
[ -s "$i" ] || continue
|
||||
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,600 @@
|
||||
#!/usr/bin/utpl -S
|
||||
|
||||
{%-
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
|
||||
|
||||
const fw4 = require('fw4');
|
||||
|
||||
function array_to_nftarr(array) {
|
||||
if (type(array) !== 'array')
|
||||
return null;
|
||||
|
||||
return `{ ${join(', ', uniq(array))} }`;
|
||||
}
|
||||
|
||||
function resolve_ipv6(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let ipv6 = fw4.parse_subnet(str)?.[0];
|
||||
if (!ipv6 || ipv6.family !== 6)
|
||||
return null;
|
||||
|
||||
if (ipv6.bits > -1)
|
||||
return `${ipv6.addr}/${ipv6.bits}`;
|
||||
else
|
||||
return `& ${ipv6.mask} == ${ipv6.addr}`;
|
||||
}
|
||||
|
||||
function resolve_mark(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let mark = fw4.parse_mark(str);
|
||||
if (isEmpty(mark))
|
||||
return null;
|
||||
|
||||
if (mark.mask === 0xffffffff)
|
||||
return fw4.hex(mark.mark);
|
||||
else if (mark.mark === 0)
|
||||
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`;
|
||||
else if (mark.mark === mark.mask)
|
||||
return `mark or ${fw4.hex(mark.mark)}`;
|
||||
else if (mark.mask === 0)
|
||||
return `mark xor ${fw4.hex(mark.mark)}`;
|
||||
else
|
||||
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`;
|
||||
}
|
||||
|
||||
/* Misc config */
|
||||
const resources_dir = '/etc/homeproxy/resources';
|
||||
|
||||
/* UCI config start */
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'proxy_banned_ru';
|
||||
let outbound_node, outbound_udp_node;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
||||
} else {
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
}
|
||||
|
||||
let routing_port = uci.get(cfgname, 'config', 'routing_port');
|
||||
if (routing_port === 'common')
|
||||
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
|
||||
|
||||
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
|
||||
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
|
||||
|
||||
let self_mark, redirect_port,
|
||||
tproxy_port, tproxy_mark,
|
||||
tun_name, tun_mark;
|
||||
|
||||
/* ByeDPI runs as root (needed for TCP_MD5SIG etc.) but under a dedicated group, so its
|
||||
* egress can be excluded from the redirect by socket group (skgid) to break the proxy
|
||||
* loop — ciadpi has no fwmark option, and root traffic can't be told apart by skuid. */
|
||||
const byedpi_enabled = uci.get(cfgname, 'config', 'byedpi_enabled') || '0';
|
||||
const byedpi_gid = uci.get(cfgname, 'infra', 'byedpi_gid') || '8181';
|
||||
|
||||
/* zapret: sing-box's `zapret-out` direct outbound stamps egress with zapret_mark (a
|
||||
* routing_mark that OVERRIDES default_mark). That mark must (a) escape the redirect loop
|
||||
* like self_mark does, and (b) be steered into NFQUEUE so nfqws can desync the handshake.
|
||||
* nfqws reinjects its fake packets carrying its own desync mark (0x40000000) — those must
|
||||
* also escape the redirect, and must NOT be re-queued. */
|
||||
const zapret_enabled = uci.get(cfgname, 'config', 'zapret_enabled') || '0';
|
||||
const zapret_mark = uci.get(cfgname, 'config', 'zapret_mark') || '110';
|
||||
const zapret_qnum = uci.get(cfgname, 'config', 'zapret_qnum') || '200';
|
||||
const zapret_desync_mark = '0x40000000'; /* nfqws DESYNC_MARK (its reinjected fakes) */
|
||||
|
||||
if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
|
||||
}
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
if (outbound_udp_node !== 'nil' || routing_mode === 'custom' || routing_mode === 'proxy_banned_ru') {
|
||||
tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
|
||||
tproxy_mark = resolve_mark(uci.get(cfgname, 'infra', 'tproxy_mark') || '101');
|
||||
}
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
tun_mark = resolve_mark(uci.get(cfgname, 'infra', 'tun_mark') || '102');
|
||||
}
|
||||
|
||||
const control_options = [
|
||||
"listen_interfaces", "lan_proxy_mode",
|
||||
"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
|
||||
"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
|
||||
"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
|
||||
"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
|
||||
"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips",
|
||||
"wan_direct_ipv4_ips", "wan_direct_ipv6_ips"
|
||||
];
|
||||
const control_info = {};
|
||||
|
||||
for (let i in control_options)
|
||||
control_info[i] = uci.get(cfgname, 'control', i);
|
||||
|
||||
const dns_redirect = uci.get(cfgname, 'infra', 'dns_redirect') || '1';
|
||||
const dns_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0',
|
||||
dns_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53';
|
||||
/* UCI config end */
|
||||
-%}
|
||||
|
||||
{# Reserved addresses -#}
|
||||
set homeproxy_local_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
0.0.0.0/8,
|
||||
10.0.0.0/8,
|
||||
100.64.0.0/10,
|
||||
127.0.0.0/8,
|
||||
169.254.0.0/16,
|
||||
172.16.0.0/12,
|
||||
192.0.0.0/24,
|
||||
192.0.2.0/24,
|
||||
192.31.196.0/24,
|
||||
192.52.193.0/24,
|
||||
192.88.99.0/24,
|
||||
192.168.0.0/16,
|
||||
192.175.48.0/24,
|
||||
198.18.0.0/15,
|
||||
198.51.100.0/24,
|
||||
203.0.113.0/24,
|
||||
224.0.0.0/4,
|
||||
240.0.0.0/4
|
||||
}
|
||||
}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_local_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
::/128,
|
||||
::1/128,
|
||||
::ffff:0:0/96,
|
||||
100::/64,
|
||||
64:ff9b::/96,
|
||||
2001::/32,
|
||||
2001:10::/28,
|
||||
2001:20::/28,
|
||||
2001:db8::/28,
|
||||
2002::/16,
|
||||
fc00::/7,
|
||||
fe80::/10,
|
||||
ff00::/8
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# WAN ACL addresses #}
|
||||
set homeproxy_wan_proxy_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_proxy_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} }
|
||||
{% endif /* wan_proxy_ipv6_ips*/ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
set homeproxy_wan_direct_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_direct_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} }
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_port): %}
|
||||
set homeproxy_routing_port {
|
||||
type inet_service
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = { {{ join(', ', split(routing_port, ',')) }} }
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# DNS hijack & TCP redirect #}
|
||||
chain dstnat {
|
||||
{% if ((dns_redirect === '1') && (dns_hijacked !== '1')): %}
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }}
|
||||
{%- endif /* listen_interfaces */ %}
|
||||
meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dns_port }} comment "!{{ cfgname }}: DNS hijack"
|
||||
{% endif /* dns_hijacked */ %}
|
||||
{% if (match(proxy_mode, /redirect/)): %}
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
|
||||
{% endif /* proxy_mode */ %}
|
||||
}
|
||||
|
||||
{# TCP redirect #}
|
||||
{% if (match(proxy_mode, /redirect/)): %}
|
||||
chain homeproxy_redirect_proxy {
|
||||
meta l4proto tcp counter redirect to :{{ redirect_port }}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_proxy_port {
|
||||
{% if (routing_port): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_redirect_proxy
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_redirect
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect {
|
||||
meta mark {{ self_mark }} counter return
|
||||
{% if (zapret_enabled === '1'): %}
|
||||
meta mark {{ zapret_mark }} counter return
|
||||
meta mark and {{ zapret_desync_mark }} != 0 counter return
|
||||
{% endif /* zapret_enabled */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_redirect_proxy_port
|
||||
}
|
||||
|
||||
chain homeproxy_output_redir {
|
||||
type nat hook output priority filter -105; policy accept
|
||||
{% if (byedpi_enabled === '1'): %}
|
||||
meta skgid {{ byedpi_gid }} counter return
|
||||
{% endif %}
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# NFQUEUE chain: feed ALL Zapret-marked (mark 110) flows to nfqws2 — any port, TCP
|
||||
and UDP — and let the strategy's own --filter-tcp/--filter-udp profiles decide what
|
||||
to desync (the rest passes through via `bypass`). This mirrors how a list→outbound
|
||||
rule routes all matching traffic: once sing-box marks a flow for zapret-out, the
|
||||
queue feeds it, regardless of port (so discord.media on 2053/8443, voice UDP, QUIC,
|
||||
etc. all work with no per-port firewall rules). `ct original packets 1-12` caps it
|
||||
to each flow's handshake; nfqws's reinjected fakes carry DESYNC_MARK (not mark 110)
|
||||
so they are never re-queued. #}
|
||||
{% if (zapret_enabled === '1'): %}
|
||||
chain homeproxy_zapret_queue {
|
||||
type filter hook postrouting priority mangle; policy accept;
|
||||
meta mark {{ zapret_mark }} meta l4proto tcp ct original packets 1-12 counter queue num {{ zapret_qnum }} bypass
|
||||
meta mark {{ zapret_mark }} meta l4proto udp ct original packets 1-12 counter queue num {{ zapret_qnum }} bypass
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# UDP tproxy #}
|
||||
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom' || routing_mode === 'proxy_banned_ru')): %}
|
||||
chain homeproxy_mangle_tproxy {
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
|
||||
{% if (ipv6_support === '1'): %}
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tproxy_port {
|
||||
{% if (routing_port): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_mangle_tproxy
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_mark {
|
||||
{% if (routing_port): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} counter accept
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(uniq([...control_info.listen_interfaces, ...['lo']])) }} counter return
|
||||
{% endif %}
|
||||
meta iifname != lo udp dport 53 counter return
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_prerouting
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_prerouting {
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tproxy_port
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_output {
|
||||
{% if (byedpi_enabled === '1'): %}
|
||||
meta skgid {{ byedpi_gid }} counter return
|
||||
{% endif %}
|
||||
meta mark {{ self_mark }} counter return
|
||||
{% if (zapret_enabled === '1'): %}
|
||||
meta mark {{ zapret_mark }} counter return
|
||||
meta mark and {{ zapret_desync_mark }} != 0 counter return
|
||||
{% endif /* zapret_enabled */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
|
||||
counter goto homeproxy_mangle_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# TUN #}
|
||||
{% if (match(proxy_mode, /tun/)): %}
|
||||
chain homeproxy_mangle_lanac {
|
||||
iifname {{ tun_name }} counter return
|
||||
udp dport 53 counter return
|
||||
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_tun
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun_mark {
|
||||
{% if (routing_port): %}
|
||||
{% if (proxy_mode === 'tun'): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* proxy_mode */ %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* routing_port */ %}
|
||||
|
||||
counter meta mark set {{ tun_mark }}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun {
|
||||
iifname {{ tun_name }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv4_ips */ %}
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter meta mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter meta mark set {{ tun_mark }}
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter meta mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tun_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
|
||||
}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, RUN_DIR } from 'homeproxy';
|
||||
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'proxy_banned_ru',
|
||||
proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
|
||||
|
||||
let outbound_node, tun_name;
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
if (routing_mode === 'custom')
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
else
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
|
||||
if (outbound_node !== 'nil')
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
}
|
||||
|
||||
const server_enabled = uci.get(cfgname, 'server', 'enabled');
|
||||
|
||||
let forward = [],
|
||||
input = [];
|
||||
|
||||
if (tun_name) {
|
||||
push(forward, `oifname ${tun_name} counter accept comment "!${cfgname}: accept tun forward"`);
|
||||
push(input ,`iifname ${tun_name} counter accept comment "!${cfgname}: accept tun input"`);
|
||||
}
|
||||
|
||||
if (server_enabled === '1') {
|
||||
uci.foreach(cfgname, 'server', (s) => {
|
||||
if (s.enabled !== '1' || s.firewall !== '1')
|
||||
return;
|
||||
|
||||
let proto = s.network || '{ tcp, udp }';
|
||||
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEmpty(forward))
|
||||
writefile(RUN_DIR + '/fw4_forward.nft', join('\n', forward) + '\n');
|
||||
|
||||
if (!isEmpty(input))
|
||||
writefile(RUN_DIR + '/fw4_input.nft', join('\n', input) + '\n');
|
||||
+1898
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
import { mkstemp } from 'fs';
|
||||
import { urldecode_params } from 'luci.http';
|
||||
|
||||
/* Global variables start */
|
||||
export const HP_DIR = '/etc/homeproxy';
|
||||
export const RUN_DIR = '/var/run/homeproxy';
|
||||
/* Global variables end */
|
||||
|
||||
/* Utilities start */
|
||||
/* Kanged from luci-app-commands */
|
||||
export function shellQuote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
};
|
||||
|
||||
export function isBinary(str) {
|
||||
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
|
||||
if (byte <= 8 || (byte >= 14 && byte <= 31))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function executeCommand(...args) {
|
||||
let outfd = mkstemp();
|
||||
let errfd = mkstemp();
|
||||
|
||||
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
||||
|
||||
outfd.seek(0);
|
||||
errfd.seek(0);
|
||||
|
||||
const stdout = outfd.read(1024 * 512) ?? '';
|
||||
const stderr = errfd.read(1024 * 512) ?? '';
|
||||
|
||||
outfd.close();
|
||||
errfd.close();
|
||||
|
||||
const binary = isBinary(stdout);
|
||||
|
||||
return {
|
||||
command: join(' ', args),
|
||||
stdout: binary ? null : stdout,
|
||||
stderr,
|
||||
exitcode,
|
||||
binary
|
||||
};
|
||||
};
|
||||
|
||||
export function getTime(epoch) {
|
||||
const local_time = localtime(epoch);
|
||||
return replace(replace(sprintf(
|
||||
'%d-%2d-%2d@%2d:%2d:%2d',
|
||||
local_time.year,
|
||||
local_time.mon,
|
||||
local_time.mday,
|
||||
local_time.hour,
|
||||
local_time.min,
|
||||
local_time.sec
|
||||
), ' ', '0'), '@', ' ');
|
||||
|
||||
};
|
||||
|
||||
export function wGET(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
if (!ua)
|
||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -qO- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
/* String helper start */
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function strToBool(str) {
|
||||
return (str === '1') || null;
|
||||
};
|
||||
|
||||
export function strToInt(str) {
|
||||
return !isEmpty(str) ? (int(str) || null) : null;
|
||||
};
|
||||
|
||||
export function strToTime(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
str = '' + str;
|
||||
/* A subscription may already deliver a unit-bearing duration (e.g. sing-box
|
||||
* "10s", "1m30s"); only append 's' to a bare number so we never emit "10ss". */
|
||||
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
||||
};
|
||||
|
||||
export function removeBlankAttrs(res) {
|
||||
let content;
|
||||
|
||||
if (type(res) === 'object') {
|
||||
content = {};
|
||||
map(keys(res), (k) => {
|
||||
if (type(res[k]) in ['array', 'object'])
|
||||
content[k] = removeBlankAttrs(res[k]);
|
||||
else if (res[k] !== null && res[k] !== '')
|
||||
content[k] = res[k];
|
||||
});
|
||||
} else if (type(res) === 'array') {
|
||||
content = [];
|
||||
map(res, (k, i) => {
|
||||
if (type(k) in ['array', 'object'])
|
||||
push(content, removeBlankAttrs(k));
|
||||
else if (k !== null && k !== '')
|
||||
push(content, k);
|
||||
});
|
||||
} else
|
||||
return res;
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export function validateHostname(hostname) {
|
||||
return (match(hostname, /^[a-zA-Z0-9_]+$/) != null ||
|
||||
(match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) &&
|
||||
match(hostname, /[^0-9.]/)));
|
||||
};
|
||||
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
|
||||
return (ret === 0);
|
||||
};
|
||||
/* String helper end */
|
||||
|
||||
/* String parser start */
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, '_', '/');
|
||||
str = replace(str, '-', '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
str = str + substr('====', padding);
|
||||
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
export function parseURL(url) {
|
||||
if (type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const services = {
|
||||
http: '80',
|
||||
https: '443'
|
||||
};
|
||||
|
||||
const objurl = {};
|
||||
|
||||
objurl.href = url;
|
||||
|
||||
url = replace(url, /#(.+)$/, (_, val) => {
|
||||
objurl.hash = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
|
||||
objurl.protocol = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /\?(.+)/, (_, val) => {
|
||||
objurl.search = val;
|
||||
objurl.searchParams = urldecode_params(val);
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
|
||||
val = replace(val, /^([^@]+)@/, (_, val) => {
|
||||
objurl.userinfo = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
val = replace(val, /:(\d+)$/, (_, val) => {
|
||||
objurl.port = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (validation('ip4addr', val) ||
|
||||
validation('ip6addr', replace(val, /\[|\]/g, '')) ||
|
||||
validation('hostname', val))
|
||||
objurl.hostname = val;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
objurl.pathname = url || '/';
|
||||
|
||||
if (!objurl.protocol || !objurl.hostname)
|
||||
return null;
|
||||
|
||||
if (objurl.userinfo) {
|
||||
objurl.userinfo = replace(objurl.userinfo, /:(.+)$/, (_, val) => {
|
||||
objurl.password = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
|
||||
objurl.username = objurl.userinfo;
|
||||
delete objurl.userinfo;
|
||||
} else {
|
||||
delete objurl.userinfo;
|
||||
delete objurl.password;
|
||||
}
|
||||
};
|
||||
|
||||
if (!objurl.port)
|
||||
objurl.port = services[objurl.protocol];
|
||||
|
||||
objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
|
||||
objurl.origin = `${objurl.protocol}://${objurl.host}`;
|
||||
|
||||
return objurl;
|
||||
};
|
||||
/* String parser end */
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Headless import of proxy share-link(s) (vless://, vmess://, trojan://, ss://,
|
||||
* hysteria://, hysteria2://, tuic://, …) into homeproxy `node` sections. This is
|
||||
* the SSH/agent-callable equivalent of the browser-only "Import share links"
|
||||
* button (handleLinkImport / parseShareLink) in the LuCI node view. It reuses the
|
||||
* shared parser in node_parse.uc — the SAME parse_uri the subscription importer
|
||||
* uses — so there is no second link-parsing implementation to drift out of sync.
|
||||
*
|
||||
* Usage: ucode import_link.uc <path-to-file-with-one-link-per-line | single-link> [label]
|
||||
* Output: one JSON line, e.g.
|
||||
* { "result": true, "imported": [ { "section": "…", "label": "…", "type": "vless" } ], "failed": 0 }
|
||||
* or { "error": "…" } with a non-zero exit code.
|
||||
*
|
||||
* Imported nodes carry NO grouphash, so they are treated as user-created and are
|
||||
* never removed by a subsequent subscription update.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { parse_uri } from 'node_parse';
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
|
||||
function emit(o) { printf('%s\n', o); }
|
||||
|
||||
/* Dependency-free stable hash (FNV-1a, 32-bit) — md5/digest is absent on the
|
||||
* OpenWrt 23.05 legacy build, so keep section naming pure-ucode like import_conf.uc. */
|
||||
function strhash(s) {
|
||||
let h = 2166136261;
|
||||
const n = length(s);
|
||||
for (let i = 0; i < n; i++) {
|
||||
h = (h ^ ord(s, i)) & 0xFFFFFFFF;
|
||||
h = (h * 16777619) & 0xFFFFFFFF;
|
||||
}
|
||||
return sprintf('%08x', h);
|
||||
}
|
||||
|
||||
const arg = ARGV[0];
|
||||
const want_label = ARGV[1];
|
||||
|
||||
if (!arg) {
|
||||
emit({ error: 'usage: import_link.uc <file-with-links|link> [label]' });
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* arg is either a readable file (one link per line) or the link itself. */
|
||||
let text = readfile(arg);
|
||||
if (!text)
|
||||
text = arg;
|
||||
|
||||
const lines = split(trim(text), '\n');
|
||||
const uci = cursor();
|
||||
|
||||
let imported = [];
|
||||
let failed = 0;
|
||||
|
||||
for (let line in lines) {
|
||||
line = trim(line);
|
||||
if (!length(line))
|
||||
continue;
|
||||
|
||||
const config = parse_uri(line);
|
||||
if (type(config) !== 'object' || !config.type) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = (want_label && length(lines) === 1)
|
||||
? want_label
|
||||
: (config.label || (config.type + '-' + (config.address || 'node')));
|
||||
config.label = label;
|
||||
|
||||
const name = strhash(label + '|' + (config.address || '') + ':' + (config.port || ''));
|
||||
uci.set(uciconfig, name, 'node');
|
||||
for (let k in config)
|
||||
if (config[k] !== null && config[k] !== '')
|
||||
uci.set(uciconfig, name, k, config[k]);
|
||||
|
||||
push(imported, { section: name, label: label, type: config.type });
|
||||
}
|
||||
|
||||
if (length(imported))
|
||||
uci.commit(uciconfig);
|
||||
|
||||
emit({ result: length(imported) > 0, imported: imported, failed: failed });
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, parseURL } from 'homeproxy';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimigration = 'migration',
|
||||
ucimain = 'config',
|
||||
ucinode = 'node',
|
||||
ucidns = 'dns',
|
||||
ucidnsserver = 'dns_server',
|
||||
ucidnsrule = 'dns_rule',
|
||||
ucirouting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule',
|
||||
uciserver = 'server';
|
||||
|
||||
/* chinadns-ng has been removed */
|
||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||
|
||||
/* chinadns server now only accepts single server */
|
||||
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||
} else {
|
||||
if (china_dns_server === 'wan_114')
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114');
|
||||
else if (match(china_dns_server, /,/))
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
||||
}
|
||||
|
||||
/* github_token option has been moved to config section */
|
||||
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
|
||||
if (github_token) {
|
||||
uci.set(uciconfig, ucimain, 'github_token', github_token);
|
||||
uci.delete(uciconfig, uciinfra, 'github_token')
|
||||
}
|
||||
|
||||
/* ntp_server was introduced */
|
||||
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
|
||||
|
||||
/* tun_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||
|
||||
/* create migration section */
|
||||
if (!uci.get(uciconfig, ucimigration))
|
||||
uci.set(uciconfig, ucimigration, uciconfig);
|
||||
|
||||
/* delete old crontab command */
|
||||
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
|
||||
if (!migration_crontab) {
|
||||
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"');
|
||||
uci.set(uciconfig, ucimigration, 'crontab', '1');
|
||||
}
|
||||
|
||||
/* log_level was introduced */
|
||||
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||
uci.set(uciconfig, ucimain, 'log_level', 'warn');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||
|
||||
/* empty value defaults to all ports now */
|
||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||
|
||||
/* experimental section was removed */
|
||||
if (uci.get(uciconfig, 'experimental'))
|
||||
uci.delete(uciconfig, 'experimental');
|
||||
|
||||
/* block-dns was removed from built-in dns servers */
|
||||
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
|
||||
if (default_dns_server === 'block-dns') {
|
||||
/* append a rule at last to block all DNS queries */
|
||||
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'mode', 'default');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'action', 'reject');
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
}
|
||||
|
||||
const dns_server_migration = {};
|
||||
/* DNS servers options */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
/* legacy format was deprecated in sb 1.12 */
|
||||
if (cfg.address) {
|
||||
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
|
||||
/* RCode was moved into DNS rules */
|
||||
if (addr.protocol === 'rcode') {
|
||||
dns_server_migration[cfg['.name']] = { action: 'predefined' };
|
||||
switch (addr.hostname) {
|
||||
case 'success':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NOERROR';
|
||||
break;
|
||||
case 'format_error':
|
||||
dns_server_migration[cfg['.name']].rcode = 'FORMERR';
|
||||
break;
|
||||
case 'server_failure':
|
||||
dns_server_migration[cfg['.name']].rcode = 'SERVFAIL';
|
||||
break;
|
||||
case 'name_error':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NXDOMAIN';
|
||||
break;
|
||||
case 'not_implemented':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NOTIMP';
|
||||
break;
|
||||
case 'refused':
|
||||
default:
|
||||
dns_server_migration[cfg['.name']].rcode = 'REFUSED';
|
||||
break;
|
||||
}
|
||||
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
return;
|
||||
}
|
||||
uci.set(uciconfig, cfg['.name'], 'type', addr.protocol);
|
||||
uci.set(uciconfig, cfg['.name'], 'server', addr.hostname);
|
||||
uci.set(uciconfig, cfg['.name'], 'server_port', addr.port);
|
||||
uci.set(uciconfig, cfg['.name'], 'path', (addr.pathname !== '/') ? addr.pathname : null);
|
||||
uci.delete(uciconfig, cfg['.name'], 'address');
|
||||
}
|
||||
|
||||
if (cfg.strategy) {
|
||||
if (cfg['.name'] === default_dns_server)
|
||||
uci.set(uciconfig, ucidns, 'default_strategy', cfg.strategy);
|
||||
dns_server_migration[cfg['.name']] = { strategy: cfg.strategy };
|
||||
uci.delete(uciconfig, cfg['.name'], 'strategy');
|
||||
}
|
||||
|
||||
if (cfg.client_subnet) {
|
||||
if (cfg['.name'] === default_dns_server)
|
||||
uci.set(uciconfig, ucidns, 'client_subnet', cfg.client_subnet);
|
||||
|
||||
if (isEmpty(dns_server_migration[cfg['.name']]))
|
||||
dns_server_migration[cfg['.name']] = {};
|
||||
dns_server_migration[cfg['.name']].client_subnet = cfg.client_subnet;
|
||||
uci.delete(uciconfig, cfg['.name'], 'client_subnet');
|
||||
}
|
||||
});
|
||||
|
||||
/* DNS rules options */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
/* outbound was removed in sb 1.12 */
|
||||
if (cfg.outbound) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
if (!cfg.enabled)
|
||||
return;
|
||||
|
||||
map(cfg.outbound, (outbound) => {
|
||||
switch (outbound) {
|
||||
case 'direct-out':
|
||||
case 'block-out':
|
||||
break;
|
||||
case 'any-out':
|
||||
uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server);
|
||||
break;
|
||||
default:
|
||||
uci.set(uciconfig, cfg.outbound, 'domain_resolver', cfg.server);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-dns was moved into action in sb 1.11 */
|
||||
if (cfg.server === 'block-dns') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
|
||||
/* strategy and client_subnet were moved into dns rules */
|
||||
if (dns_server_migration[cfg.server]) {
|
||||
if (dns_server_migration[cfg.server].strategy)
|
||||
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
|
||||
|
||||
if (dns_server_migration[cfg.server].client_subnet)
|
||||
uci.set(uciconfig, cfg['.name'], 'client_subnet', dns_server_migration[cfg.server].client_subnet);
|
||||
|
||||
if (dns_server_migration[cfg.server].rcode) {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'predefined');
|
||||
uci.set(uciconfig, cfg['.name'], 'rcode', dns_server_migration[cfg.server].rcode);
|
||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* nodes options */
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
|
||||
|
||||
/* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_enable_pqss))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
|
||||
|
||||
/* wireguard_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.wireguard_gso))
|
||||
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
|
||||
});
|
||||
|
||||
/* routing rules options */
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-out was moved into action in sb 1.11 */
|
||||
if (cfg.outbound === 'block-out') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'outbound');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
});
|
||||
|
||||
/* server options */
|
||||
/* auto_firewall was moved into server options */
|
||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||
if (!isEmpty(auto_firewall))
|
||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
/* auto_firewall was moved into server options */
|
||||
if (auto_firewall === '1')
|
||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||
|
||||
/* sniff_override was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.sniff_override))
|
||||
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
|
||||
|
||||
/* domain_strategy is now pointless without sniff override */
|
||||
if (!isEmpty(cfg.domain_strategy))
|
||||
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
||||
});
|
||||
|
||||
if (!isEmpty(uci.changes(uciconfig)))
|
||||
uci.commit(uciconfig);
|
||||
@@ -0,0 +1,680 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Shared share-link parser: turns a single proxy share-link (vless://, vmess://,
|
||||
* trojan://, ss://, hysteria://, hysteria2://, tuic://, …) into a homeproxy node
|
||||
* config object. Extracted verbatim from update_subscriptions.uc so the
|
||||
* subscription importer AND the standalone link importer (import_link.uc) use one
|
||||
* implementation. `log` is an injected callback (no-op by default) so callers can
|
||||
* route diagnostics where they like.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
import { urldecode, urlencode, urldecode_params } from 'luci.http';
|
||||
import { parseURL, decodeBase64Str, validation, isEmpty } from 'homeproxy';
|
||||
import { connect } from 'ubus';
|
||||
const sing_features = connect().call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||
|
||||
export function parse_uri(uri, log) {
|
||||
if (type(log) != 'function') log = function() {};
|
||||
let config, url, params;
|
||||
|
||||
if (type(uri) === 'object') {
|
||||
if (uri.nodetype === 'sip008') {
|
||||
/* https://shadowsocks.org/guide/sip008.html */
|
||||
config = {
|
||||
label: uri.remarks,
|
||||
type: 'shadowsocks',
|
||||
address: uri.server,
|
||||
port: uri.server_port,
|
||||
shadowsocks_encrypt_method: uri.method,
|
||||
password: uri.password,
|
||||
shadowsocks_plugin: uri.plugin,
|
||||
shadowsocks_plugin_opts: uri.plugin_opts
|
||||
};
|
||||
} else {
|
||||
/* Pre-parsed sing-box JSON outbound from parse_singbox_outbound() */
|
||||
config = uri;
|
||||
}
|
||||
} else if (type(uri) === 'string') {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
switch (uri[0]) {
|
||||
case 'anytls':
|
||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'anytls',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: urldecode(url.username),
|
||||
tls: '1',
|
||||
tls_sni: params.sni,
|
||||
tls_insecure: (params.insecure === '1') ? '1' : '0'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'http':
|
||||
case 'https':
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'http',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
tls: (uri[0] === 'https') ? '1' : '0'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'hysteria':
|
||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'hysteria',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
hysteria_protocol: params.protocol || 'udp',
|
||||
hysteria_auth_type: params.auth ? 'string' : null,
|
||||
hysteria_auth_payload: params.auth,
|
||||
hysteria_obfs_password: params.obfsParam,
|
||||
hysteria_down_mbps: params.downmbps,
|
||||
hysteria_up_mbps: params.upmbps,
|
||||
tls: '1',
|
||||
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
|
||||
tls_sni: params.peer,
|
||||
tls_alpn: params.alpn
|
||||
};
|
||||
|
||||
break;
|
||||
case 'hysteria2':
|
||||
case 'hy2':
|
||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
if (!sing_features.with_quic) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'hysteria2',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: url.username ? (
|
||||
urldecode(url.username + (url.password ? (':' + url.password) : ''))
|
||||
) : null,
|
||||
hysteria_obfs_type: params.obfs,
|
||||
hysteria_obfs_password: params['obfs-password'],
|
||||
tls: '1',
|
||||
tls_insecure: (params.insecure === '1') ? '1' : '0',
|
||||
tls_sni: params.sni
|
||||
};
|
||||
|
||||
break;
|
||||
case 'naive':
|
||||
case 'naive+http':
|
||||
case 'naive+https':
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
let naive_extra_headers = null;
|
||||
if (params.header) {
|
||||
const hdr = split(params.header, ':', 2);
|
||||
if (length(hdr) === 2) {
|
||||
let hdrs = {};
|
||||
hdrs[trim(hdr[0])] = trim(hdr[1]);
|
||||
naive_extra_headers = sprintf('%J', hdrs);
|
||||
}
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'naive',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
tls: (uri[0] === 'naive+https' || params.security === 'tls') ? '1' : '0',
|
||||
tls_sni: params.sni || url.hostname,
|
||||
naive_udp_over_tcp: (params.uot === '1') ? '1' : null,
|
||||
naive_quic: (params.quic === '1') ? '1' : null,
|
||||
naive_extra_headers: naive_extra_headers
|
||||
};
|
||||
|
||||
break;
|
||||
case 'socks':
|
||||
case 'socks4':
|
||||
case 'socks4a':
|
||||
case 'socsk5':
|
||||
case 'socks5h':
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'socks',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
socks_version: (match(uri[0], /4/)) ? '4' : '5'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'ss':
|
||||
/* "Lovely" Shadowrocket format */
|
||||
const ss_suri = split(uri[1], '#');
|
||||
let ss_slabel = '';
|
||||
if (length(ss_suri) <= 2) {
|
||||
if (length(ss_suri) === 2)
|
||||
ss_slabel = '#' + urlencode(ss_suri[1]);
|
||||
if (decodeBase64Str(ss_suri[0]))
|
||||
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
|
||||
}
|
||||
|
||||
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
||||
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
||||
|
||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
let ss_userinfo = {};
|
||||
if (url.username && url.password)
|
||||
/* User info encoded with URIComponent */
|
||||
ss_userinfo = [url.username, urldecode(url.password)];
|
||||
else if (url.username)
|
||||
/* User info encoded with base64 */
|
||||
ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':', 2);
|
||||
|
||||
let ss_plugin, ss_plugin_opts;
|
||||
if (url.search && url.searchParams.plugin) {
|
||||
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
|
||||
ss_plugin = ss_plugin_info[0];
|
||||
if (ss_plugin === 'simple-obfs')
|
||||
/* Fix non-standard plugin name */
|
||||
ss_plugin = 'obfs-local';
|
||||
ss_plugin_opts = ss_plugin_info[1];
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'shadowsocks',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
shadowsocks_encrypt_method: ss_userinfo[0],
|
||||
password: ss_userinfo[1],
|
||||
shadowsocks_plugin: ss_plugin,
|
||||
shadowsocks_plugin_opts: ss_plugin_opts
|
||||
};
|
||||
|
||||
break;
|
||||
case 'shadowtls':
|
||||
/* shadowtls://STLS_PASS@HOST:PORT?version=V&sni=SNI&fp=FP&method=SS_METHOD&password=SS_PASS#Label
|
||||
* Represents a Shadowsocks 2022 connection wrapped in ShadowTLS transport. */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'shadowsocks',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
shadowsocks_encrypt_method: params.method || '2022-blake3-aes-256-gcm',
|
||||
password: params.password ? urldecode(params.password) : null,
|
||||
shadowtls_enabled: '1',
|
||||
shadowtls_password: url.username ? urldecode(url.username) : null,
|
||||
shadowtls_version: params.version || '3',
|
||||
tls_sni: params.sni || null,
|
||||
tls_utls: params.fp || null
|
||||
};
|
||||
|
||||
break;
|
||||
case 'ssh':
|
||||
/* Manual parse to avoid parseURL corruption on long base64 query values */
|
||||
let ssh_str = trim(uri[1]);
|
||||
let ssh_label = null;
|
||||
|
||||
const ssh_hash_idx = index(ssh_str, '#');
|
||||
if (ssh_hash_idx >= 0) {
|
||||
ssh_label = urldecode(substr(ssh_str, ssh_hash_idx + 1));
|
||||
ssh_str = substr(ssh_str, 0, ssh_hash_idx);
|
||||
}
|
||||
|
||||
let ssh_params = {};
|
||||
const ssh_q = index(ssh_str, '?');
|
||||
if (ssh_q >= 0) {
|
||||
ssh_params = urldecode_params(substr(ssh_str, ssh_q + 1)) || {};
|
||||
ssh_str = substr(ssh_str, 0, ssh_q);
|
||||
}
|
||||
ssh_str = replace(ssh_str, /\/+$/, '');
|
||||
|
||||
const ssh_at = index(ssh_str, '@');
|
||||
let ssh_user = null, ssh_pass = null, ssh_host = null, ssh_port = null;
|
||||
if (ssh_at >= 0) {
|
||||
const ssh_userinfo = substr(ssh_str, 0, ssh_at);
|
||||
const ssh_hostport = substr(ssh_str, ssh_at + 1);
|
||||
const ssh_colon = index(ssh_userinfo, ':');
|
||||
if (ssh_colon >= 0) {
|
||||
ssh_user = urldecode(substr(ssh_userinfo, 0, ssh_colon));
|
||||
ssh_pass = urldecode(substr(ssh_userinfo, ssh_colon + 1));
|
||||
} else {
|
||||
ssh_user = urldecode(ssh_userinfo);
|
||||
}
|
||||
const ssh_hp = split(ssh_hostport, ':');
|
||||
ssh_port = pop(ssh_hp);
|
||||
ssh_host = join(':', ssh_hp) || null;
|
||||
}
|
||||
|
||||
let ssh_host_key = null;
|
||||
/* Hiddify: hk=key1\n,key2\n,key3\n — urldecode_params already decodes values */
|
||||
const raw_hk = ssh_params.hk || ssh_params.host_key || ssh_params.hostKey;
|
||||
if (raw_hk) {
|
||||
const ssh_hk_lines = filter(split(raw_hk, ','), (l) => length(trim(l)) > 0);
|
||||
ssh_host_key = length(ssh_hk_lines) ? ssh_hk_lines : null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: ssh_label,
|
||||
type: 'ssh',
|
||||
address: ssh_host,
|
||||
port: ssh_port,
|
||||
username: ssh_user,
|
||||
password: length(ssh_pass) ? ssh_pass : null,
|
||||
/* Hiddify uses pk= (short) or private_key= ; urldecode_params already decoded */
|
||||
ssh_priv_key: ssh_params.pk || ssh_params.private_key || ssh_params.privateKey || null,
|
||||
ssh_priv_key_pp: length(ssh_params.passphrase) ? ssh_params.passphrase : null,
|
||||
ssh_host_key: ssh_host_key,
|
||||
/* Hiddify always enables udp_over_tcp for SSH; only disable if explicitly set to 0 */
|
||||
ssh_udp_over_tcp: (ssh_params.uot === '0' || ssh_params.udp_over_tcp in ['0', 'false']) ? null : '1'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'trojan':
|
||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'trojan',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: urldecode(url.username),
|
||||
transport: (params.type && params.type !== 'tcp') ? params.type : null,
|
||||
tls: '1',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
tls_reality: (params.security === 'reality') ? '1' : '0',
|
||||
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
||||
tls_reality_short_id: params.sid,
|
||||
tls_utls: sing_features.with_utls ? params.fp : null
|
||||
};
|
||||
switch(params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = params.serviceName;
|
||||
break;
|
||||
case 'http':
|
||||
case 'tcp':
|
||||
if (params.type === 'http' || params.headerType === 'http') {
|
||||
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
}
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = params.host ? urldecode(params.host) : null;
|
||||
config.ws_path = params.path ? urldecode(params.path) : null;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
case 'xhttp':
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
config.http_host = params.host ? urldecode(params.host) : null;
|
||||
config.xhttp_mode = params.mode || null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (params.hiddify === '1') {
|
||||
if (params.fragment) {
|
||||
const fparts = split(urldecode(params.fragment), ',');
|
||||
if (length(fparts) >= 2) {
|
||||
config.tls_fragment = '1';
|
||||
config.tls_fragment_size = fparts[0];
|
||||
config.tls_fragment_sleep = fparts[1];
|
||||
config.tls_fragment_type = fparts[2] || null;
|
||||
}
|
||||
}
|
||||
if (params.allowInsecure === 'true' || params.insecure === 'true')
|
||||
config.tls_insecure = '1';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'tuic':
|
||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
if (!sing_features.with_quic) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'tuic',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
uuid: url.username,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
tuic_congestion_control: params.congestion_control,
|
||||
tuic_udp_relay_mode: params.udp_relay_mode,
|
||||
tuic_enable_zero_rtt: params.zero_rtt_handshake || null,
|
||||
tuic_heartbeat: params.heartbeat || null,
|
||||
tls: '1',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
};
|
||||
|
||||
break;
|
||||
case 'vless':
|
||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
/* Unsupported protocol */
|
||||
if (params.type === 'kcp') {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
return null;
|
||||
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'vless',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
uuid: url.username,
|
||||
transport: (params.type !== 'tcp') ? params.type : null,
|
||||
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
tls_reality: (params.security === 'reality') ? '1' : '0',
|
||||
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
||||
tls_reality_short_id: params.sid,
|
||||
tls_utls: sing_features.with_utls ? params.fp : null,
|
||||
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
|
||||
};
|
||||
switch(params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = params.serviceName;
|
||||
break;
|
||||
case 'http':
|
||||
case 'tcp':
|
||||
if (params.type === 'http' || params.headerType === 'http') {
|
||||
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
}
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = params.host ? urldecode(params.host) : null;
|
||||
config.ws_path = params.path ? urldecode(params.path) : null;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
case 'xhttp':
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
config.http_host = params.host ? urldecode(params.host) : null;
|
||||
config.xhttp_mode = params.mode || null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (params.hiddify === '1') {
|
||||
if (params.fragment) {
|
||||
const fparts = split(urldecode(params.fragment), ',');
|
||||
if (length(fparts) >= 2) {
|
||||
config.tls_fragment = '1';
|
||||
config.tls_fragment_size = fparts[0];
|
||||
config.tls_fragment_sleep = fparts[1];
|
||||
config.tls_fragment_type = fparts[2] || null;
|
||||
}
|
||||
}
|
||||
if (params.allowInsecure === 'true' || params.insecure === 'true')
|
||||
config.tls_insecure = '1';
|
||||
if (params.extra) {
|
||||
try {
|
||||
const extra = json(urldecode(params.extra));
|
||||
if (extra.headers && length(keys(extra.headers)) > 0)
|
||||
config.xhttp_headers = sprintf('%J', extra.headers);
|
||||
if (extra.downloadSettings) {
|
||||
const dl = extra.downloadSettings;
|
||||
config.xhttp_download_server = dl.address;
|
||||
config.xhttp_download_port = '' + dl.port;
|
||||
if (dl.xhttpSettings) {
|
||||
config.xhttp_download_path = dl.xhttpSettings.path;
|
||||
config.xhttp_download_host = dl.xhttpSettings.host;
|
||||
config.xhttp_download_mode = dl.xhttpSettings.mode;
|
||||
}
|
||||
config.xhttp_download_security = dl.security;
|
||||
if (dl.security === 'reality' && dl.realitySettings) {
|
||||
config.xhttp_download_sni = dl.realitySettings.serverName;
|
||||
config.xhttp_download_fp = dl.realitySettings.fingerprint;
|
||||
config.xhttp_download_pbk = dl.realitySettings.publicKey;
|
||||
config.xhttp_download_sid = dl.realitySettings.shortId;
|
||||
} else if (dl.security === 'tls' && dl.tlsSettings) {
|
||||
config.xhttp_download_sni = dl.tlsSettings.serverName;
|
||||
config.xhttp_download_alpn = dl.tlsSettings.alpn;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'vmess':
|
||||
/* "Lovely" shadowrocket format */
|
||||
if (match(uri, /&/)) {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
||||
try {
|
||||
uri = json(decodeBase64Str(uri[1])) || {};
|
||||
} catch(e) {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uri.v != '2') {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
/* Unsupported protocol */
|
||||
} else if (uri.net === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
|
||||
return null;
|
||||
} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
* 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
|
||||
* else if (uri.aid && int(uri.aid) !== 0) {
|
||||
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
|
||||
* return null;
|
||||
* }
|
||||
*/
|
||||
|
||||
config = {
|
||||
label: uri.ps ? urldecode(uri.ps) : null,
|
||||
type: 'vmess',
|
||||
address: uri.add,
|
||||
port: uri.port,
|
||||
uuid: uri.id,
|
||||
vmess_alterid: uri.aid,
|
||||
vmess_encrypt: uri.scy || 'auto',
|
||||
vmess_global_padding: '1',
|
||||
transport: (uri.net !== 'tcp') ? uri.net : null,
|
||||
tls: (uri.tls === 'tls') ? '1' : '0',
|
||||
tls_sni: uri.sni || uri.host,
|
||||
tls_alpn: uri.alpn ? split(uri.alpn, ',') : null,
|
||||
tls_utls: sing_features.with_utls ? uri.fp : null
|
||||
};
|
||||
switch (uri.net) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = uri.path;
|
||||
break;
|
||||
case 'h2':
|
||||
case 'tcp':
|
||||
if (uri.net === 'h2' || uri.type === 'http') {
|
||||
config.transport = 'http';
|
||||
config.http_host = uri.host ? split(uri.host, ',') : null;
|
||||
config.http_path = uri.path;
|
||||
}
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
config.httpupgrade_host = uri.host;
|
||||
config.http_path = uri.path;
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = uri.host;
|
||||
config.ws_path = uri.path;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'mieru':
|
||||
/* https://github.com/enfein/mieru */
|
||||
url = parseURL('http://' + uri[1]) || {};
|
||||
params = url.searchParams || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'mieru',
|
||||
address: url.hostname,
|
||||
port: '0',
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
mieru_protocol: params.protocol || null,
|
||||
mieru_port_range: params.port || null,
|
||||
mieru_multiplexing: params.multiplexing || null,
|
||||
mieru_handshake_mode: params['handshake-mode'] || null
|
||||
};
|
||||
|
||||
break;
|
||||
case 'wg':
|
||||
case 'wireguard':
|
||||
/* Manual parse: WireGuard private key may contain URL-encoded chars
|
||||
* incompatible with parseURL's userinfo regex */
|
||||
let wg_str = trim(uri[1]);
|
||||
let wg_label = null;
|
||||
|
||||
const wg_hash = index(wg_str, '#');
|
||||
if (wg_hash >= 0) {
|
||||
wg_label = urldecode(substr(wg_str, wg_hash + 1));
|
||||
wg_str = substr(wg_str, 0, wg_hash);
|
||||
}
|
||||
|
||||
let wg_params = {};
|
||||
const wg_q = index(wg_str, '?');
|
||||
if (wg_q >= 0) {
|
||||
wg_params = urldecode_params(substr(wg_str, wg_q + 1)) || {};
|
||||
wg_str = substr(wg_str, 0, wg_q);
|
||||
}
|
||||
|
||||
/* Strip trailing slash left by "KEY@HOST:PORT/?params" format */
|
||||
wg_str = replace(wg_str, /\/+$/, '');
|
||||
|
||||
const wg_at = index(wg_str, '@');
|
||||
let wg_priv_key = null, wg_host = null, wg_port = null;
|
||||
if (wg_at >= 0) {
|
||||
wg_priv_key = urldecode(substr(wg_str, 0, wg_at));
|
||||
const wg_hp_parts = split(substr(wg_str, wg_at + 1), ':');
|
||||
wg_port = pop(wg_hp_parts);
|
||||
wg_host = join(':', wg_hp_parts) || null;
|
||||
} else {
|
||||
/* wg://HOST:PORT?privateKey=...&publicKey=... format (no userinfo) */
|
||||
const wg_hp_parts = split(wg_str, ':');
|
||||
wg_port = pop(wg_hp_parts);
|
||||
wg_host = join(':', wg_hp_parts) || null;
|
||||
wg_priv_key = wg_params.privateKey || wg_params.privatekey || null;
|
||||
}
|
||||
|
||||
const wg_local_addr = wg_params.address || wg_params.ip || null;
|
||||
config = {
|
||||
label: wg_label,
|
||||
type: 'wireguard',
|
||||
address: wg_host,
|
||||
port: wg_port,
|
||||
wireguard_private_key: wg_priv_key,
|
||||
wireguard_peer_public_key: wg_params.publicKey || wg_params.publickey || null,
|
||||
wireguard_pre_shared_key: wg_params.presharedKey || wg_params.presharedkey || null,
|
||||
wireguard_local_address: wg_local_addr ? split(wg_local_addr, ',') : null,
|
||||
wireguard_mtu: wg_params.mtu || null,
|
||||
wireguard_reserved: wg_params.reserved ? split(wg_params.reserved, ',') : null
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(config)) {
|
||||
if (config.address)
|
||||
config.address = replace(config.address, /\[|\]/g, '');
|
||||
|
||||
if (!validation('host', config.address) || (config.type !== 'mieru' && !validation('port', config.port))) {
|
||||
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
|
||||
return null;
|
||||
} else if (!config.label)
|
||||
config.label = (validation('ip6addr', config.address) ?
|
||||
`[${config.address}]` : config.address) + ':' + config.port;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||
|
||||
# Region rule-sets (geosite/geoip .srs) are remote sing-box rule_sets — the core downloads
|
||||
# and refreshes them itself (update_interval). No firewall list files to cron-update anymore.
|
||||
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# Resolve the Zapret tester's fixed test hosts to one IPv4 each — ONCE, up front,
|
||||
# so the full-test sweep doesn't re-hit DNS for every one of its candidates.
|
||||
#
|
||||
# Resolution uses the router's normal resolver: the SAME secure-DNS / direct-egress
|
||||
# path production uses for these blocked domains (generate_client.uc), so the IP is
|
||||
# representative for the TLS-handshake probe — a foreign resolver could hand back a
|
||||
# different CDN edge and make the result meaningless.
|
||||
#
|
||||
# Each lookup is time-bounded (busybox has no `timeout`: bg + watchdog kill) so a
|
||||
# wedged resolver can't hang the rpcd call (which otherwise surfaces in LuCI as
|
||||
# "ubus call error"), and the whole pass is retried once because applying Zapret
|
||||
# restarts homeproxy's DNS resolver and it's briefly unavailable right after.
|
||||
#
|
||||
# Output: one JSON line —
|
||||
# {"ok":1,"ips":{"yt":"1.2.3.4","tg":"…","dc":"…","st":"…"}}
|
||||
# {"ok":0,"error":"…"}
|
||||
|
||||
# tag|host — mirrors zapret_test.sh's HOSTS (far/near spread).
|
||||
HOSTS="yt|redirector.googlevideo.com tg|telegram.org dc|discord.com st|www.speedtest.net"
|
||||
|
||||
TMP=/tmp/zapret_resolve.$$
|
||||
mkdir -p "$TMP" 2>/dev/null
|
||||
trap 'rm -rf "$TMP" 2>/dev/null' EXIT INT TERM
|
||||
|
||||
resolve_one() { # $1=host -> echoes one non-loopback IPv4 (or empty); bounded ~2s
|
||||
nslookup "$1" > "$TMP/o" 2>/dev/null & np=$!
|
||||
( sleep 2; kill "$np" 2>/dev/null ) >/dev/null 2>&1 & sw=$!
|
||||
wait "$np" 2>/dev/null
|
||||
kill "$sw" 2>/dev/null
|
||||
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' "$TMP/o" 2>/dev/null | grep -vE '^127\.' | tail -1
|
||||
}
|
||||
|
||||
JSON=""; OK=0; n=0
|
||||
while [ "$n" -lt 2 ]; do
|
||||
JSON=""; ANY=""
|
||||
for h in $HOSTS; do
|
||||
tag=${h%%|*}; host=${h##*|}
|
||||
ip=$(resolve_one "$host")
|
||||
[ -n "$ip" ] && ANY=1
|
||||
[ -n "$JSON" ] && JSON="$JSON,"
|
||||
JSON="$JSON\"$tag\":\"$ip\""
|
||||
done
|
||||
[ -n "$ANY" ] && { OK=1; break; }
|
||||
n=$((n+1)); sleep 1
|
||||
done
|
||||
|
||||
if [ "$OK" = 1 ]; then
|
||||
echo "{\"ok\":1,\"ips\":{$JSON}}"
|
||||
else
|
||||
echo '{"ok":0,"error":"could not resolve any test host (DNS not ready? wait a few seconds after Apply, then retry)"}'
|
||||
fi
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/sh
|
||||
# Isolated nfqws2 strategy tester.
|
||||
#
|
||||
# nfqws2 is a packet mangler (NFQUEUE), not a proxy, so it can't be probed
|
||||
# "through" like ByeDPI's SOCKS port. Instead we queue ONLY the resolved
|
||||
# test-site IPs to a TEMP NFQUEUE running the candidate strategy, probe the TLS
|
||||
# handshake with curl, and tear everything down via a trap — without disturbing
|
||||
# the live homeproxy queue (qnum 200) or other traffic.
|
||||
#
|
||||
# Usage: zapret_test.sh "<strategy opts>"
|
||||
# Output: a single JSON line: {"ok":N,"total":M,"results":[{tag,label,host,ip,tls,ok,reason},...]}
|
||||
#
|
||||
# Pass signal = curl time_appconnect > 0 (the TLS handshake completed = the desync
|
||||
# got the ClientHello past the DPI). HTTP status is irrelevant (a 404 is a pass).
|
||||
|
||||
STRAT="$1"
|
||||
IPS_ARG="$2" # optional pre-resolved "tag=ip tag=ip …" (full-test
|
||||
# passes these so 36 candidates don't each re-hit DNS)
|
||||
QNUM=209 # temp queue, distinct from the live 200
|
||||
DMARK=0x40000000 # nfqws2 DESYNC_MARK (its reinjected fakes)
|
||||
NFQWS=/opt/zapret2/nfq2/nfqws2
|
||||
LUA="--lua-init=@/opt/zapret2/lua/zapret-lib.lua --lua-init=@/opt/zapret2/lua/zapret-antidpi.lua --lua-init=@/opt/zapret2/lua/zapret-auto.lua"
|
||||
TABLE=zapret_test
|
||||
CMT=zapret_test # comment tag on our output_redir bypass rules
|
||||
TMP=/tmp/zapret_test.$$
|
||||
|
||||
# tag|label|host — far/near spread (mirrors the ByeDPI tester)
|
||||
HOSTS="yt|YouTube|redirector.googlevideo.com tg|Telegram|telegram.org dc|Discord|discord.com st|Speedtest|www.speedtest.net"
|
||||
|
||||
NFQ_PID=""
|
||||
WATCH_PID=""
|
||||
cleanup() {
|
||||
# nfqws2 daemonizes (so $! is stale) and busybox has no pkill. Match nfqws2 by
|
||||
# process NAME (never the shell/grep itself), then kill only the one on our temp
|
||||
# qnum (sparing the live qnum 200 instance).
|
||||
for p in $(pgrep nfqws2 2>/dev/null); do
|
||||
tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null | grep -q "qnum=$QNUM" && kill "$p" 2>/dev/null
|
||||
done
|
||||
[ -n "$NFQ_PID" ] && kill "$NFQ_PID" 2>/dev/null
|
||||
nft delete table inet "$TABLE" 2>/dev/null
|
||||
# remove our surgical bypass rules from the LIVE output_redir chain (by comment)
|
||||
for hnd in $(nft -a list chain inet fw4 homeproxy_output_redir 2>/dev/null | grep "$CMT" | grep -oE 'handle [0-9]+' | grep -oE '[0-9]+'); do
|
||||
nft delete rule inet fw4 homeproxy_output_redir handle "$hnd" 2>/dev/null
|
||||
done
|
||||
[ -n "$WATCH_PID" ] && kill "$WATCH_PID" 2>/dev/null
|
||||
rm -rf "$TMP" 2>/dev/null
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
fail() { echo "{\"ok\":0,\"total\":0,\"error\":\"$1\"}"; exit 0; }
|
||||
|
||||
[ -x "$NFQWS" ] || fail "zapret2 not installed"
|
||||
command -v curl >/dev/null 2>&1 || fail "curl not installed"
|
||||
mkdir -p "$TMP" 2>/dev/null
|
||||
|
||||
# Watchdog: guarantee teardown within 30s even if curl/nslookup wedges, so the
|
||||
# live firewall is never left modified (busybox has no `timeout` applet).
|
||||
# Redirect its fds to /dev/null: otherwise the backgrounded `sleep` keeps the
|
||||
# caller's stdout pipe open and rpcd/popen blocks on read() for the full 30s
|
||||
# (the result is ready in ~2s — the watchdog just held the pipe).
|
||||
( sleep 30; kill -TERM $$ 2>/dev/null ) >/dev/null 2>&1 &
|
||||
WATCH_PID=$!
|
||||
|
||||
# 1. Test-host IPs — BEFORE touching the firewall, so a slow resolver never runs
|
||||
# while the redirect is modified. Prefer IPs passed in via "$2" (the full-test
|
||||
# sweep resolves ONCE up front via zapret_resolve.sh and reuses them for every
|
||||
# candidate); fall back to resolving here for a standalone call.
|
||||
IPSET=""
|
||||
if [ -n "$IPS_ARG" ]; then
|
||||
for kv in $IPS_ARG; do
|
||||
tag=${kv%%=*}; ip=${kv#*=}
|
||||
[ -n "$tag" ] && echo "$ip" > "$TMP/ip_$tag"
|
||||
[ -n "$ip" ] && IPSET="$IPSET$ip,"
|
||||
done
|
||||
else
|
||||
for h in $HOSTS; do
|
||||
tag=${h%%|*}; host=${h##*|}
|
||||
ip=$(nslookup "$host" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -vE '^127\.' | tail -1)
|
||||
echo "$ip" > "$TMP/ip_$tag"
|
||||
[ -n "$ip" ] && IPSET="$IPSET$ip,"
|
||||
done
|
||||
fi
|
||||
IPSET=$(echo "$IPSET" | sed 's/[[:space:]]//g; s/,$//')
|
||||
[ -z "$IPSET" ] && fail "could not resolve any test host"
|
||||
|
||||
# 2. Surgically send ONLY the test IPs' TCP/443 direct (return from output_redir,
|
||||
# so router-origin curl egresses direct where our temp queue can catch it).
|
||||
# Everything else stays redirected. Tagged with a comment for clean teardown.
|
||||
if nft list chain inet fw4 homeproxy_output_redir >/dev/null 2>&1; then
|
||||
nft insert rule inet fw4 homeproxy_output_redir ip daddr "{ $IPSET }" tcp dport 443 counter return comment "\"$CMT\"" 2>/dev/null
|
||||
fi
|
||||
|
||||
# 3. Temp table: queue test-IP TCP/443 handshakes to QNUM (exclude nfqws's own
|
||||
# reinjected DMARK fakes so they aren't re-queued); notrack those fakes.
|
||||
nft add table inet "$TABLE" 2>/dev/null
|
||||
nft add chain inet "$TABLE" pre "{ type filter hook postrouting priority mangle; policy accept; }" 2>/dev/null
|
||||
nft add rule inet "$TABLE" pre "meta mark and $DMARK == 0 ip daddr { $IPSET } tcp dport 443 ct original packets 1-12 counter queue num $QNUM bypass" 2>/dev/null
|
||||
nft add chain inet "$TABLE" out "{ type filter hook output priority -150; policy accept; }" 2>/dev/null
|
||||
nft add rule inet "$TABLE" out "meta mark and $DMARK != 0 notrack" 2>/dev/null
|
||||
|
||||
# 4. Start the candidate nfqws2 on the temp queue.
|
||||
$NFQWS --qnum=$QNUM --user=daemon --fwmark=$DMARK $LUA $STRAT >/dev/null 2>&1 &
|
||||
NFQ_PID=$!
|
||||
sleep 1
|
||||
pgrep -f "qnum=$QNUM" >/dev/null 2>&1 || fail "nfqws2 did not start - check strategy"
|
||||
|
||||
# 5. Probe every host in parallel: curl forced to the resolved IP (keeps SNI),
|
||||
# write "time_appconnect curl_exit". Wait ONLY on the curl jobs — a bare `wait`
|
||||
# would also block on the 30s watchdog child.
|
||||
CPIDS=""
|
||||
for h in $HOSTS; do
|
||||
tag=${h%%|*}; host=${h##*|}
|
||||
ip=$(cat "$TMP/ip_$tag" 2>/dev/null)
|
||||
if [ -z "$ip" ]; then echo "0 6" > "$TMP/r_$tag"; continue; fi
|
||||
( out=$(curl -s --resolve "${host}:443:${ip}" -o /dev/null -w '%{time_appconnect}' --connect-timeout 4 --max-time 10 "https://${host}" 2>/dev/null); echo "$out $?" > "$TMP/r_$tag" ) >/dev/null 2>&1 &
|
||||
CPIDS="$CPIDS $!"
|
||||
done
|
||||
[ -n "$CPIDS" ] && wait $CPIDS
|
||||
|
||||
# 6. Build JSON.
|
||||
JSON=""; OKC=0; TOT=0
|
||||
for h in $HOSTS; do
|
||||
tag=${h%%|*}; rest=${h#*|}; label=${rest%%|*}; host=${rest##*|}
|
||||
ip=$(cat "$TMP/ip_$tag" 2>/dev/null)
|
||||
raw=$(cat "$TMP/r_$tag" 2>/dev/null); tls=${raw%% *}; rc=${raw##* }
|
||||
TOT=$((TOT+1))
|
||||
ok=0; case "$tls" in 0|0.000000|"") ok=0;; *) ok=1;; esac
|
||||
[ "$ok" = 1 ] && OKC=$((OKC+1))
|
||||
reason=""
|
||||
if [ "$ok" = 0 ]; then
|
||||
case "$rc" in 6) reason=dns;; 7) reason=refused;; 28) reason=timeout;; 35|51|53|56|58|59|60) reason=tls;; *) reason=fail;; esac
|
||||
fi
|
||||
[ -n "$JSON" ] && JSON="$JSON,"
|
||||
JSON="$JSON{\"tag\":\"$tag\",\"label\":\"$label\",\"host\":\"$host\",\"ip\":\"$ip\",\"tls\":\"$tls\",\"ok\":$ok,\"reason\":\"$reason\"}"
|
||||
done
|
||||
|
||||
echo "{\"ok\":$OKC,\"total\":$TOT,\"results\":[$JSON]}"
|
||||
Reference in new issue
Block a user