From 7fd585b5d4579ce3a84194400721fcd02f0f400f Mon Sep 17 00:00:00 2001 From: xream Date: Wed, 17 Jan 2024 09:15:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SurgeMac=20=E6=94=AF=E6=8C=81=20externa?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/core/proxy-utils/parsers/index.js | 84 +++++++++++++++++- .../core/proxy-utils/parsers/peggy/surge.js | 18 ++-- .../core/proxy-utils/parsers/peggy/surge.peg | 18 ++-- .../core/proxy-utils/producers/surgemac.js | 88 +++++++++++++++---- 5 files changed, 173 insertions(+), 37 deletions(-) diff --git a/backend/package.json b/backend/package.json index df915df..cb53c5d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.171", + "version": "2.14.172", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { diff --git a/backend/src/core/proxy-utils/parsers/index.js b/backend/src/core/proxy-utils/parsers/index.js index c23ebfe..d0b0cf3 100644 --- a/backend/src/core/proxy-utils/parsers/index.js +++ b/backend/src/core/proxy-utils/parsers/index.js @@ -1,4 +1,11 @@ -import { getIfNotBlank, isPresent, isNotBlank, getIfPresent } from '@/utils'; +import { + isIPv4, + isIPv6, + getIfNotBlank, + isPresent, + isNotBlank, + getIfPresent, +} from '@/utils'; import getSurgeParser from './peggy/surge'; import getLoonParser from './peggy/loon'; import getQXParser from './peggy/qx'; @@ -872,6 +879,76 @@ function Surge_Socks5() { return { name, test, parse }; } +function Surge_External() { + const name = 'Surge External Parser'; + const test = (line) => { + return /^.*=\s*external/.test(line.split(',')[0]); + }; + const parse = (line) => { + let parsed = /^\s*(.*?)\s*?=\s*?external\s*?,\s*(.*?)\s*$/.exec(line); + + // eslint-disable-next-line no-unused-vars + let [_, name, other] = parsed; + line = other; + + // exec = "/usr/bin/ssh" 或 exec = /usr/bin/ssh + let exec = /(,|^)\s*?exec\s*?=\s*"(.*?)"\s*?(,|$)/.exec(line)?.[2]; + if (!exec) { + exec = /(,|^)\s*?exec\s*?=\s*(.*?)\s*?(,|$)/.exec(line)?.[2]; + } + + // local-port = "1080" 或 local-port = 1080 + let localPort = /(,|^)\s*?local-port\s*?=\s*"(.*?)"\s*?(,|$)/.exec( + line, + )?.[2]; + if (!localPort) { + localPort = /(,|^)\s*?local-port\s*?=\s*(.*?)\s*?(,|$)/.exec( + line, + )?.[2]; + } + + const argsRegex = /(,|^)\s*?args\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g; + let argsMatch; + const args = []; + while ((argsMatch = argsRegex.exec(line)) !== null) { + if (argsMatch[3] != null) { + args.push(argsMatch[3]); + } else if (argsMatch[4] != null) { + args.push(argsMatch[4]); + } + } + const addressesRegex = + /(,|^)\s*?addresses\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g; + let addressesMatch; + const addresses = []; + while ((addressesMatch = addressesRegex.exec(line)) !== null) { + let ip; + if (addressesMatch[3] != null) { + ip = addressesMatch[3]; + } else if (addressesMatch[4] != null) { + ip = addressesMatch[4]; + } + if (ip != null) { + ip = `${ip}`.trim().replace(/^\[/, '').replace(/\]$/, ''); + } + if (isIP(ip)) { + addresses.push(ip); + } + } + + const proxy = { + type: 'external', + name, + exec, + 'local-port': localPort, + args, + addresses, + }; + return proxy; + }; + return { name, test, parse }; +} + function Surge_Snell() { const name = 'Surge Snell Parser'; const test = (line) => { @@ -907,6 +984,10 @@ function Surge_Hysteria2() { return { name, test, parse }; } +function isIP(ip) { + return isIPv4(ip) || isIPv6(ip); +} + export default [ URI_SS(), URI_SSR(), @@ -924,6 +1005,7 @@ export default [ Surge_WireGuard(), Surge_Hysteria2(), Surge_Socks5(), + Surge_External(), Loon_SS(), Loon_SSR(), Loon_VMess(), diff --git a/backend/src/core/proxy-utils/parsers/peggy/surge.js b/backend/src/core/proxy-utils/parsers/peggy/surge.js index 33aef91..006c3c4 100644 --- a/backend/src/core/proxy-utils/parsers/peggy/surge.js +++ b/backend/src/core/proxy-utils/parsers/peggy/surge.js @@ -36,7 +36,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v return proxy; } -shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "ss"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { @@ -46,7 +46,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ $set(proxy, "plugin-opts.path", obfs.path); } } -vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; if (proxy.aead) { @@ -56,18 +56,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/ } handleWebsocket(); } -trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "trojan"; handleWebsocket(); } -https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; proxy.tls = true; } http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; } -snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "snell"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { @@ -76,10 +76,10 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_ $set(proxy, "obfs-opts.path", obfs.path); } } -tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "tuic"; } -tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "tuic"; proxy.version = 5; } @@ -89,10 +89,10 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "hysteria2"; } -socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; } -socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; proxy.tls = true; } diff --git a/backend/src/core/proxy-utils/parsers/peggy/surge.peg b/backend/src/core/proxy-utils/parsers/peggy/surge.peg index b79ba07..cdcaab5 100644 --- a/backend/src/core/proxy-utils/parsers/peggy/surge.peg +++ b/backend/src/core/proxy-utils/parsers/peggy/surge.peg @@ -34,7 +34,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v return proxy; } -shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "ss"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { @@ -44,7 +44,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ $set(proxy, "plugin-opts.path", obfs.path); } } -vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; if (proxy.aead) { @@ -54,18 +54,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/ } handleWebsocket(); } -trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "trojan"; handleWebsocket(); } -https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; proxy.tls = true; } http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; } -snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "snell"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { @@ -74,10 +74,10 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_ $set(proxy, "obfs-opts.path", obfs.path); } } -tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "tuic"; } -tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "tuic"; proxy.version = 5; } @@ -87,10 +87,10 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "hysteria2"; } -socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; } -socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { +socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; proxy.tls = true; } diff --git a/backend/src/core/proxy-utils/producers/surgemac.js b/backend/src/core/proxy-utils/producers/surgemac.js index ca8a3bb..95a1bab 100644 --- a/backend/src/core/proxy-utils/producers/surgemac.js +++ b/backend/src/core/proxy-utils/producers/surgemac.js @@ -1,13 +1,17 @@ import { Result } from './utils'; import Surge_Producer from './surge'; +import { isIPv4, isIPv6, isPresent } from '@/utils'; +import $ from '@/core/app'; -// const targetPlatform = 'SurgeMac'; +const targetPlatform = 'SurgeMac'; const surge_Producer = Surge_Producer(); export default function SurgeMac_Producer() { const produce = (proxy) => { switch (proxy.type) { + case 'external': + return external(proxy); case 'ssr': return shadowsocksr(proxy); default: @@ -16,19 +20,67 @@ export default function SurgeMac_Producer() { }; return { produce }; } - -function shadowsocksr(proxy) { +function external(proxy) { const result = new Result(proxy); - - proxy.local_port = '__SubStoreLocalPort__'; - proxy.local_address = proxy.local_address ?? '127.0.0.1'; - + if (!proxy.exec || !proxy['local-port']) { + throw new Error(`${proxy.type}: exec and local-port are required`); + } result.append( - `${proxy.name} = external, exec = "${ - proxy.exec || '/usr/local/bin/ssr-local' - }", address = "${proxy.server}", local-port = ${proxy.local_port}`, + `${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`, ); + if (Array.isArray(proxy.args)) { + proxy.args.map((args) => { + result.append(`,args="${args}"`); + }); + } + if (Array.isArray(proxy.addresses)) { + proxy.addresses.map((addresses) => { + result.append(`,addresses=${addresses}`); + }); + } + + result.appendIfPresent( + `,no-error-alert=${proxy['no-error-alert']}`, + 'no-error-alert', + ); + + // tfo + if (isPresent(proxy, 'tfo')) { + result.append(`,tfo=${proxy['tfo']}`); + } else if (isPresent(proxy, 'fast-open')) { + result.append(`,tfo=${proxy['fast-open']}`); + } + + // test-url + result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); + + // block-quic + result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); + + return result.toString(); +} +function shadowsocksr(proxy) { + const external_proxy = { + ...proxy, + type: 'external', + exec: proxy.exec || '/usr/local/bin/ssr-local', + 'local-port': '__SubStoreLocalPort__', + args: [], + addresses: [], + 'local-address': + proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1', + }; + + // https://manual.nssurge.com/policy/external-proxy.html + if (isIP(proxy.server)) { + external_proxy.addresses.push(proxy.server); + } else { + $.log( + `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`, + ); + } + for (const [key, value] of Object.entries({ cipher: '-m', obfs: '-o', @@ -37,14 +89,16 @@ function shadowsocksr(proxy) { protocol: '-O', 'protocol-param': '-G', server: '-s', - local_port: '-l', - local_address: '-b', + 'local-port': '-l', + 'local-address': '-b', })) { - result.appendIfPresent( - `, args = "${value}", args = "${proxy[key]}"`, - key, - ); + external_proxy.args.push(value); + external_proxy.args.push(external_proxy[key]); } - return result.toString(); + return external(external_proxy); +} + +function isIP(ip) { + return isIPv4(ip) || isIPv6(ip); }