mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 07:35:13 +08:00
🏅 Sync 2026-10-06 17:10:26
This commit is contained in:
1 parent
c3b5349800
commit
4d2ba50e17
35 files changed
+1231
-76
No files matched your search
@@ -41,7 +41,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy-pro
|
||||
PKG_VERSION:=28.10.1.14
|
||||
PKG_RELEASE:=23
|
||||
PKG_RELEASE:=24
|
||||
|
||||
LUCI_BASENAME:=homeproxy-pro
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
*/
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, isValidCIDR } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
|
||||
import { isEmpty, isValidCIDR, udp_transport_verdict } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
|
||||
import {
|
||||
ipv4_to_nftarr,
|
||||
mac_to_nftarr,
|
||||
@@ -288,15 +288,38 @@ if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = mark_value_or(uci.get(cfgname, 'infra', 'self_mark'), resolve_mark('100'));
|
||||
redirect_port = port_or(uci.get(cfgname, 'infra', 'redirect_port'), '5331');
|
||||
}
|
||||
/* Whether UDP can actually be proxied in this configuration: there is a
|
||||
* UDP-capable node, or the user took the decision over with custom rules.
|
||||
* The UDP tproxy chain already uses exactly this condition, and the QUIC reject
|
||||
* has to agree with it - it keyed off `routing_mode !== 'custom'` only, so a
|
||||
/* Whether QUIC has to be rejected, and why.
|
||||
*
|
||||
* This used to ask "is there a UDP node?" and that question has been wrong in
|
||||
* both directions.
|
||||
*
|
||||
* Wrong by too much: it keyed off `routing_mode !== 'custom'` alone, so a
|
||||
* router with `main_udp_node` set still rejected QUIC on UDP 443 (measured on
|
||||
* the device: 1722 QUIC packets rejected in one sample) and browsers that had
|
||||
* learned HTTP/3 fell back to TCP needlessly. Now the reject is emitted only
|
||||
* when UDP really is not proxyable. */
|
||||
const udp_proxyable = (outbound_udp_node !== 'nil' || routing_mode === 'custom');
|
||||
* already learned HTTP/3 fell back to TCP needlessly.
|
||||
*
|
||||
* Wrong by too little, which is what the current form is: "a UDP node exists"
|
||||
* says nothing about what that node's transport is. `main_udp_node = same`
|
||||
* with a TCP-transport main node is the shape that slips through - the UI
|
||||
* label reads "Same as main node" and nothing tells the user the consequence.
|
||||
* The UDP is proxied, yes, but it is tunnelled over TCP, and QUIC over a TCP
|
||||
* tunnel stalls: measured with both nodes on the SAME server and only the
|
||||
* transport differing (anytls TCP:443 vs hysteria2 QUIC/UDP:50466), UDP came
|
||||
* back at 174 ms +/- 1 ms through hysteria2 and produced a 568 ms spike with
|
||||
* 37 ms jitter and outright loss through anytls.
|
||||
*
|
||||
* So the gate is now the transport, resolved by udp_transport_verdict() in
|
||||
* homeproxy-pro.uc - one definition, shared with the diagnostic report so the log
|
||||
* and the rule cannot disagree.
|
||||
*
|
||||
* NOTE the deliberate asymmetry with the tproxy chain below and in the UDP
|
||||
* tproxy block: those still key off the plain "is there a UDP node" test,
|
||||
* because they decide whether UDP is PROXIED at all. This gate decides
|
||||
* whether QUIC is WORTH proxying. A TCP-transport UDP node still proxies
|
||||
* games, voice and everything else; only QUIC is turned away, so the browser
|
||||
* falls straight back to a TCP path that the same tunnel carries far better. */
|
||||
const udp_transport = udp_transport_verdict(cfgname, uci);
|
||||
const udp_quic_rejected = !udp_transport.native;
|
||||
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
|
||||
@@ -1022,17 +1045,17 @@ chain homeproxy_mangle_tun {
|
||||
{% if (v6_handled): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* v6_handled */ %}
|
||||
{% if (!udp_proxyable): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% endif /* udp_proxyable */ %}
|
||||
{% if (udp_quic_rejected): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (UDP path is not UDP-native)"
|
||||
{% endif /* udp_quic_rejected */ %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (v6_handled): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* v6_handled */ %}
|
||||
{% if (!udp_proxyable): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% endif /* udp_proxyable */ %}
|
||||
{% if (udp_quic_rejected): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (UDP path is not UDP-native)"
|
||||
{% endif /* udp_quic_rejected */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (v6_handled): %}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { lstat, mkdtemp, readfile, writefile } from 'fs';
|
||||
import { Loader } from './config/loader.uc';
|
||||
import { generate } from './generator/client.uc';
|
||||
import { rule_set_tags } from './generator/common.uc';
|
||||
import { removeBlankAttrs, isEmpty, isValidCIDR, probeRuleSetFile, ruleSetFormatFromPath, shellQuote, validateRuleSetPath, validation, HP_DIR, RUN_DIR, UCICONFIG_DIR } from './homeproxy-pro.uc';
|
||||
import { removeBlankAttrs, isEmpty, isValidCIDR, probeRuleSetFile, ruleSetFormatFromPath, shellQuote, udp_transport_verdict, validateRuleSetPath, validation, HP_DIR, RUN_DIR, UCICONFIG_DIR } from './homeproxy-pro.uc';
|
||||
|
||||
/* Resolve the GenerationContext inputs. This is the only impure step on the
|
||||
* client generation path, and it is deliberately here rather than under
|
||||
@@ -129,6 +129,39 @@ function export_node_addresses(config) {
|
||||
dump(RUN_DIR + '/node-addr-domains.txt', domains);
|
||||
}
|
||||
|
||||
/* Export the UDP transport verdict for the shell runtime to read.
|
||||
*
|
||||
* runtime/health.sh wants to say out loud why QUIC was or was not rejected,
|
||||
* and it is a shell script: it cannot call into homeproxy-pro.uc, because that
|
||||
* module imports luci.http and a plain `ucode -e` outside rpcd does not
|
||||
* resolve it. Re-implementing the judgement in shell would duplicate the
|
||||
* UDP_NATIVE_PROTOCOLS set, and a duplicated set is a set that drifts - the
|
||||
* firewall would reject QUIC on one list while the log explained another.
|
||||
*
|
||||
* So the verdict is computed once, here, and written out. The consumer is
|
||||
* a one-line read, and a stale file only costs a stale log line.
|
||||
*
|
||||
* Format is one token per line, deliberately not prose: the shell reads the
|
||||
* flag with `read`, and the reason is a single line safe to print as-is. */
|
||||
function export_udp_transport() {
|
||||
const v = udp_transport_verdict('homeproxy-pro');
|
||||
|
||||
/* join(sep, list), not list.join(sep): ucode will not let an array literal
|
||||
* be the receiver of a method call, and `['a'].join('\n')` fails to parse
|
||||
* as "call join on the literal" - it raises "left-hand side is not a
|
||||
* function" at the closing bracket. The dump() helper above already
|
||||
* spells it the other way for the same reason. */
|
||||
const body = join('\n', [
|
||||
v.native ? '1' : '0',
|
||||
v.configured ? '1' : '0',
|
||||
v.protocol,
|
||||
v.reason
|
||||
]);
|
||||
|
||||
if (writefile(RUN_DIR + '/udp-transport.txt', body + '\n') == null)
|
||||
warn(sprintf('homeproxy-pro: could not write %s\n', RUN_DIR + '/udp-transport.txt'));
|
||||
}
|
||||
|
||||
function resolve_env(dm) {
|
||||
const routing_mode = dm.general.routing_mode || 'bypass_mainland_china';
|
||||
const ubus = connect();
|
||||
@@ -380,6 +413,7 @@ if (system('sing-box check --config ' + shellQuote(tmp)) !== 0) {
|
||||
* dnsmasq snippets read the exported list, and they must never be pointed at
|
||||
* addresses from a configuration that does not start. */
|
||||
export_node_addresses(config);
|
||||
export_udp_transport();
|
||||
|
||||
if (system('mv -f ' + shellQuote(tmp) + ' ' + shellQuote(RUN_DIR) + '/sing-box-c.json') !== 0) {
|
||||
system('rm -rf ' + shellQuote(work_dir));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { access, lstat, mkdtemp, open, rmdir, unlink } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { urldecode_params } from 'luci.http';
|
||||
|
||||
/* Global variables start */
|
||||
@@ -701,6 +702,157 @@ export function strToTime(str) {
|
||||
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
||||
};
|
||||
|
||||
/* Which node transports carry UDP as their own wire format.
|
||||
*
|
||||
* QUIC inside a tunnel whose own transport is TCP is the worst of both
|
||||
* worlds: the QUIC layer retransmits on a schedule that assumes it owns the
|
||||
* path, and the TCP layer underneath retransmits on a schedule that assumes
|
||||
* nothing above it is doing that. Measured on a router where both candidate
|
||||
* nodes were the SAME server and only the transport differed (anytls on
|
||||
* TCP:443 vs hysteria2 on QUIC/UDP:50466), DNS-over-UDP through the tunnel
|
||||
* came back at 174 ms +/- 1 ms through hysteria2 and produced a 568 ms spike
|
||||
* with 37 ms jitter and outright loss through anytls. Same server, same
|
||||
* path - the variable really was the transport, not the node.
|
||||
*
|
||||
* A set rather than a list so membership is an object lookup and an unknown
|
||||
* protocol name is simply absent rather than accidentally matching.
|
||||
*
|
||||
* This is the TRANSPORT, not the sing-box build feature: the `with_quic`
|
||||
* marker in htdocs/.../homeproxy-pro.js records whether the binary was compiled
|
||||
* with QUIC support, which is a different question and the wrong thing to
|
||||
* test here. */
|
||||
export const UDP_NATIVE_PROTOCOLS = {
|
||||
'hysteria': true,
|
||||
'hysteria2': true,
|
||||
'tuic': true,
|
||||
'wireguard': true
|
||||
};
|
||||
|
||||
/* Decide whether the UDP leaving this router has a UDP-native path, and say
|
||||
* why. Used by firewall_post.ut to decide whether QUIC has to be rejected,
|
||||
* and by the diagnostic report to explain it - one definition, two
|
||||
* consumers, because a second copy of this judgement is a copy that drifts.
|
||||
*
|
||||
* Note what this is NOT: a capability check. A TCP-transport node still
|
||||
* proxies UDP perfectly well, sing-box will tunnel it. The question is only
|
||||
* whether putting QUIC on that path is a good idea, so `native: false` means
|
||||
* "QUIC would perform badly", never "UDP cannot be proxied".
|
||||
*
|
||||
* Returns { native, configured, protocol, reason }:
|
||||
* native - true when the effective UDP path is UDP end to end
|
||||
* configured - false when there is no UDP node at all (nothing to judge)
|
||||
* protocol - the resolved node type, for the log
|
||||
* reason - one sentence, safe to print
|
||||
*
|
||||
* WHY THIS READS UCI RATHER THAN TAKING A MODEL, given guard 7 keeps the
|
||||
* semantic layers off the cursor: the three consumers of this judgement -
|
||||
* the fw4 template, the RPC diagnostic report and (through the exported file)
|
||||
* the shell runtime - do not share a data model. firewall_post.ut already
|
||||
* reads main_node/main_udp_node straight from UCI for the tproxy chain, so
|
||||
* this is not a new dependency in that file. The alternative - resolving it
|
||||
* in generator/context.uc from the Loader's model and exporting it - would
|
||||
* make the firewall depend on a value the generator produced, which is the
|
||||
* coupling firewall_post.ut's own header warns against when it reads
|
||||
* node-addr-ips.txt instead of re-deriving node selection. One function,
|
||||
* three callers, no second opinion: that is the property worth protecting
|
||||
* here. The uci module is available everywhere this runs (the CI toolchain
|
||||
* builds ucode with -DUCI_SUPPORT=ON).
|
||||
*/
|
||||
export function udp_transport_verdict(cfgname, cursor_arg) {
|
||||
/* The cursor is a parameter so a caller that already has one - the fw4
|
||||
* template and the RPC report both do - reuses it instead of opening a
|
||||
* second view on the same config, and so the unit test can point this at
|
||||
* a sandbox directory. Callers on the client path pass nothing and get
|
||||
* the default confdir. */
|
||||
const uci = cursor_arg ?? cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
/* Custom routing mode is the user driving the rule graph by hand. They
|
||||
* have already said what carries what and this function cannot read it,
|
||||
* so it must not second-guess them - that is the same reasoning the QUIC
|
||||
* gate used when it keyed on `routing_mode === 'custom'` alone. */
|
||||
if (routing_mode === 'custom')
|
||||
return {
|
||||
native: true,
|
||||
configured: true,
|
||||
protocol: 'custom',
|
||||
reason: 'custom routing mode: the rule graph decides what carries UDP'
|
||||
};
|
||||
|
||||
const udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
||||
const main_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
|
||||
const node_type = (sec) => (isEmpty(sec) || sec === 'nil')
|
||||
? null
|
||||
: (uci.get(cfgname, sec, 'type') || null);
|
||||
|
||||
/* `same` is the case this function was written for. It is also the
|
||||
* silent one: the UI label reads "Same as main node", and nothing
|
||||
* anywhere tells the user that a TCP-transport main node turns every UDP
|
||||
* flow into a TCP tunnel. */
|
||||
if (udp_node === 'same') {
|
||||
const type = node_type(main_node);
|
||||
if (type === null)
|
||||
return {
|
||||
native: false, configured: false, protocol: 'nil',
|
||||
reason: 'UDP follows the main node and no main node is set'
|
||||
};
|
||||
|
||||
return (UDP_NATIVE_PROTOCOLS[type] === true)
|
||||
? { native: true, configured: true, protocol: type,
|
||||
reason: sprintf('UDP follows the main node (%s), which is UDP-native', type) }
|
||||
: { native: false, configured: true, protocol: type,
|
||||
reason: sprintf('UDP follows the main node (%s), which is a TCP transport - QUIC would be tunnelled over TCP', type) };
|
||||
}
|
||||
|
||||
/* The url-test group picks a member per connection, so "all of them" is
|
||||
* the only answer that holds for every packet: a single TCP member is
|
||||
* enough to put QUIC back on the stalling path whenever it happens to be
|
||||
* the selected one. */
|
||||
if (udp_node === 'urltest') {
|
||||
const members = uci.get(cfgname, 'config', 'main_udp_urltest_nodes');
|
||||
if (isEmpty(members))
|
||||
return {
|
||||
native: false, configured: false, protocol: 'urltest',
|
||||
reason: 'the UDP url-test group has no members'
|
||||
};
|
||||
|
||||
let offender = null;
|
||||
for (let sec in members) {
|
||||
const type = node_type(sec);
|
||||
if (UDP_NATIVE_PROTOCOLS[type] !== true && offender === null)
|
||||
offender = type || 'unknown';
|
||||
}
|
||||
|
||||
return (offender === null)
|
||||
? { native: true, configured: true, protocol: 'urltest',
|
||||
reason: sprintf('every member of the UDP url-test group is UDP-native (%d members)', length(members)) }
|
||||
: { native: false, configured: true, protocol: offender,
|
||||
reason: sprintf('the UDP url-test group contains a TCP-transport member (%s) - QUIC would be tunnelled over TCP whenever it is selected', offender) };
|
||||
}
|
||||
|
||||
if (udp_node === 'nil')
|
||||
return {
|
||||
native: false, configured: false, protocol: 'nil',
|
||||
reason: 'UDP is disabled: no UDP node is configured'
|
||||
};
|
||||
|
||||
const type = node_type(udp_node);
|
||||
if (type === null)
|
||||
return {
|
||||
native: false, configured: false, protocol: 'nil',
|
||||
reason: sprintf('the UDP node "%s" does not exist', udp_node)
|
||||
};
|
||||
|
||||
return (UDP_NATIVE_PROTOCOLS[type] === true)
|
||||
? { native: true, configured: true, protocol: type,
|
||||
reason: sprintf('the UDP node is %s, which is UDP-native', type) }
|
||||
: { native: false, configured: true, protocol: type,
|
||||
reason: sprintf('the UDP node is %s, which is a TCP transport - QUIC would be tunnelled over TCP', type) };
|
||||
};
|
||||
|
||||
/* Turn a UCI list of port strings into the int array sing-box wants
|
||||
* (e.g. the WireGuard `reserved` list, a routing rule's `port` /
|
||||
* `source_port`). Returns null for anything that is not a non-empty
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
import { popen } from 'fs';
|
||||
|
||||
import { HP_DIR, RUN_DIR, shellQuote } from '../homeproxy-pro.uc';
|
||||
import { HP_DIR, RUN_DIR, shellQuote, udp_transport_verdict } from '../homeproxy-pro.uc';
|
||||
|
||||
export const DEBUG_REPORT_PATH = RUN_DIR + '/debug.log';
|
||||
|
||||
@@ -274,6 +274,18 @@ function configSummary(uci) {
|
||||
(v == null || v === '') ? '_(unset)_' : (isSecretKey(sw[1]) ? maskValue(v) : v));
|
||||
}
|
||||
|
||||
/* The QUIC verdict, because it is the one conclusion in this report that
|
||||
* is derived rather than reported: the switches table shows
|
||||
* `main_udp_node`, and `main_udp_node` on its own does not say whether
|
||||
* QUIC was rejected by the firewall or why. udp_transport_verdict() is
|
||||
* the same function firewall_post.ut gates the nft rule with, so this
|
||||
* line describes the rule that is actually loaded rather than a second
|
||||
* opinion about it. */
|
||||
const udp = udp_transport_verdict('homeproxy-pro', uci);
|
||||
out += sprintf('\n- UDP path: **%s** - %s\n',
|
||||
udp.native ? 'UDP-native' : (udp.configured ? 'not UDP-native' : 'UDP not configured'),
|
||||
udp.reason);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
|
||||
@@ -339,6 +339,47 @@ hp_probe_tcp() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# hp_report_udp_transport
|
||||
# Say out loud, on every start, whether QUIC is being rejected and why.
|
||||
#
|
||||
# This is a shell script and the judgement lives in ucode (udp_transport_verdict
|
||||
# in homeproxy-pro.uc), which this file cannot call into: homeproxy-pro.uc imports
|
||||
# luci.http, which a plain `ucode -e` outside rpcd does not resolve. So the
|
||||
# generator computes it once and drops it in RUN_DIR, and this reads that file.
|
||||
# Re-implementing the rule here instead would duplicate the protocol set, and
|
||||
# the log would then explain a different decision from the one nft actually has.
|
||||
#
|
||||
# The file is written by generate_client.uc at every generation. A missing
|
||||
# file means generation has not run yet, which is not this function's problem
|
||||
# to report - it stays silent rather than guessing.
|
||||
hp_report_udp_transport() {
|
||||
# Same inline default as runtime/dns.sh uses for its run-dir arguments:
|
||||
# this file is sourced by init.d/homeproxy-pro but is also exercised by
|
||||
# tests/runtime/ without it.
|
||||
local run_dir="${HP_RUN_DIR:-/var/run/homeproxy-pro}"
|
||||
local f="${run_dir}/udp-transport.txt"
|
||||
local native configured proto reason
|
||||
|
||||
[ -r "$f" ] || return 0
|
||||
|
||||
{
|
||||
read -r native
|
||||
read -r configured
|
||||
read -r proto
|
||||
read -r reason
|
||||
} < "$f"
|
||||
|
||||
[ -n "$native" ] || return 0
|
||||
|
||||
if [ "$native" = "1" ]; then
|
||||
log "UDP path is UDP-native (${proto}): QUIC is proxied as-is."
|
||||
elif [ "$configured" = "1" ]; then
|
||||
log "Warning: ${reason}. QUIC on UDP 80/443 is rejected so browsers fall back to TCP; every other UDP flow is still proxied."
|
||||
else
|
||||
log "UDP path: ${reason}. QUIC on UDP 80/443 is rejected."
|
||||
fi
|
||||
}
|
||||
|
||||
# hp_run_probes <label> <dns-port> <mixed-port>
|
||||
# Report the probe results for one side. Returns 1 only when a probe failed
|
||||
# AND HP_PROBE_STRICT=1; a probe that could not be performed never fails it.
|
||||
@@ -368,6 +409,8 @@ hp_run_probes() {
|
||||
log "Warning: cannot probe the ${label} mixed inbound (no nc, or no mixed-in listener in the running configuration)." ;;
|
||||
esac
|
||||
|
||||
hp_report_udp_transport
|
||||
|
||||
if [ "$failed" = "1" ] && [ "${HP_PROBE_STRICT:-0}" = "1" ]; then
|
||||
log "Error: the ${label} functional probes failed and health_probe_strict is on."
|
||||
return 1
|
||||
|
||||
@@ -290,6 +290,12 @@ echo "== subscription updater actually runs =="
|
||||
# URL points at a closed port, so main() fails before its reload call.
|
||||
sh "$ROOT/tests/ucode/test_subscription_updater_runs.sh" "$ROOT" "$WORK/updater_runs" || FAILED=1
|
||||
|
||||
echo "== udp transport verdict =="
|
||||
# Decides whether QUIC is rejected in the fw4 ruleset. Its 'same' +
|
||||
# TCP-transport-main-node case is the regression: both UCI fields read
|
||||
# perfectly reasonable and the QUIC reject stayed off anyway.
|
||||
sh "$ROOT/tests/ucode/test_udp_transport.sh" "$ROOT" "$WORK/udp_transport" || FAILED=1
|
||||
|
||||
echo "== rpcd method behaviour =="
|
||||
# The rpcd module used to be syntax-checked and never executed, which is how
|
||||
# certificate_write('client_ech_conf') stayed broken from the initial commit:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Unit tests for udp_transport_verdict() in homeproxy-pro.uc.
|
||||
#
|
||||
# The judgement decides whether QUIC is rejected in the fw4 ruleset, so every
|
||||
# branch is pinned: each of the four main_udp_node values, and each hop of
|
||||
# the resolution ('same' -> the main node, 'urltest' -> every member, a named
|
||||
# section, and 'nil').
|
||||
#
|
||||
# The interesting case is 'same' with a TCP-transport main node - the shape
|
||||
# this function exists for. It is invisible in the UCI value alone: both
|
||||
# fields read perfectly reasonable.
|
||||
#
|
||||
# A real UCI cursor is required (the function reads configuration), so the
|
||||
# runner stages a sandboxed config dir and passes the cursor in. cursor(dir)
|
||||
# reads <dir>/<name> directly, NOT <dir>/config/<name>.
|
||||
#
|
||||
# Each case re-seeds the sandbox from inside the .uc driver through the
|
||||
# cursor's own API and re-reads with a fresh cursor, so libuci's change
|
||||
# tracking cannot carry one case's options into the next.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_udp_transport.sh <repo-root> [work-dir]
|
||||
|
||||
ROOT="${1:-.}"
|
||||
WORK="${2:-/tmp/hp-udp-transport-test}"
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
FAILED=0
|
||||
|
||||
SANDBOX="$WORK/sandbox"
|
||||
STAGE="$WORK/scripts"
|
||||
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$SANDBOX" "$STAGE"
|
||||
|
||||
if ! command -v ucode > "/dev/null" 2>&1; then
|
||||
echo "NOT RUN: ucode is not on PATH."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# /sbin/validate_data does not exist off-target and production homeproxy-pro.uc
|
||||
# shells out to it from validation(); the driver never calls validation(), but
|
||||
# the module still has to load. tests/ucode/test_mock_sync.sh rewrites the
|
||||
# same literal for the same reason.
|
||||
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
|
||||
if [ ! -x "$VALIDATE_DATA" ] && [ -x "$ROOT/tests/toolchain/validate-data.sh" ]; then
|
||||
VALIDATE_DATA="$ROOT/tests/toolchain/validate-data.sh"
|
||||
fi
|
||||
|
||||
sed -e "s#/sbin/validate_data#$VALIDATE_DATA#" \
|
||||
"$ROOT/root/etc/homeproxy-pro/scripts/homeproxy-pro.uc" > "$STAGE/homeproxy-pro.uc"
|
||||
cp "$ROOT/tests/ucode/test_udp_transport.uc" "$STAGE/test_udp_transport.uc"
|
||||
|
||||
cd "$STAGE" || exit 1
|
||||
if HP_T_SANDBOX="$SANDBOX" ucode test_udp_transport.uc; then
|
||||
echo "PASS: udp_transport_verdict"
|
||||
else
|
||||
echo "FAIL: udp_transport_verdict"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
exit "$FAILED"
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Driver for tests/ucode/test_udp_transport.sh. Builds a UCI sandbox
|
||||
* in-process and asserts udp_transport_verdict()'s verdict for every branch
|
||||
* of its resolution.
|
||||
*
|
||||
* Each case gets a fresh cursor over the same sandbox directory after a fresh
|
||||
* seed, so libuci's change tracking cannot leak one case's options into the
|
||||
* next - a leaked `main_udp_node` would make a later case pass for the wrong
|
||||
* reason, which is the failure mode this file is most exposed to.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { writefile } from 'fs';
|
||||
import { udp_transport_verdict } from 'homeproxy-pro';
|
||||
|
||||
/* getenv(), not argv: a ucode script has no argv global, and the runner
|
||||
* convention here is HP_T_* environment values (see
|
||||
* test_homeproxy_utils.uc). */
|
||||
const SANDBOX = getenv('HP_T_SANDBOX');
|
||||
if (!SANDBOX || SANDBOX === '') {
|
||||
printf('FAIL: HP_T_SANDBOX is not set; run through test_udp_transport.sh\n');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let failures = 0,
|
||||
checks = 0,
|
||||
/* The case currently running, so a bare assertion name can be traced
|
||||
* back. Without it a failure reads "expected anytls, got nil" and there
|
||||
* are three ways to read that. */
|
||||
current_case = '(none)';
|
||||
|
||||
function expect(name, actual, want) {
|
||||
checks++;
|
||||
if (sprintf('%J', actual) !== sprintf('%J', want)) {
|
||||
printf('FAIL [%s] %s: expected %J, got %J\n', current_case, name, want, actual);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Seed the sandbox by rewriting the UCI file from scratch. `opts` sets
|
||||
* options on the `config` section; `nodes` maps a section name to its type.
|
||||
*
|
||||
* Written as text rather than built with add()/set()/commit() on purpose:
|
||||
* every case reuses the same section names, and add() on a name that already
|
||||
* exists does not reliably give back that name - the first couple of cases
|
||||
* passed and then every later one read the section as missing, which looks
|
||||
* exactly like a bug in the function under test and is not one. Truncating
|
||||
* the file first makes each case independent of the ones before it. */
|
||||
function seed(opts, nodes) {
|
||||
let out = 'config homeproxy-pro \'config\'\n';
|
||||
|
||||
/* A UCI list has to be written as `list`, one line per value. Emitting it
|
||||
* as `option` produces a scalar, uci.get() then hands the function a
|
||||
* string, and iterating a string walks its CHARACTERS - which reads as
|
||||
* "the url-test group has no members" for every group. */
|
||||
for (let k in keys(opts)) {
|
||||
let v = opts[k];
|
||||
if (type(v) === 'array') {
|
||||
for (let item in v)
|
||||
out += sprintf('\tlist %s \'%s\'\n', k, item);
|
||||
} else {
|
||||
out += sprintf('\toption %s \'%s\'\n', k, v);
|
||||
}
|
||||
}
|
||||
|
||||
for (let name in keys(nodes || {})) {
|
||||
out += sprintf('config homeproxy-pro \'%s\'\n', name);
|
||||
out += sprintf('\toption type \'%s\'\n', nodes[name]);
|
||||
out += sprintf('\toption address \'%s.example.com\'\n', name);
|
||||
}
|
||||
|
||||
writefile(SANDBOX + '/homeproxy-pro', out);
|
||||
}
|
||||
|
||||
/* Run one case: seed, open a fresh cursor, judge. */
|
||||
function verdict(name, opts, nodes, check) {
|
||||
current_case = name;
|
||||
seed(opts, nodes);
|
||||
let v = udp_transport_verdict('homeproxy-pro', cursor(SANDBOX));
|
||||
check(v);
|
||||
/* The reason is a log line, so it only has to be present and have to
|
||||
* mention the thing it is explaining - not be worded a particular way,
|
||||
* which would break every time the sentence is improved. */
|
||||
if (!v.reason || v.reason === '')
|
||||
expect(name + '.reason is set', v.reason, 'a non-empty sentence');
|
||||
}
|
||||
|
||||
const HY2 = { 'nodeHy2': 'hysteria2' },
|
||||
ANYTLS = { 'nodeAnyTLS': 'anytls' },
|
||||
MIXED = { 'nodeHy2': 'hysteria2', 'nodeTrojan': 'trojan' };
|
||||
|
||||
/* --- 'same': the case this function exists for ------------------------- */
|
||||
|
||||
verdict('same + udp-native main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'same' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
/* The regression: a TCP-transport main node with 'same'. The UCI values look
|
||||
* entirely reasonable, which is exactly why this was missed - the QUIC
|
||||
* reject stayed off and QUIC was tunnelled over TCP. */
|
||||
verdict('same + TCP-transport main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'same' }, ANYTLS,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'anytls');
|
||||
});
|
||||
|
||||
verdict('same + no main node', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'same' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- a named UDP node --------------------------------------------------- */
|
||||
|
||||
verdict('named udp-native node', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'nodeHy2' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
/* The pairing that actually fixes the measured problem: TCP main, UDP-native
|
||||
* sidecar. */
|
||||
verdict('named UDP-native beside TCP main', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'nodeHy2' }, MIXED,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'hysteria2');
|
||||
});
|
||||
|
||||
verdict('named TCP-transport node', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'nodeAnyTLS' },
|
||||
{ 'nodeHy2': 'hysteria2', 'nodeAnyTLS': 'anytls' },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('protocol', v.protocol, 'anytls');
|
||||
});
|
||||
|
||||
verdict('named node that does not exist', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeMissing' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- url-test membership ------------------------------------------------ */
|
||||
|
||||
/* Conservative on purpose: the group picks a member per connection, so ONE
|
||||
* TCP member is enough to put QUIC back on the stalling path whenever it
|
||||
* happens to be the selected one. */
|
||||
verdict('urltest with one TCP member', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest',
|
||||
main_udp_urltest_nodes: [ 'nodeHy2', 'nodeTrojan' ] }, MIXED,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, 'trojan');
|
||||
});
|
||||
|
||||
verdict('urltest all UDP-native', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest',
|
||||
main_udp_urltest_nodes: [ 'nodeHy2', 'nodeHy2' ] }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
});
|
||||
|
||||
verdict('urltest with no members', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'urltest' }, {},
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* --- the remaining shapes ----------------------------------------------- */
|
||||
|
||||
verdict('udp disabled', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2', main_udp_node: 'nil' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* An unset main_udp_node is 'nil', not an error: that is the factory default
|
||||
* and a fresh install must not read as misconfigured. */
|
||||
verdict('main_udp_node unset', { routing_mode: 'bypass_mainland_china',
|
||||
main_node: 'nodeHy2' }, HY2,
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, false);
|
||||
});
|
||||
|
||||
/* Custom mode is the user driving the rule graph by hand; this function must
|
||||
* not second-guess them, which is also what the QUIC gate did before it
|
||||
* learned to read the transport. */
|
||||
verdict('custom routing mode', { routing_mode: 'custom',
|
||||
main_node: 'nodeAnyTLS', main_udp_node: 'same' }, ANYTLS,
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, 'custom');
|
||||
});
|
||||
|
||||
/* --- the set itself ----------------------------------------------------- */
|
||||
|
||||
/* Every protocol the UI offers is classified, not just the ones this change
|
||||
* happens to touch. A protocol added to homeproxy-pro.js's table without a
|
||||
* decision here fails this, which is the point: the set has to be a
|
||||
* deliberate answer rather than a default that silently means "no".
|
||||
*
|
||||
* The native list is duplicated here on purpose - a test that read the
|
||||
* constant under test would agree with any change to it, including a wrong
|
||||
* one. Both lists come from the protocol table in
|
||||
* htdocs/luci-static/resources/homeproxy-pro.js. */
|
||||
const NATIVE = [ 'hysteria', 'hysteria2', 'tuic', 'wireguard' ],
|
||||
NOT_NATIVE = [ 'anytls', 'http', 'mixed', 'naive', 'shadowsocks',
|
||||
'shadowtls', 'snell', 'socks', 'ssh', 'trojan',
|
||||
'vless', 'vmess' ];
|
||||
|
||||
for (let type in NATIVE) {
|
||||
verdict('native: ' + type, { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': type },
|
||||
v => {
|
||||
expect('native', v.native, true);
|
||||
expect('protocol', v.protocol, type);
|
||||
});
|
||||
}
|
||||
|
||||
for (let type in NOT_NATIVE) {
|
||||
verdict('not native: ' + type, { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': type },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
expect('protocol', v.protocol, type);
|
||||
});
|
||||
}
|
||||
|
||||
/* A protocol nobody has classified must read as not-native. Failing closed
|
||||
* here means an unrecognised node gets QUIC rejected - browsers fall back to
|
||||
* TCP, which every transport carries - rather than QUIC tunnelled over a path
|
||||
* that may not be able to carry it. */
|
||||
verdict('unclassified protocol fails closed', { routing_mode: 'bypass_mainland_china',
|
||||
main_udp_node: 'nodeP' }, { 'nodeP': 'something-new' },
|
||||
v => {
|
||||
expect('native', v.native, false);
|
||||
expect('configured', v.configured, true);
|
||||
});
|
||||
|
||||
printf('%d checks, %d failures\n', checks, failures);
|
||||
exit(failures === 0 ? 0 : 1);
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-open-box
|
||||
PKG_VERSION:=0.1.287
|
||||
PKG_RELEASE:=11
|
||||
PKG_VERSION:=0.1.288
|
||||
PKG_RELEASE:=12
|
||||
|
||||
PKG_MAINTAINER:=liandu2024
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.10.4
|
||||
PKG_RELEASE:=317
|
||||
PKG_RELEASE:=318
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
margin-top: -0.34rem;
|
||||
}
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
|
||||
color: #525f7f;
|
||||
background: #fff;
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.10.1
|
||||
PKG_RELEASE:=137
|
||||
PKG_RELEASE:=138
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
margin-top: -0.34rem;
|
||||
}
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
|
||||
color: #525f7f;
|
||||
background: #fff;
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=196
|
||||
PKG_RELEASE:=83
|
||||
PKG_RELEASE:=84
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
@@ -27,7 +27,7 @@ PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
|
||||
|
||||
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Clash LuCI interface
|
||||
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/AnyTLS/Socks5/Clash LuCI interface
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+libuci-lua +lua +luci-compat +dns2tcp +dnsmasq-full \
|
||||
|
||||
@@ -397,6 +397,7 @@ local function get_active_node_runtime(sid)
|
||||
trojan = "Trojan",
|
||||
socks = "SOCKS5",
|
||||
hysteria2 = "Hysteria2",
|
||||
anytls = "AnyTLS",
|
||||
shadowsocks = "Shadowsocks",
|
||||
http = "HTTP"
|
||||
}
|
||||
|
||||
@@ -326,6 +326,13 @@ local function migrate_xray_protocol_nodes()
|
||||
luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss' && " .. "uci delete shadowsocksr.%s.v2ray_protocol", escaped_sid, escaped_sid))
|
||||
changed = true
|
||||
end
|
||||
if stype == "anytls" then
|
||||
uci:set("shadowsocksr", sid, "type", "v2ray")
|
||||
uci:set("shadowsocksr", sid, "v2ray_protocol", "anytls")
|
||||
if not section.tls then uci:set("shadowsocksr", sid, "tls", "1") end
|
||||
uci:save("shadowsocksr")
|
||||
changed = true
|
||||
end
|
||||
if stype == "hysteria2" then
|
||||
luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='hysteria2'", escaped_sid, escaped_sid))
|
||||
changed = true
|
||||
@@ -459,6 +466,9 @@ o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
|
||||
o:value("vless", translate("VLESS"))
|
||||
o:value("vmess", translate("VMess"))
|
||||
o:value("trojan", translate("Trojan"))
|
||||
if is_finded("xray") or is_finded("mihomo") then
|
||||
o:value("anytls", "AnyTLS")
|
||||
end
|
||||
o:value("shadowsocks", translate("ShadowSocks"))
|
||||
if is_finded("xray") then
|
||||
o:value("wireguard", translate("WireGuard"))
|
||||
@@ -551,6 +561,19 @@ o:depends({type = "v2ray", v2ray_protocol = "http", auth_enable = true})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "socks", socks_ver = "5", auth_enable = true})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "trojan"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "anytls"})
|
||||
|
||||
for _, field in ipairs({
|
||||
{"anytls_idle_session_check_interval", "Idle session check interval (seconds)", "30"},
|
||||
{"anytls_idle_session_timeout", "Idle session timeout (seconds)", "30"},
|
||||
{"anytls_min_idle_session", "Minimum idle sessions", "0"}
|
||||
}) do
|
||||
o = s:option(Value, field[1], translate(field[2]))
|
||||
o.placeholder = field[3]
|
||||
o.datatype = "uinteger"
|
||||
o.rmempty = true
|
||||
o:depends({type = "v2ray", v2ray_protocol = "anytls"})
|
||||
end
|
||||
|
||||
o = s:option(Value, "snell_psk", translate("Snell PSK"))
|
||||
o.password = true
|
||||
@@ -1435,6 +1458,7 @@ o:depends({type = "v2ray", v2ray_protocol = "vmess", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "trojan", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "anytls", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "socks", socks_ver = "5", reality = false})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "http", reality = false})
|
||||
o:depends("type", "trojan")
|
||||
@@ -1601,11 +1625,7 @@ o:depends("type", "hysteria2")
|
||||
o:depends("type", "trojan")
|
||||
o:depends("type", "tuic")
|
||||
o.description = translate("If true, allowss insecure connection at TLS client, e.g., TLS server uses unverifiable certificates.")
|
||||
-- Xray 支持时间判断
|
||||
-- if os.time() < os.time({year=2026,month=6,day=1}) then
|
||||
-- Xray 支持到 26.06.01
|
||||
o:depends("tls", true)
|
||||
o:depends({ type = "v2ray", v2ray_protocol = "vless", reality = true })
|
||||
o:depends("tls", true)
|
||||
|
||||
if xray_version_val >= 260131 then
|
||||
-- Xray 版本大于等于 26.1.31
|
||||
|
||||
@@ -48,7 +48,7 @@ local function clash_display_name(s)
|
||||
return "[CLASH]"
|
||||
end
|
||||
|
||||
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
|
||||
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/ANYTLS/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
|
||||
m:section(SimpleSection).template = "shadowsocksr/status"
|
||||
|
||||
local server_table = {}
|
||||
|
||||
@@ -140,6 +140,43 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return false;
|
||||
case "anytls":
|
||||
try {
|
||||
var url = new URL("http://" + ssu[1]);
|
||||
var params = url.searchParams;
|
||||
} catch(e) {
|
||||
alert(e);
|
||||
return false;
|
||||
}
|
||||
if (!hasMihomoOrXray()) {
|
||||
alert("AnyTLS requires Xray or Mihomo.");
|
||||
return false;
|
||||
}
|
||||
function setAnytlsField(name, value) {
|
||||
var field = document.getElementsByName('cbid.shadowsocksr.' + sid + '.' + name)[0];
|
||||
if (!field) return;
|
||||
if (field.type === "checkbox") field.checked = value;
|
||||
else field.value = value;
|
||||
field.dispatchEvent(event);
|
||||
}
|
||||
setAnytlsField("type", "v2ray");
|
||||
setAnytlsField("v2ray_protocol", "anytls");
|
||||
setAnytlsField("transport", "raw");
|
||||
setAnytlsField("reality", false);
|
||||
setAnytlsField("tls", true);
|
||||
setAnytlsField("mux", false);
|
||||
setAnytlsField("server", normalizeHost(url.hostname));
|
||||
setAnytlsField("server_port", url.port || "443");
|
||||
setAnytlsField("password", decodeURIComponent(url.username + (url.password ? ":" + url.password : "")));
|
||||
setAnytlsField("tls_host", params.get("sni") || params.get("servername") || "");
|
||||
setAnytlsField("tls_alpn", params.get("alpn") || "");
|
||||
setAnytlsField("fingerprint", params.get("fp") || params.get("fingerprint") || "");
|
||||
setAnytlsField("tls_CertSha", params.get("pcs") || "");
|
||||
setAnytlsField("tls_CertByName", params.get("vcn") || "");
|
||||
setAnytlsField("insecure", /^(1|true)$/i.test(params.get("insecure") || params.get("allowInsecure") || params.get("allow_insecure") || ""));
|
||||
setAnytlsField("alias", url.hash ? decodeURIComponent(url.hash.slice(1)) : "");
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return false;
|
||||
case "hysteria2":
|
||||
case "hy2":
|
||||
try {
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
margin-top: -0.34rem;
|
||||
}
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
|
||||
color: #525f7f;
|
||||
background: #fff;
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:51
|
||||
msgid ""
|
||||
"<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH "
|
||||
"<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/ANYTLS/NAIVEPROXY/SOCKS5/CLASH "
|
||||
"etc.</h3>"
|
||||
msgstr ""
|
||||
|
||||
@@ -3369,3 +3369,12 @@ msgstr ""
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1678
|
||||
msgid "xudpProxyUDP443"
|
||||
msgstr ""
|
||||
|
||||
msgid "Idle session check interval (seconds)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Idle session timeout (seconds)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimum idle sessions"
|
||||
msgstr ""
|
||||
@@ -78,10 +78,10 @@ msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:51
|
||||
msgid ""
|
||||
"<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH "
|
||||
"<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/ANYTLS/NAIVEPROXY/SOCKS5/CLASH "
|
||||
"etc.</h3>"
|
||||
msgstr ""
|
||||
"<h3>支持 SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH 等协"
|
||||
"<h3>支持 SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/ANYTLS/NAIVEPROXY/SOCKS5/CLASH 等协"
|
||||
"议。</h3>"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:269
|
||||
@@ -3861,3 +3861,12 @@ msgstr "对被代理的 UDP/443 流量处理方式"
|
||||
|
||||
#~ msgid "Include Mihomo (Clash Support)"
|
||||
#~ msgstr "包含 Mihomo(Clash 支持)"
|
||||
|
||||
msgid "Idle session check interval (seconds)"
|
||||
msgstr "空闲会话检查间隔(秒)"
|
||||
|
||||
msgid "Idle session timeout (seconds)"
|
||||
msgstr "空闲会话超时(秒)"
|
||||
|
||||
msgid "Minimum idle sessions"
|
||||
msgstr "最小空闲会话数"
|
||||
@@ -1186,6 +1186,12 @@ normalize_xray_protocol_nodes() {
|
||||
changed=1
|
||||
fi
|
||||
fi
|
||||
if [ "$type" = "anytls" ]; then
|
||||
uci -q set "$NAME.$section.type=v2ray"
|
||||
uci -q set "$NAME.$section.v2ray_protocol=anytls"
|
||||
[ -n "$(uci_get_by_name "$section" tls)" ] || uci -q set "$NAME.$section.tls=1"
|
||||
changed=1
|
||||
fi
|
||||
if [ "$type" = "hysteria2" ]; then
|
||||
echolog "提示:检测到原生 Hysteria2 节点,自动迁移为 Xray(Hysteria2)。"
|
||||
uci -q set "$NAME.$section.type=v2ray"
|
||||
|
||||
@@ -849,6 +849,13 @@ local function build_v2ray_mihomo_proxy(sid)
|
||||
proxy.cipher = get_server_field(sid, "encrypt_method_ss", "none")
|
||||
proxy.password = get_server_field(sid, "password", "")
|
||||
build_shadowsocks_plugin(proxy, sid)
|
||||
elseif protocol == "anytls" then
|
||||
proxy.type = "anytls"
|
||||
proxy.password = get_server_field(sid, "password", "")
|
||||
apply_trojan_tls_options(proxy, sid)
|
||||
proxy["idle-session-check-interval"] = number_or_nil(get_server_field(sid, "anytls_idle_session_check_interval", ""))
|
||||
proxy["idle-session-timeout"] = number_or_nil(get_server_field(sid, "anytls_idle_session_timeout", ""))
|
||||
proxy["min-idle-session"] = number_or_nil(get_server_field(sid, "anytls_min_idle_session", ""))
|
||||
elseif protocol == "hysteria2" then
|
||||
proxy.type = "hysteria2"
|
||||
proxy.password = get_server_field(sid, "hy2_auth", "")
|
||||
|
||||
@@ -47,6 +47,14 @@ end
|
||||
local chain_local_port = split(chain, "/")[2] or "0"
|
||||
|
||||
local server = ucursor:get_all("shadowsocksr", server_section)
|
||||
-- AnyTLS subscriptions previously used a standalone type with no launcher.
|
||||
if server.type == "anytls" then
|
||||
server.type = "v2ray"
|
||||
server.v2ray_protocol = "anytls"
|
||||
end
|
||||
if server.v2ray_protocol == "anytls" and server.tls == nil then
|
||||
server.tls = "1"
|
||||
end
|
||||
local socks_server = ucursor:get_all("shadowsocksr", "@socks5_proxy[0]") or {}
|
||||
local xray_fragment = ucursor:get_all("shadowsocksr", "@global_xray_fragment[0]") or {}
|
||||
local xray_noise = ucursor:get_all("shadowsocksr", "@xray_noise_packets[0]") or {}
|
||||
@@ -363,6 +371,16 @@ function outbound:handleIndex(index)
|
||||
wireguard = function()
|
||||
wireguard()
|
||||
end,
|
||||
anytls = function()
|
||||
outbound_settings = {
|
||||
address = server.server,
|
||||
port = tonumber(server.server_port),
|
||||
password = server.password,
|
||||
idleSessionCheckInterval = tonumber(server.anytls_idle_session_check_interval),
|
||||
idleSessionTimeout = tonumber(server.anytls_idle_session_timeout),
|
||||
minIdleSession = tonumber(server.anytls_min_idle_session)
|
||||
}
|
||||
end,
|
||||
hysteria2 = function()
|
||||
xray_hysteria2()
|
||||
end
|
||||
@@ -510,10 +528,7 @@ Xray.outbounds = {
|
||||
fingerprint = server.fingerprint,
|
||||
allowInsecure = (function()
|
||||
if server.tls_CertSha and server.tls_CertSha ~= "" then return nil end
|
||||
if os.date("%Y.%m.%d") < "2026.06.01" then
|
||||
return server.insecure == "1"
|
||||
end
|
||||
return nil
|
||||
return server.insecure == "1"
|
||||
end)(),
|
||||
serverName = server.tls_host,
|
||||
certificates = server.certificate and {
|
||||
@@ -849,7 +864,7 @@ Xray.outbounds = {
|
||||
((remarks and remarks ~= "") and (node_id .. "." .. remarks) or ("direct" .. "." .. node_id)) or nil
|
||||
}
|
||||
} or nil,
|
||||
mux = (server.v2ray_protocol ~= "hysteria2" and server.v2ray_protocol ~= "wireguard") and {
|
||||
mux = (server.v2ray_protocol ~= "hysteria2" and server.v2ray_protocol ~= "wireguard" and server.v2ray_protocol ~= "anytls") and {
|
||||
-- mux
|
||||
enabled = (server.mux == "1"), -- Mux
|
||||
concurrency = (server.mux == "1" and (tonumber(server.concurrency) or -1)) or nil, -- TCP 最大并发连接数
|
||||
|
||||
@@ -472,8 +472,11 @@ local function parseAnytlsShare(content)
|
||||
end
|
||||
return sni
|
||||
end)(),
|
||||
allow_insecure = (params.insecure == "1" or params.allow_insecure == "1" or params.allowinsecure == "1"),
|
||||
client_fingerprint = params.fp or params.fingerprint or ""
|
||||
allow_insecure = (params.insecure == "1" or params.insecure == "true" or params.allow_insecure == "1" or params.allow_insecure == "true" or params.allowinsecure == "1" or params.allowinsecure == "true"),
|
||||
client_fingerprint = params.fp or params.fingerprint or "",
|
||||
alpn = params.alpn,
|
||||
pinned_cert = params.pcs,
|
||||
cert_name = params.vcn
|
||||
}
|
||||
end
|
||||
|
||||
@@ -514,13 +517,16 @@ local function buildAnytlsClashYaml(entries, group_name)
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
local function anytls_to_mihomo_node(entry)
|
||||
local function anytls_to_node(entry)
|
||||
if not entry then
|
||||
return nil
|
||||
end
|
||||
|
||||
local result = {
|
||||
type = "anytls",
|
||||
type = "v2ray",
|
||||
v2ray_protocol = "anytls",
|
||||
tls = "1",
|
||||
transport = "raw",
|
||||
alias = entry.name,
|
||||
raw_alias = entry.name,
|
||||
server = entry.server,
|
||||
@@ -528,7 +534,10 @@ local function anytls_to_mihomo_node(entry)
|
||||
password = entry.password,
|
||||
tls_host = entry.sni,
|
||||
insecure = entry.allow_insecure and "1" or "0",
|
||||
fingerprint = entry.client_fingerprint
|
||||
fingerprint = entry.client_fingerprint,
|
||||
tls_alpn = entry.alpn,
|
||||
tls_CertSha = entry.pinned_cert,
|
||||
tls_CertByName = entry.cert_name
|
||||
}
|
||||
|
||||
local saved_alias = result.alias
|
||||
@@ -806,7 +815,7 @@ local function to_mihomo_proxy(node)
|
||||
proxy.obfs = string_from_value(node.obfs_type)
|
||||
proxy["obfs-password"] = string_from_value(node.salamander)
|
||||
end
|
||||
elseif node.type == "anytls" then
|
||||
elseif node.type == "anytls" or (node.type == "v2ray" and node.v2ray_protocol == "anytls") then
|
||||
proxy.type = "anytls"
|
||||
proxy.password = node.password
|
||||
proxy.sni = string_from_value(node.tls_host)
|
||||
@@ -854,6 +863,7 @@ local function can_group_into_mihomo(node)
|
||||
or node.v2ray_protocol == "hysteria2"
|
||||
or node.v2ray_protocol == "hy2"
|
||||
or node.v2ray_protocol == "snell"
|
||||
or node.v2ray_protocol == "anytls"
|
||||
) then
|
||||
return true
|
||||
end
|
||||
@@ -2712,7 +2722,7 @@ local execute = function()
|
||||
if line:match("^anytls://") then
|
||||
local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
|
||||
if parsed then
|
||||
table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
|
||||
table.insert(anytls_nodes, anytls_to_node(parsed))
|
||||
end
|
||||
elseif line ~= "" then
|
||||
table.insert(normal_nodes, node)
|
||||
|
||||
@@ -2,6 +2,81 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.7.2-r6] - Loon / Surge 家族写法按官方文档对齐(LEGACY_ISSUES 7.9)
|
||||
|
||||
`docs/LEGACY_ISSUES.md` 第七节 2.7.2 审计新发现的**第五轮**修复:(a)(b)(c)(d)
|
||||
全部实施,(e) 保持保守。改动集中在 `output_formats.surge_line`(生成端)与
|
||||
`parser_surge.parse_surge_line`(解析端)—— 两端必须成对,否则导出的配置再导入
|
||||
回来会**静默丢字段**。
|
||||
|
||||
### 修复 — 7.9(a):Loon 的 TLS 开关是 `over-tls`,Trojan 例外
|
||||
|
||||
Loon 文档(`nsloon.app/docs/Node/`)通篇用 `over-tls=true`,从没把 `tls` 列为
|
||||
它的别名;本生成器此前对 Loon 也写 Surge 的 `tls=true`。若 Loon 忽略它,vmess /
|
||||
vless / trojan 会按明文连 —— 包括 Reality 节点(公钥写了、TLS 标志却没生效)。
|
||||
|
||||
```lua
|
||||
local tls_flag = is_loon and "over-tls=true" or "tls=true"
|
||||
```
|
||||
|
||||
**Trojan 反过来**:Loon 文档的明文与 Reality 两条示例都**没有**任何 TLS 键
|
||||
(Trojan 本身建立在 TLS 之上),所以 Loon 的 trojan 行整个不写 TLS 参数。
|
||||
Surge 家族的 `tls=true` 是文档要求的,保持不变。
|
||||
|
||||
### 修复 — 7.9(b):Loon 的 UDP 参数是 `udp`
|
||||
|
||||
Loon 文档每个示例都写 `udp=true`,`udp-relay` 一次都没出现。Surge 家族仍用
|
||||
`udp-relay=true`。
|
||||
|
||||
### 修复 — 7.9(c):Surge 家族补上 vmess 的 `encrypt-method`
|
||||
|
||||
Surge 手册的 vmess 页有具名参数 `encrypt-method`,取值只有 `aes-128-gcm`
|
||||
(默认)与 `chacha20-ietf-poly1305`。本生成器此前只写 `username=`,于是 cipher 是
|
||||
chacha20 的节点被 Surge 按默认的 `aes-128-gcm` 去连 —— **静默用错算法**。新增
|
||||
`SURGE_VMESS_CIPHER` 做拼写映射(模型写 `chacha20-poly1305`,客户端写
|
||||
`chacha20-ietf-poly1305`)。映射不到的取值(`zero` / `none` / `auto` /
|
||||
`aes-128-cfb`)不写该参数、节点保留。
|
||||
|
||||
### 修复 — 7.9(d):Loon 的 ss / ssr / hysteria2 凭据是位置参数
|
||||
|
||||
```
|
||||
Shadowsocks,服务器,端口,加密方式,"密码"
|
||||
ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…
|
||||
Hysteria2,服务器,端口,"密码"
|
||||
```
|
||||
|
||||
此前一律写具名的 `encrypt-method=` / `password=` —— 落在 Loon 的语法里就是第 4、
|
||||
5 个位置参数各拿到一串 `key=value` 文本,加密方式解析不出来、密码也不再是密码,
|
||||
节点必然连不上。ssr 的 `protocol=` / `obfs=` 及其参数仍是具名。
|
||||
|
||||
### 同步修改解析端(并修掉一个既有缺陷)
|
||||
|
||||
`parse_surge_line` 补上位置参数提取,保住「Loon 输出 → 导入」回环。其中
|
||||
**SSR 此前根本没有取字段的分支** —— 不论哪种写法导入,加密方式、密码、协议插件、
|
||||
混淆方式全是 nil,节点带着空凭据进入统一模型,导出时再被静默丢掉。这是本轮顺带
|
||||
修掉的既有缺陷,不是 (d) 引入的。
|
||||
|
||||
### 保持保守 — 7.9(e)
|
||||
|
||||
值里含英文逗号时仍按「整条丢弃」处理。Loon 的引号按文档确实能保住逗号,但本行的
|
||||
具名参数一律不带引号,同一行混用两种约定会让「哪个值被包裹」变成依赖客户端实现的
|
||||
行为。已加断言钉住这个选择。
|
||||
|
||||
### 测试
|
||||
|
||||
* `tests/ssr_test.lua`:`loon encrypt-method` 一条改为断言位置参数写法,新增 9 条
|
||||
Loon 输出 → 导入回环断言。
|
||||
* `tests/output_formats_test.lua`:新增 7.9 专节 24 条断言。
|
||||
* 反向验证:逐条把代码改回旧行为,确认测试变红(3 / 2 / 2 / 2 / 5 / 1 / 1 条 FAIL),
|
||||
还原后全部 PASS。
|
||||
* 全套 **59 个测试文件、0 失败**。
|
||||
|
||||
### 本轮新发现(未实施,记录在 LEGACY_ISSUES)
|
||||
|
||||
(g) Loon 的节点类型清单里没有 Hysteria v1,本生成器仍会输出 `hysteria` 行;
|
||||
(h) `skip-cert-verify=1` 这个取值两家文档都没写;(i) 协议名大小写(Loon 文档用
|
||||
首字母大写,本生成器用小写)。三条都只记录、不动代码。
|
||||
|
||||
## [2.7.2-r5] - 后端错误串 msgid 化(LEGACY_ISSUES 7.7)
|
||||
|
||||
`docs/LEGACY_ISSUES.md` 第七节 2.7.2 审计新发现的**第四轮**修复。
|
||||
|
||||
@@ -10,7 +10,7 @@ PKG_NAME:=luci-app-substore
|
||||
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
|
||||
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
|
||||
PKG_VERSION:=2.7.2
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
|
||||
|
||||
|
||||
@@ -658,12 +658,13 @@ crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每
|
||||
| 7.6 | 修复 | **已修复**(第一轮,见下) |
|
||||
| 7.7 | A:后端错误串 msgid 化 | **已实施**(第四轮,见下) |
|
||||
| 7.8 | A:删除 `age.lua` / `age_test.lua` | **已删除**(第一轮) |
|
||||
| 7.9 | 记录(第二轮实施时新发现) | 待决策 |
|
||||
| 7.9 | (a)(b)(c)(d) 全部实施;(e) 保持保守(含逗号整条丢弃) | **已实施**(第五轮,见下) |
|
||||
|
||||
7.5 / 7.6 的修复见本节末尾「7.5 / 7.6 修复记录」,
|
||||
7.1 / 7.3 / 7.4 的实施见「第二轮修复记录」,
|
||||
7.2 的实施见「第三轮修复记录」,
|
||||
7.7 的实施见「第四轮修复记录」。
|
||||
7.7 的实施见「第四轮修复记录」,
|
||||
7.9 的实施见「第五轮修复记录」。
|
||||
|
||||
| # | 问题 | 位置 | 依据 | 影响 |
|
||||
|---|---|---|---|---|
|
||||
@@ -675,7 +676,7 @@ crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每
|
||||
| 7.6 | ~~Loon 的 `transport=ws` 未映射到 `net`~~ **已修复** | `parser_surge.parse_surge_line` 的 `transport=` 分支 | 只认 `ws=true`(Surge 旧写法)与 `obfs=ws`(QX);`nsloon.app/docs/Node/` 用 `transport=ws` + `path=` + `host=` | Loon 的 ws 节点导入后 `net=tcp`,`path` / `host` 全丢 → 导出到任何格式都按 tcp 连,握手失败**且不报错** |
|
||||
| 7.7 | ~~后端模块仍有 **113 处**硬编码中文字符串字面量(注释外)~~ **已实施** | `core.lua` 47 / `http.lua` 47 / `parser.lua` 9 / `util.lua` 5 / `output_wireguard_conf.lua` 3 / `node.lua` 2 | 扫描脚本(去注释后提取含 CJK / 全角的字符串字面量),见下 | 控制器文案已接入 i18n,但这些来自后端的失败原因经 `?err=` **原样**显示,英文界面下仍是中文。**已按 A 实施**:全部改为语言中立的英文 msgid,组合消息用 `msg.lua` 的分隔符机制,翻译只在显示边界发生(详见「第四轮修复记录」) |
|
||||
| 7.8 | ~~`root/usr/share/substore/age.lua` 与 `tests/age_test.lua` 未被 git 跟踪,且 `age.lua` 未被任何模块 `require`~~ **已删除** | 仓库根 | `grep -rn require` 无引用 | 未随包发布;留在工作区会被后续审计反复重新评估 |
|
||||
| 7.9 | Loon 的节点行仍有**多处**与官方文档不一致;Surfboard 的 trojan / vmess / vless 凭据同样是位置参数;Loon 的双引号其实**能**保住逗号 | `output_formats.surge_line` | `nsloon.app/docs/Node/`;`getsurfboard.com` | 逐条见下「7.9 的明细」。均未实施(本轮只做决策里点名的 7.4-A) |
|
||||
| 7.9 | ~~Loon 的节点行仍有**多处**与官方文档不一致~~ **已实施** | `output_formats.surge_line` / `parser_surge.parse_surge_line` | `nsloon.app/docs/Node/`;`manual.nssurge.com`;`getsurfboard.com` | 逐条见下「7.9 的明细」。**已按 (a)(b)(c)(d) 全部实施、(e) 保持保守**:Loon 的 TLS 开关改 `over-tls`(Trojan 除外)、UDP 改 `udp`、Surge 家族补 `encrypt-method`、Loon 的 ss/ssr/hysteria2 改位置参数;解析端同步跟进以保住回环(详见「第五轮修复记录」)。实施中新发现三条待决策项,见下「第五轮新发现」 |
|
||||
|
||||
### 7.7 的统计口径与例外(修复前必读)
|
||||
|
||||
@@ -708,11 +709,13 @@ crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每
|
||||
(英文),翻译只发生在显示边界(视图 / 控制器),后端模块不得 `require("luci.i18n")`
|
||||
—— `substore-cron.sh` 会在独立的 lua 进程里跑 `core.sync`,那里没有 LuCI 环境。
|
||||
|
||||
### 7.9 的明细(第二轮实施时新发现,**均未实施**)
|
||||
### 7.9 的明细(第二轮实施时新发现,**第五轮已实施**)
|
||||
|
||||
第二轮为实施 7.4-A 去核对 Loon 的节点行文档(`nsloon.app/docs/Node/`),顺带发现
|
||||
Loon 与 Surge 家族的不一致远不止「凭据位置」。以下每条都对照官方文档原文,
|
||||
**没有推测项**;本轮按用户的轮次安排只做决策里点名的 7.4-A,其余记录在此待决策。
|
||||
**第五轮(2.7.2-r6)已按 (a)(b)(c)(d) 全部实施、(e) 保持保守**,各条下方的
|
||||
「→ 实施」注明落点。
|
||||
|
||||
**(a) Loon 的 TLS 开关写作 `over-tls`,本生成器写 `tls`** —— 影响最大的一条。
|
||||
Loon 文档的通用参数表与 Reality 示例都用 `over-tls=true`:
|
||||
@@ -730,9 +733,18 @@ Loon 文档**只**为传输参数明说了旧写法别名(`ws=true` ↔ `trans
|
||||
**包括本轮新支持的 Reality 节点**(公钥写了、TLS 标志却没生效)。
|
||||
这条不修,7.4-A 与 Reality 支持在真机 Loon 上的收益都要打折。
|
||||
|
||||
**→ 实施(第五轮)**:`surge_line` 引入 `tls_flag`(`is_loon and "over-tls=true"
|
||||
or "tls=true"`),vmess / vless 用它。**Trojan 例外**:复核 Loon 文档的明文与
|
||||
Reality 两条 Trojan 示例,**都没有任何 TLS 键** —— Trojan 本身就建立在 TLS 之上,
|
||||
Loon 不再要这个开关,所以 Loon 的 Trojan 行整个不写 TLS 参数(Surge 家族的
|
||||
`tls=true` 是文档要求的,保持不变)。解析端早已同时认 `tls` 与 `over-tls`
|
||||
(`parser_surge.lua`),回环不受影响。
|
||||
|
||||
**(b) Loon 的 UDP 参数写作 `udp`,本生成器写 `udp-relay`** —— 同上,Loon 文档是
|
||||
`udp=true`。影响较小(UDP 转发失效,节点本身还能用)。
|
||||
|
||||
**→ 实施(第五轮)**:`udp = is_loon and "udp=true" or "udp-relay=true"`。
|
||||
|
||||
**(c) Surge 家族的 vmess 不输出 `encrypt-method`** —— Surge 手册的 vmess 页有
|
||||
`encrypt-method` 参数,取值 `aes-128-gcm`(默认)或 `chacha20-ietf-poly1305`。
|
||||
本生成器只写 `username=`,所以 cipher 是 chacha20 的节点会被 Surge 按默认的
|
||||
@@ -740,6 +752,11 @@ Loon 文档**只**为传输参数明说了旧写法别名(`ws=true` ↔ `trans
|
||||
统一模型写 `chacha20-poly1305`,Surge / Loon 都写 `chacha20-ietf-poly1305`
|
||||
(Loon 侧本轮已用 `LOON_VMESS_CIPHER` 映射,Surge 侧没有对应参数可写)。
|
||||
|
||||
**→ 实施(第五轮)**:新增 `SURGE_VMESS_CIPHER`(`aes-128-gcm` → `aes-128-gcm`、
|
||||
`chacha20-poly1305` → `chacha20-ietf-poly1305`),在**非 Loon** 的 vmess 分支输出
|
||||
`encrypt-method=`。取值不在 Surge 清单里时(`zero` / `none` / `auto` /
|
||||
`aes-128-cfb`)不写该参数、节点保留 —— 写一个客户端读不懂的取值比不写更糟。
|
||||
|
||||
**(d) Loon 的 shadowsocks / ssr / hysteria2 凭据也是位置参数** —— Loon 文档:
|
||||
|
||||
```
|
||||
@@ -750,6 +767,12 @@ Loon 文档**只**为传输参数明说了旧写法别名(`ws=true` ↔ `trans
|
||||
|
||||
本生成器对这些协议一律写具名参数(`encrypt-method=` / `password=`)。
|
||||
|
||||
**→ 实施(第五轮)**:Loon 分支下 ss / ssr 的加密方式与密码、hysteria2 的密码
|
||||
一律改位置参数(密码走 `loon_positional` 加双引号);ssr 的 `protocol=` / `obfs=`
|
||||
及其参数仍是具名。**解析端同步跟进**(`parser_surge.parse_surge_line`)——
|
||||
其中 **SSR 此前根本没有取字段的分支**,任何写法导入后加密方式、密码、协议插件、
|
||||
混淆方式全是 nil,是这一轮顺带修掉的既有缺陷。
|
||||
|
||||
**(e) Loon 的双引号确实能保住逗号** —— 文档原文「参数值中含有英文逗号时,
|
||||
请使用双引号包裹」。本文件此前注释写的「Loon 的那对引号只是标记,值里的逗号
|
||||
照样是分隔符」是**错的**,已在本轮更正。当前实现仍按「含逗号就整条丢弃」处理
|
||||
@@ -763,6 +786,38 @@ vmess 页:其 Format 模板写 `{username}` 位置,但**实际示例与参
|
||||
trojan 页同样写 `username=` / `password=`。所以 Surge / Surfboard / SurgeMac 的
|
||||
具名写法**是对的**,7.4 只该改 Loon —— 7.4 行已按此更正。
|
||||
|
||||
**(e) 实施说明(第五轮)**:保持不变。生成端仍按「位置参数或具名值里含英文逗号
|
||||
就整条丢弃」处理,并在 `tests/output_formats_test.lua` 钉了一条断言防止日后被
|
||||
无意改成半截输出。
|
||||
|
||||
### 第五轮新发现(**均未实施,待决策**)
|
||||
|
||||
第五轮实施 (a)–(d) 时对照 `nsloon.app/docs/Node/` 全文复核,另发现三处不一致。
|
||||
每条都只陈述**文档写了什么**与**本实现写了什么**,不推测客户端的实际行为。
|
||||
|
||||
**(g) Loon 没有 Hysteria v1,本生成器会把 `hysteria` 行输出给它** —— Loon 文档的
|
||||
节点类型清单里只有 **Hysteria2**,没有 Hysteria(v1)。而 `FAMILY_CAPS` 只按
|
||||
vless / ssr 分客户端能力,`hysteria` 不在过滤表里,所以统一模型里的 hysteria v1
|
||||
节点会生成一条 Loon 读不懂的行。修法二选一:(i) 给 `FAMILY_CAPS.loon` 加
|
||||
`hysteria = false` 整条丢弃(与丢弃 wireguard / ssr 同一约定);(ii) 保持现状。
|
||||
**未实施** —— 需要先确认「Loon 是否真的不认 `hysteria`」以及 hysteria v1 节点在
|
||||
用户订阅里的实际占比。
|
||||
|
||||
**(h) `skip-cert-verify` 写成 `1`,两家文档都没写这个取值** —— Loon 文档的示例
|
||||
一律写 `skip-cert-verify=false`;Surge 手册的 TLS 页只写「Optional, boolean,
|
||||
default: false」,**没有**列出可接受的字面量。本生成器一律写
|
||||
`skip-cert-verify=1`。`1` 是否被接受**无依据**。解析端两种都认(`parser_surge`
|
||||
把非 `0` / 非 `false` 都当 true),所以回环不受影响,改与不改都不会破坏导入。
|
||||
**未实施** —— 改成 `true` 会动到所有 Surge 系输出的既有断言,收益(若 `1` 本就被
|
||||
接受则为零)尚不明确。
|
||||
|
||||
**(i) 协议名大小写:Loon 文档一律首字母大写,本生成器一律小写** —— Loon 文档写
|
||||
`Hysteria2` / `VMess` / `VLESS` / `Shadowsocks` / `AnyTLS`;本生成器输出
|
||||
`hysteria2` / `vmess` / `vless` / `shadowsocks` / `anytls`。Loon 官方定位是
|
||||
「兼容 Surge 配置」,而 Surge 手册的语法行用的是小写(`Name = vmess, <host>, …`),
|
||||
所以小写**很可能**被接受,但**没有文档明说**。**未实施** —— 这是全协议面的写法,
|
||||
改动面大且依据不足;若日后确认大小写敏感,应改的是 Loon 分支的类型名映射。
|
||||
|
||||
## 7.1 / 7.2 / 7.3 的实测探针
|
||||
|
||||
**修复前**(`[2.7.2-r2]` 及更早):
|
||||
@@ -1176,6 +1231,90 @@ message"` 后跑 `tests/backend_i18n_test.lua`,得到 **1 条 FAIL**(`no po
|
||||
|
||||
---
|
||||
|
||||
## 第五轮修复记录(7.9)
|
||||
|
||||
**7.9 (a)(b)(c)(d) 按官方文档对齐 Loon / Surge 家族的写法**,(e) 保持保守。
|
||||
全部改动集中在 `root/usr/share/substore/output_formats.lua` 的 `M.surge_line`
|
||||
(生成端)与 `root/usr/share/substore/parser_surge.lua` 的 `parse_surge_line`
|
||||
(解析端)—— 两端必须成对,否则导出的配置再导入回来会静默丢字段。
|
||||
|
||||
### (a) TLS 开关:Loon 用 `over-tls`,Trojan 例外
|
||||
|
||||
```lua
|
||||
local is_loon = (flavor == "loon")
|
||||
local tls_flag = is_loon and "over-tls=true" or "tls=true"
|
||||
```
|
||||
|
||||
vmess / vless 的 `if tls then … end` 改用 `tls_flag`。**Trojan 反过来**:Loon 文档
|
||||
的明文与 Reality 两条示例都**没有**任何 TLS 键(Trojan 本身建立在 TLS 之上),
|
||||
所以 Loon 的 trojan 行整个不写 TLS 参数,Surge 家族的 `tls=true` 保持不动。
|
||||
|
||||
### (b) UDP 开关:Loon 用 `udp`
|
||||
|
||||
```lua
|
||||
if n.udp then e[#e + 1] = is_loon and "udp=true" or "udp-relay=true" end
|
||||
```
|
||||
|
||||
### (c) Surge 家族补 `encrypt-method`
|
||||
|
||||
新增 `SURGE_VMESS_CIPHER`(`aes-128-gcm` → `aes-128-gcm`、
|
||||
`chacha20-poly1305` → `chacha20-ietf-poly1305`),在**非 Loon** 的 vmess 分支写出。
|
||||
映射不到的取值(`zero` / `none` / `auto` / `aes-128-cfb`)**不写该参数**、节点保留:
|
||||
Surge 的清单只有那两个取值,写一个客户端读不懂的比不写更糟(写与不写都是
|
||||
`aes-128-gcm` 默认值,语义等价)。Loon 侧加密方式本就在位置参数上,不受影响。
|
||||
|
||||
### (d) Loon 的 ss / ssr / hysteria2 改位置参数
|
||||
|
||||
```
|
||||
Shadowsocks,服务器,端口,加密方式,"密码"
|
||||
ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…
|
||||
Hysteria2,服务器,端口,"密码"
|
||||
```
|
||||
|
||||
密码走 `loon_positional`(加双引号,值里含双引号则整条丢弃);ssr 的
|
||||
`protocol=` / `obfs=` / `obfs-param=` / `protocol-param=` 仍是具名参数。
|
||||
|
||||
### 解析端同步(**并修掉一个既有缺陷**)
|
||||
|
||||
`parse_surge_line` 的字段提取补上位置参数:
|
||||
|
||||
| 协议 | 生成端(Loon) | 解析端 |
|
||||
|---|---|---|
|
||||
| shadowsocks | `加密方式,"密码"` | `kv.* or pos[1]` / `kv.password or pos[2]` |
|
||||
| ssr | 同上 + 具名插件参数 | **新增分支**(此前 ssr 没有任何取字段的分支) |
|
||||
| hysteria2 | `"密码"` | `kv.password or pos[1]` |
|
||||
|
||||
**SSR 此前根本没有取字段的分支** —— 不论哪种写法导入,加密方式、密码、协议插件、
|
||||
混淆方式全是 nil,节点带着空凭据进入统一模型,导出时再被静默丢掉。这是本轮顺带
|
||||
修掉的既有缺陷,不是 (d) 引入的。`obfs-param` / `protocol-param` 两种拼写都写一份
|
||||
(`PROTO_FIELDS` / `DEFAULTS` 用带连字符的规范写法,各输出模块读的是下划线别名,
|
||||
与 `parser.lua` 同时写 `method` / `cipher` 同一理由)。
|
||||
|
||||
### 顺手更正的陈旧注释
|
||||
|
||||
`surge_line` 里 anytls 分支上方原写着「这是本文件里唯一按 flavor 分叉的协议:
|
||||
trojan / vmess / vless 一律用具名参数」—— 7.4-A 之后就不成立了,已改写。
|
||||
|
||||
### 测试
|
||||
|
||||
* `tests/ssr_test.lua`:`loon encrypt-method` 一条改为断言位置参数写法,并新增
|
||||
**9 条 Loon 输出 → 导入回环**断言。
|
||||
* `tests/output_formats_test.lua`:新增 **7.9 专节 24 条**((a) 6 / (b) 3 / (c) 4 /
|
||||
(d) 4 / (e) 1 / 回环 6)。
|
||||
* **反向验证**(逐条把代码改回旧行为,确认测试变红):`tls_flag` 写死 `tls=true`
|
||||
→ 3 条 FAIL;UDP 写死 `udp-relay` → 2 条 FAIL;去掉 `encrypt-method` → 2 条 FAIL;
|
||||
Loon 的 ss / hysteria2 改回具名 → 2 条 FAIL;解析端 ssr 分支停用 → 5 条 FAIL;
|
||||
解析端 hysteria2 / ss 位置参数去掉 → 各 1 条 FAIL。还原后全部 PASS。
|
||||
|
||||
全套 **59 个测试文件、0 失败**。
|
||||
|
||||
### 本轮新发现(未实施,见上文「第五轮新发现」)
|
||||
|
||||
(g) Loon 没有 Hysteria v1;(h) `skip-cert-verify=1` 的取值无文档依据;
|
||||
(i) 协议名大小写。三条都只记录,不动代码。
|
||||
|
||||
---
|
||||
|
||||
# 附:P2 修复范围(不含本文件所列项)
|
||||
|
||||
P2 为审计表中**其余中危 / 低危**项中性质明确、无需另行决策的缺陷,例如
|
||||
|
||||
@@ -33,6 +33,25 @@ local LOON_VMESS_CIPHER = {
|
||||
["chacha20-ietf-poly1305"] = "chacha20-ietf-poly1305",
|
||||
}
|
||||
|
||||
-- Surge 家族的 VMess「加密方式」是**具名**参数 encrypt-method,与 Loon 把它放在
|
||||
-- 位置参数上不同(manual.nssurge.com/policies/vmess.html 原文:
|
||||
-- `Name = vmess, <host>, <port>, username=<UUID>[, parameter=value, ...]`
|
||||
-- encrypt-method 取值只有 aes-128-gcm / chacha20-ietf-poly1305,默认 aes-128-gcm;
|
||||
-- Surfboard 的 vmess 页同此)。
|
||||
--
|
||||
-- 只有模型的 chacha20-poly1305 需要写出来:它的拼写与客户端不同
|
||||
-- (chacha20-ietf-poly1305),不写这个参数 Surge 会按默认的 aes-128-gcm 去连,
|
||||
-- chacha20 的节点**静默用错算法**——握手失败或更糟。aes-128-gcm 是客户端默认值,
|
||||
-- 写与不写等价,这里仍写出来是为了让导出结果自解释。
|
||||
-- 其余取值(none / zero / auto / aes-128-cfb)不在客户端的清单里,映射不到就不写
|
||||
-- 该参数、由客户端取默认值;写一个客户端不认的取值是「写了读不懂的东西」,
|
||||
-- 与本文件丢弃 wireguard / ssr 同一约定。
|
||||
local SURGE_VMESS_CIPHER = {
|
||||
["aes-128-gcm"] = "aes-128-gcm",
|
||||
["chacha20-poly1305"] = "chacha20-ietf-poly1305",
|
||||
["chacha20-ietf-poly1305"] = "chacha20-ietf-poly1305",
|
||||
}
|
||||
|
||||
-- 支持 Reality 的目标格式。Reality 只在 Loon 的节点行里有官方文档:
|
||||
-- nsloon.app/docs/Node/ 给出 VLESS / VMess / Trojan / AnyTLS 的 Reality 示例,
|
||||
-- 统一用 public-key / short-id 两个具名参数。
|
||||
@@ -91,6 +110,16 @@ function M.surge_line(n, flavor)
|
||||
local e = {}
|
||||
local tls = (n.security and n.security ~= "none") or (n.tls and n.tls ~= false and n.tls ~= "none")
|
||||
|
||||
-- TLS 开关的**参数名**各家不同:
|
||||
-- Surge / Surfboard / SurgeMac:`tls=true`
|
||||
-- (manual.nssurge.com/policies/vmess.html 与 .../vless.html 的 tls 参数)
|
||||
-- Loon:`over-tls=true`
|
||||
-- (nsloon.app/docs/Node/ 通篇用 over-tls;文档只为 ws 传输列过别名
|
||||
-- 「ws=true ↔ transport=ws」,从没把 tls 列为 over-tls 的别名)
|
||||
-- 只换名字,不换语义:两边都是「本连接走 TLS」这一个布尔量。
|
||||
local is_loon = (flavor == "loon")
|
||||
local tls_flag = is_loon and "over-tls=true" or "tls=true"
|
||||
|
||||
-- Loon 的凭据写法:端口之后的**位置参数**且用双引号包起来
|
||||
-- Trojan = Trojan,h,p,"密码"
|
||||
-- VLESS = VLESS,h,p,"UUID"
|
||||
@@ -140,8 +169,22 @@ function M.surge_line(n, flavor)
|
||||
end
|
||||
|
||||
if proto == "shadowsocks" then
|
||||
e[#e + 1] = "encrypt-method=" .. (n.method or n.cipher or "aes-256-gcm")
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
local cipher = n.method or n.cipher or "aes-256-gcm"
|
||||
if is_loon then
|
||||
-- Loon 的 Shadowsocks 把加密方式与密码都画在位置参数上,密码带双引号:
|
||||
-- `Shadowsocks,服务器,端口,加密方式,"密码"`(nsloon.app/docs/Node/)
|
||||
-- 加密方式是裸值,与 VMess 行的加密方式同形。
|
||||
-- 此前写成具名的 encrypt-method= / password=,落在 Loon 的语法里就是
|
||||
-- 第 4、5 个位置参数各拿到一串 `key=value` 文本 —— 加密方式解析不出来,
|
||||
-- 密码也不再是密码,节点必然连不上。
|
||||
local pw = loon_positional(n.password)
|
||||
if not pw then return nil end
|
||||
positional[#positional + 1] = cipher
|
||||
positional[#positional + 1] = pw
|
||||
else
|
||||
e[#e + 1] = "encrypt-method=" .. cipher
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
end
|
||||
elseif proto == "vmess" then
|
||||
if flavor == "loon" then
|
||||
-- Loon 的 VMess 把加密方式与 UUID 都放在位置参数上(见 loon_positional)
|
||||
@@ -152,8 +195,11 @@ function M.surge_line(n, flavor)
|
||||
positional[#positional + 1] = id
|
||||
else
|
||||
e[#e + 1] = "username=" .. (n.uuid or "")
|
||||
-- Surge 家族把加密方式写在具名参数 encrypt-method 上(见 SURGE_VMESS_CIPHER)
|
||||
local sc = SURGE_VMESS_CIPHER[n.cipher]
|
||||
if sc then e[#e + 1] = "encrypt-method=" .. sc end
|
||||
end
|
||||
if tls then e[#e + 1] = "tls=true" end
|
||||
if tls then e[#e + 1] = tls_flag end
|
||||
if n.sni then e[#e + 1] = "sni=" .. n.sni end
|
||||
-- VMess 的 Reality 与 VLESS 同形。Loon 文档的 VMess-Reality 示例:
|
||||
-- `VMess = VMess,host,443,aes-128-gcm,"uuid",transport=tcp,alterId=0,`
|
||||
@@ -172,7 +218,7 @@ function M.surge_line(n, flavor)
|
||||
else
|
||||
e[#e + 1] = "username=" .. (n.uuid or "")
|
||||
end
|
||||
if tls then e[#e + 1] = "tls=true" end
|
||||
if tls then e[#e + 1] = tls_flag end
|
||||
if n.sni then e[#e + 1] = "sni=" .. n.sni end
|
||||
if n.flow then e[#e + 1] = "flow=" .. n.flow end
|
||||
add_reality()
|
||||
@@ -186,7 +232,11 @@ function M.surge_line(n, flavor)
|
||||
else
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
end
|
||||
e[#e + 1] = "tls=true"
|
||||
-- Loon 的 Trojan 行**没有** TLS 开关:nsloon.app/docs/Node/ 的明文与 Reality
|
||||
-- 两条示例都只写 `Trojan,服务器,端口,"密码",sni=…[,public-key=…,short-id=…]`,
|
||||
-- 既没有 over-tls 也没有 tls。Trojan 本身就建立在 TLS 之上,Loon 不再要这个
|
||||
-- 开关;写上去是客户端读不懂的参数。Surge 家族的 tls=true 相反是文档要求的。
|
||||
if not is_loon then e[#e + 1] = "tls=true" end
|
||||
if n.sni then e[#e + 1] = "sni=" .. n.sni end
|
||||
add_reality()
|
||||
elseif proto == "anytls" then
|
||||
@@ -205,9 +255,12 @@ function M.surge_line(n, flavor)
|
||||
-- 其余 flavor(surge / surgemac)走上面的具名写法。
|
||||
-- (Egern 已不在本文件里 —— 它的配置是 YAML,见 output_egern.lua。)
|
||||
--
|
||||
-- 这是本文件里唯一按 flavor 分叉的协议:trojan / vmess / vless 一律用具名
|
||||
-- 参数,而 Loon / Surfboard 的文档同样把它们的凭据画在位置参数上 —— 那是
|
||||
-- 既有实现,改动面大且不在本次范围内,所以只让 anytls 按文档写对。
|
||||
-- 按 flavor 分叉的协议不止 anytls:Loon 的 vmess / vless / trojan /
|
||||
-- shadowsocks / ssr / hysteria2 同样把凭据画在位置参数上,TLS 开关与 UDP
|
||||
-- 参数的名字也和 Surge 家族不同 —— 那些分叉在各分支里就地处理(见
|
||||
-- is_loon / tls_flag 与 loon_positional 上方的说明)。anytls 的特别之处
|
||||
-- 只在于它的密码写法在**三家之间**都不同(Surge 具名 / Surfboard 裸位置
|
||||
-- 参数 / Loon 带引号位置参数),所以只有它在这里写成了三分支。
|
||||
if flavor == "surfboard" then
|
||||
positional[#positional + 1] = n.password or ""
|
||||
elseif flavor == "loon" then
|
||||
@@ -220,8 +273,21 @@ function M.surge_line(n, flavor)
|
||||
if n.sni then e[#e + 1] = "sni=" .. n.sni end
|
||||
add_reality()
|
||||
elseif proto == "ssr" then
|
||||
e[#e + 1] = "encrypt-method=" .. (n.method or n.cipher or "aes-128-cfb")
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
local cipher = n.method or n.cipher or "aes-128-cfb"
|
||||
if is_loon then
|
||||
-- Loon 的 ShadowsocksR 同样把加密方式与密码放在位置参数上(密码带引号),
|
||||
-- 其余仍是具名参数:
|
||||
-- `ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…`
|
||||
-- (nsloon.app/docs/Node/)。具名的 encrypt-method= / password= 在这里
|
||||
-- 会各自占掉一个位置参数,加密方式与密码都解析不出来。
|
||||
local pw = loon_positional(n.password)
|
||||
if not pw then return nil end
|
||||
positional[#positional + 1] = cipher
|
||||
positional[#positional + 1] = pw
|
||||
else
|
||||
e[#e + 1] = "encrypt-method=" .. cipher
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
end
|
||||
e[#e + 1] = "protocol=" .. (n.protocol or "origin")
|
||||
e[#e + 1] = "obfs=" .. (n.obfs or "plain")
|
||||
local op = n.obfs_param or n["obfs-param"]
|
||||
@@ -229,7 +295,15 @@ function M.surge_line(n, flavor)
|
||||
if op and op ~= "" then e[#e + 1] = "obfs-param=" .. op end
|
||||
if pp and pp ~= "" then e[#e + 1] = "protocol-param=" .. pp end
|
||||
elseif proto == "hysteria2" or proto == "hysteria" then
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
if is_loon then
|
||||
-- Loon 的 Hysteria2 把密码放在位置参数上,带双引号:
|
||||
-- `Hysteria2,服务器,端口,"密码",sni=…`(nsloon.app/docs/Node/)
|
||||
local pw = loon_positional(n.password)
|
||||
if not pw then return nil end
|
||||
positional[#positional + 1] = pw
|
||||
else
|
||||
e[#e + 1] = "password=" .. (n.password or "")
|
||||
end
|
||||
if n.sni then e[#e + 1] = "sni=" .. n.sni end
|
||||
elseif proto == "tuic" then
|
||||
e[#e + 1] = "username=" .. (n.uuid or "")
|
||||
@@ -266,7 +340,10 @@ function M.surge_line(n, flavor)
|
||||
end
|
||||
|
||||
if n["skip-cert-verify"] then e[#e + 1] = "skip-cert-verify=1" end
|
||||
if n.udp then e[#e + 1] = "udp-relay=true" end
|
||||
-- UDP 转发的参数名同样各家不同:Surge 家族是 udp-relay=true,Loon 是 udp=true
|
||||
-- (nsloon.app/docs/Node/ 的每个示例都写 udp=true,udp-relay 一次都没出现;
|
||||
-- 相关的另外两个开关叫 udp-over-tcp / udp-port)。
|
||||
if n.udp then e[#e + 1] = is_loon and "udp=true" or "udp-relay=true" end
|
||||
|
||||
-- 参数值里的逗号无法用这些格式表达,整条丢弃(返回 nil,由调用方剔除)。
|
||||
-- [Proxy] 行与 QX 的 [server_local] 行都是逗号分隔的 `key=value` 序列,
|
||||
|
||||
@@ -106,9 +106,38 @@ local function parse_surge_line(name, rest)
|
||||
local data = {
|
||||
proto = proto, name = util.trim(name), server = server, port = port,
|
||||
}
|
||||
-- 凭据在两种写法里的落点,与 output_formats.surge_line 的分叉一一对应:
|
||||
-- shadowsocks : Surge 家族 `encrypt-method=…,password=…`;Loon `加密方式,"密码"`
|
||||
-- ssr : 同上,另有 protocol= / obfs= / obfs-param= / protocol-param=
|
||||
-- vmess : Surge 家族 `username=…`;Loon `加密方式,"UUID"`
|
||||
-- vless : Surge 家族 `username=…`;Loon `"UUID"`
|
||||
-- trojan : Surge 家族 `password=…`;Loon `"密码"`
|
||||
-- hysteria2 : Surge 家族 `password=…`;Loon `"密码"`
|
||||
-- anytls : Surge 家族 `password=…`;Surfboard 裸位置参数;Loon `"密码"`
|
||||
-- 位置参数(pos)只收集「不含 = 的字段」,具名写法完全不受影响,所以两种写法
|
||||
-- 可以无条件并存 —— 本函数导入的既有 Surge / Loon 配置不会因此改变解析结果。
|
||||
if proto == "shadowsocks" then
|
||||
data.method = kv["encrypt-method"] or kv.cipher or kv.method
|
||||
data.password = kv.password
|
||||
data.method = kv["encrypt-method"] or kv.cipher or kv.method or pos[1]
|
||||
data.password = kv.password or pos[2]
|
||||
elseif proto == "ssr" then
|
||||
-- SSR 此前**没有**取字段的分支:不管哪种写法导入,加密方式、密码、协议插件、
|
||||
-- 混淆方式全是 nil,节点带着空凭据进入统一模型,导出时再被静默丢掉。
|
||||
data.method = kv["encrypt-method"] or kv.cipher or kv.method or pos[1]
|
||||
data.password = kv.password or pos[2]
|
||||
if kv.protocol then data.protocol = kv.protocol end
|
||||
if kv.obfs then data.obfs = kv.obfs end
|
||||
-- 两种拼写都写一份:PROTO_FIELDS / DEFAULTS 用的是带连字符的规范写法
|
||||
-- (表单据此渲染),而 parser.lua 与各输出模块读的是下划线别名
|
||||
-- (`n.obfs_param or n["obfs-param"]`)—— 与 parser.lua 同时写
|
||||
-- method / cipher 是同一个理由,只写一份就会在某条输出路径上丢失。
|
||||
if kv["obfs-param"] then
|
||||
data["obfs-param"] = kv["obfs-param"]
|
||||
data.obfs_param = kv["obfs-param"]
|
||||
end
|
||||
if kv["protocol-param"] then
|
||||
data["protocol-param"] = kv["protocol-param"]
|
||||
data.protocol_param = kv["protocol-param"]
|
||||
end
|
||||
elseif proto == "vmess" or proto == "vless" then
|
||||
-- Loon 的 VMess 位置参数是「加密方式, UUID」两个字段;VLESS 只有一个 UUID
|
||||
if proto == "vmess" and pos[2] then
|
||||
@@ -123,7 +152,8 @@ local function parse_surge_line(name, rest)
|
||||
elseif proto == "anytls" then
|
||||
data.password = kv.password or pos[1]
|
||||
elseif proto == "hysteria2" or proto == "hysteria" then
|
||||
data.password = kv.password
|
||||
-- Loon 的 Hysteria2 把密码写在端口之后的位置参数上(`Hysteria2,h,p,"密码"`)
|
||||
data.password = kv.password or pos[1]
|
||||
end
|
||||
|
||||
-- Reality 参数。Loon 的 VLESS / VMess / Trojan / AnyTLS Reality 行共用
|
||||
|
||||
@@ -153,5 +153,114 @@ local qx_ok = fmts.to_qx({ { proto = "trojan", name = "XY", server = "1.1.1.1",
|
||||
check("qx newline name does not add lines", count_lines(qx_nl) == count_lines(qx_ok))
|
||||
check("qx newline name flattened", qx_nl:find("X Y", 1, true) ~= nil)
|
||||
|
||||
-- ---------- 7.9:Loon 与 Surge 家族的参数名 / 写法分叉 ----------
|
||||
-- 依据:nsloon.app/docs/Node/(Loon 节点行)与 manual.nssurge.com/policies/*.html
|
||||
-- (Surge 家族)。两家语法在四处不同:TLS 开关名、UDP 开关名、VMess 加密方式的
|
||||
-- 落点、以及凭据是否写成端口之后的位置参数。写错任何一处都不会报错,只是节点
|
||||
-- 连不上 —— 所以每处分叉都钉一条断言。
|
||||
|
||||
-- (a) TLS 开关名:Surge 家族 tls=true,Loon over-tls=true
|
||||
local vm_tls = { proto = "vmess", name = "V", server = "1.1.1.1", port = 443,
|
||||
uuid = "u", security = "tls" }
|
||||
check("surge vmess tls=true", fmts.surge_line(vm_tls, "surge"):find("tls=true", 1, true) ~= nil)
|
||||
check("loon vmess over-tls=true", fmts.surge_line(vm_tls, "loon"):find("over-tls=true", 1, true) ~= nil)
|
||||
-- 具名参数之间是 ", " 分隔,所以独立的 tls 参数会以 ", tls=true" 出现;
|
||||
-- 直接找 "tls=true" 会命中 "over-tls=true" 的子串,必须带上前导分隔符。
|
||||
check("loon vmess no bare tls=true",
|
||||
fmts.surge_line(vm_tls, "loon"):find(", tls=true", 1, true) == nil)
|
||||
|
||||
local vl_tls = { proto = "vless", name = "L", server = "1.1.1.1", port = 443,
|
||||
uuid = "u", security = "tls" }
|
||||
check("surge vless tls=true", fmts.surge_line(vl_tls, "surge"):find("tls=true", 1, true) ~= nil)
|
||||
check("loon vless over-tls=true", fmts.surge_line(vl_tls, "loon"):find("over-tls=true", 1, true) ~= nil)
|
||||
|
||||
-- Trojan 是例外:Loon 文档的 Trojan 示例(明文与 Reality 两条)里根本没有 TLS 键
|
||||
-- —— Trojan 本身就建立在 TLS 之上,Loon 不再要这个开关。Surge 家族的 tls=true
|
||||
-- 则是文档要求的,不能跟着一起删。
|
||||
local tj_node = { proto = "trojan", name = "T", server = "1.1.1.1", port = 443, password = "p" }
|
||||
check("surge trojan tls=true", fmts.surge_line(tj_node, "surge"):find("tls=true", 1, true) ~= nil)
|
||||
local loon_tj = fmts.surge_line(tj_node, "loon")
|
||||
check("loon trojan has no tls key", loon_tj:find("tls", 1, true) == nil)
|
||||
check("loon trojan positional password", loon_tj:find('trojan, 1.1.1.1, 443, "p"', 1, true) ~= nil)
|
||||
|
||||
-- (b) UDP 开关名:Surge 家族 udp-relay=true,Loon udp=true
|
||||
local udp_node = { proto = "trojan", name = "U", server = "1.1.1.1", port = 443,
|
||||
password = "p", udp = true }
|
||||
check("surge udp-relay=true", fmts.surge_line(udp_node, "surge"):find("udp-relay=true", 1, true) ~= nil)
|
||||
local loon_udp = fmts.surge_line(udp_node, "loon")
|
||||
check("loon udp=true", loon_udp:find("udp=true", 1, true) ~= nil)
|
||||
check("loon has no udp-relay", loon_udp:find("udp-relay", 1, true) == nil)
|
||||
|
||||
-- (c) Surge 家族的 VMess 加密方式写在具名参数 encrypt-method 上,取值只有
|
||||
-- aes-128-gcm / chacha20-ietf-poly1305(默认前者)。模型的 chacha20-poly1305
|
||||
-- 与客户端拼写不同,不写出来 Surge 会按默认的 aes-128-gcm 去连 —— 静默用错算法。
|
||||
local vm_chacha = { proto = "vmess", name = "C", server = "1.1.1.1", port = 443,
|
||||
uuid = "u", cipher = "chacha20-poly1305" }
|
||||
check("surge vmess chacha mapped",
|
||||
fmts.surge_line(vm_chacha, "surge"):find("encrypt-method=chacha20-ietf-poly1305", 1, true) ~= nil)
|
||||
local vm_gcm = { proto = "vmess", name = "C", server = "1.1.1.1", port = 443,
|
||||
uuid = "u", cipher = "aes-128-gcm" }
|
||||
check("surge vmess aes-128-gcm",
|
||||
fmts.surge_line(vm_gcm, "surge"):find("encrypt-method=aes-128-gcm", 1, true) ~= nil)
|
||||
-- 不在客户端清单里的取值(zero / none / auto / aes-128-cfb)不写该参数、节点保留:
|
||||
-- 写一个客户端读不懂的取值比不写更糟
|
||||
local vm_zero = { proto = "vmess", name = "C", server = "1.1.1.1", port = 443,
|
||||
uuid = "u", cipher = "zero" }
|
||||
local zero_line = fmts.surge_line(vm_zero, "surge")
|
||||
check("surge vmess unmapped cipher omitted",
|
||||
zero_line ~= nil and zero_line:find("encrypt-method", 1, true) == nil)
|
||||
-- Loon 把加密方式放在位置参数上,不该出现具名的 encrypt-method
|
||||
check("loon vmess no named encrypt-method",
|
||||
fmts.surge_line(vm_chacha, "loon"):find("encrypt-method", 1, true) == nil)
|
||||
|
||||
-- (d) Loon 的 shadowsocks / hysteria2 凭据同样是位置参数(密码带双引号)
|
||||
local ss_node = { proto = "shadowsocks", name = "S", server = "1.1.1.1", port = 8388,
|
||||
method = "aes-256-gcm", password = "pw" }
|
||||
check("surge ss named", fmts.surge_line(ss_node, "surge"):find("encrypt-method=aes-256-gcm", 1, true) ~= nil)
|
||||
check("loon ss positional",
|
||||
fmts.surge_line(ss_node, "loon"):find('shadowsocks, 1.1.1.1, 8388, aes-256-gcm, "pw"', 1, true) ~= nil)
|
||||
|
||||
local hy_node = { proto = "hysteria2", name = "H", server = "1.1.1.1", port = 443, password = "pw" }
|
||||
check("surge hysteria2 named", fmts.surge_line(hy_node, "surge"):find("password=pw", 1, true) ~= nil)
|
||||
check("loon hysteria2 positional",
|
||||
fmts.surge_line(hy_node, "loon"):find('hysteria2, 1.1.1.1, 443, "pw"', 1, true) ~= nil)
|
||||
|
||||
-- (e) 值里含英文逗号:Loon 的引号按文档确实能保住逗号,本实现仍按「整条丢弃」
|
||||
-- 处理(具名参数不带引号,同一行混用两种约定会让「哪个值被包裹」变成依赖客户端
|
||||
-- 实现的行为)。这里钉住这个保守选择,避免日后被无意改成半截输出。
|
||||
local ss_comma = { proto = "shadowsocks", name = "S", server = "1.1.1.1", port = 8388,
|
||||
method = "aes-256-gcm", password = "pa,ss" }
|
||||
check("loon comma password drops node", fmts.surge_line(ss_comma, "loon") == nil)
|
||||
|
||||
-- ---------- Loon 输出 → 导入回环 ----------
|
||||
-- 生成端与解析端必须成对改动:只改生成端的话,导出的 Loon 配置再导入回来会静默
|
||||
-- 丢掉凭据(parser_surge 此前对 ssr 根本没有取字段的分支,对 shadowsocks /
|
||||
-- hysteria2 也不认位置参数)。
|
||||
local psurge = require("substore.parser_surge")
|
||||
local function loon_roundtrip(node)
|
||||
return psurge.parse(fmts.to_loon({ node }, { name = "P" }))[1]
|
||||
end
|
||||
|
||||
local rt_ss = loon_roundtrip(ss_node)
|
||||
check("loon rt ss method", rt_ss and rt_ss.method == "aes-256-gcm")
|
||||
check("loon rt ss password", rt_ss and rt_ss.password == "pw")
|
||||
|
||||
local rt_hy = loon_roundtrip(hy_node)
|
||||
check("loon rt hysteria2 password", rt_hy and rt_hy.password == "pw")
|
||||
check("loon rt hysteria2 security", rt_hy and rt_hy.security == "tls")
|
||||
|
||||
local rt_vm = loon_roundtrip(vm_tls)
|
||||
check("loon rt vmess uuid", rt_vm and rt_vm.uuid == "u")
|
||||
check("loon rt vmess security", rt_vm and rt_vm.security == "tls")
|
||||
|
||||
local rt_vl = loon_roundtrip(vl_tls)
|
||||
check("loon rt vless uuid", rt_vl and rt_vl.uuid == "u")
|
||||
check("loon rt vless security", rt_vl and rt_vl.security == "tls")
|
||||
|
||||
-- Trojan:Loon 行里不再有 TLS 开关,security 由 node.normalize 的 TLS_ONLY 补回
|
||||
local rt_tj = loon_roundtrip(tj_node)
|
||||
check("loon rt trojan password", rt_tj and rt_tj.password == "p")
|
||||
check("loon rt trojan security", rt_tj and rt_tj.security == "tls")
|
||||
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -75,8 +75,28 @@ check("shadowrocket has ssr", shr and util.base64_decode(shr):find("ssr://") ~=
|
||||
-- ---------- Loon 保留 SSR,Surge 丢弃 ----------
|
||||
local loon = output.generate({ n }, "loon")
|
||||
check("loon has ssr", loon and loon:find("ssr, 127%.0%.0%.1, 8388") ~= nil)
|
||||
check("loon encrypt-method", loon and loon:find("encrypt%-method=aes%-128%-cfb") ~= nil)
|
||||
-- Loon 把加密方式与密码写成端口之后的**位置参数**(密码带双引号),不是具名参数:
|
||||
-- `ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…`(nsloon.app/docs/Node/)
|
||||
-- 具名的 encrypt-method= / password= 在 Loon 的语法里会各自占掉一个位置参数,
|
||||
-- 加密方式与密码都解析不出来。
|
||||
check("loon cipher positional", loon and loon:find('ssr, 127%.0%.0%.1, 8388, aes%-128%-cfb, "test%-password"') ~= nil)
|
||||
check("loon no named encrypt-method", loon and loon:find("encrypt%-method=") == nil)
|
||||
check("loon protocol", loon and loon:find("protocol=auth_aes128_md5") ~= nil)
|
||||
|
||||
-- ---------- Loon 输出 → 导入回环(位置参数写法) ----------
|
||||
-- 生成端与解析端必须成对:只改生成端的话,导出的 Loon 配置再导入回来会丢掉
|
||||
-- 加密方式与密码(parser_surge 此前对 ssr 根本没有取字段的分支)。
|
||||
local loon_parsed = require("substore.parser_surge").parse(loon)
|
||||
check("loon roundtrip count", #loon_parsed == 1)
|
||||
local lr = loon_parsed[1]
|
||||
check("loon roundtrip proto", lr and lr.proto == "ssr")
|
||||
check("loon roundtrip method", lr and lr.method == "aes-128-cfb")
|
||||
check("loon roundtrip password", lr and lr.password == "test-password")
|
||||
check("loon roundtrip protocol", lr and lr.protocol == "auth_aes128_md5")
|
||||
check("loon roundtrip obfs", lr and lr.obfs == "http_simple")
|
||||
check("loon roundtrip obfs_param", lr and (lr.obfs_param == "download.windowsupdate.com"
|
||||
or lr["obfs-param"] == "download.windowsupdate.com"))
|
||||
check("loon roundtrip name", lr and lr.name == "测试节点")
|
||||
local surge = output.generate({ n }, "surge")
|
||||
check("surge drops ssr", surge and surge:find("127%.0%.0%.1") == nil)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=my-default-settings
|
||||
PKG_VERSION:=2
|
||||
PKG_RELEASE:=32
|
||||
PKG_RELEASE:=33
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ msgid "upload"
|
||||
msgstr "上传"
|
||||
|
||||
msgid "UPTIME"
|
||||
msgstr "在线时长"
|
||||
msgstr "已运行"
|
||||
|
||||
msgid "STORAGE"
|
||||
msgstr "存储"
|
||||
|
||||
Reference in new issue
Block a user