Expand Shadowsocks runtime compatibility

This commit is contained in:
JettChenT 2026-06-05 10:33:41 -07:00
parent d1638726ca
commit 4d1f589280
167 changed files with 57173 additions and 1640 deletions

View file

@ -183,15 +183,20 @@ private struct StoredProxySubscriptionNodeConfig: Decodable {
var network: StoredProxySubscriptionNetworkTransport?
var cipher: String?
var plugin: StoredJSONValue?
var smux: StoredJSONValue?
var udp: Bool?
var udpOverTCP: Bool?
var clientFingerprint: String?
var runtimeSupported: Bool {
switch type {
case "trojan":
return network?.isTrojanTCP ?? true
case "shadowsocks":
return plugin == nil
&& udpOverTCP != true
guard Self.supportedShadowsocksSmux(smux) else {
return false
}
return Self.supportedShadowsocksPlugin(plugin, clientFingerprint: clientFingerprint)
&& Self.supportedShadowsocksCiphers.contains(cipher?.lowercased() ?? "")
default:
return false
@ -203,18 +208,215 @@ private struct StoredProxySubscriptionNodeConfig: Decodable {
case network
case cipher
case plugin
case smux
case udp
case udpOverTCP = "udp_over_tcp"
case clientFingerprint = "client_fingerprint"
}
private static let supportedShadowsocksCiphers: Set<String> = [
"none",
"plain",
"table",
"rc4-md5",
"rc4",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"aes-128-cfb",
"aes-128-cfb1",
"aes-128-cfb8",
"aes-128-cfb128",
"aes-192-cfb",
"aes-192-cfb1",
"aes-192-cfb8",
"aes-192-cfb128",
"aes-256-cfb",
"aes-256-cfb1",
"aes-256-cfb8",
"aes-256-cfb128",
"aes-128-ofb",
"aes-192-ofb",
"aes-256-ofb",
"camellia-128-ctr",
"camellia-192-ctr",
"camellia-256-ctr",
"camellia-128-cfb",
"camellia-128-cfb1",
"camellia-128-cfb8",
"camellia-128-cfb128",
"camellia-192-cfb",
"camellia-192-cfb1",
"camellia-192-cfb8",
"camellia-192-cfb128",
"camellia-256-cfb",
"camellia-256-cfb1",
"camellia-256-cfb8",
"camellia-256-cfb128",
"camellia-128-ofb",
"camellia-192-ofb",
"camellia-256-ofb",
"chacha20",
"chacha20-ietf",
"xchacha20",
"aes-128-gcm",
"aead_aes_128_gcm",
"aes-192-gcm",
"aes-256-gcm",
"aead_aes_256_gcm",
"aes-128-ccm",
"aes-192-ccm",
"aes-256-ccm",
"aes-128-gcm-siv",
"aes-256-gcm-siv",
"chacha8-ietf-poly1305",
"chacha20-ietf-poly1305",
"chacha20-poly1305",
"aead_chacha20_poly1305",
"rabbit128-poly1305",
"xchacha8-ietf-poly1305",
"xchacha20-ietf-poly1305",
"sm4-gcm",
"sm4-ccm",
"aegis-128l",
"aegis-256",
"aez-384",
"deoxys-ii-256-128",
"ascon128",
"ascon128a",
"lea-128-gcm",
"lea-192-gcm",
"lea-256-gcm",
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-aes-128-ccm",
"2022-blake3-aes-256-ccm",
"2022-blake3-chacha20-poly1305",
"2022-blake3-chacha8-poly1305",
]
private static func supportedShadowsocksPlugin(
_ plugin: StoredJSONValue?,
clientFingerprint: String?
) -> Bool {
guard let plugin else {
return true
}
guard case .object(let object) = plugin,
case .string(let name)? = object["name"]
else {
return false
}
let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let opts: [String: StoredJSONValue]
if case .object(let decodedOpts)? = object["opts"] {
opts = decodedOpts
} else {
opts = [:]
}
if normalizedName == "restls",
let clientFingerprint,
Self.shadowsocksClientFingerprintIsActive(clientFingerprint)
{
return false
}
if normalizedName == "v2ray-plugin" || normalizedName == "gost-plugin" {
guard let mode = opts["mode"]?.stringValue ?? opts["obfs"]?.stringValue else {
return false
}
guard mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "websocket" else {
return false
}
return true
}
if normalizedName == "shadow-tls" {
let version = opts["version"]?.stringValue ?? "2"
let normalizedVersion = version.trimmingCharacters(in: .whitespacesAndNewlines)
switch normalizedVersion {
case "1":
return true
case "2", "3":
if let clientFingerprint,
Self.shadowsocksClientFingerprintIsActive(clientFingerprint)
{
return false
}
return true
default:
return false
}
}
if normalizedName == "kcptun" {
guard let smuxVersion = opts["smuxver"]?.stringValue else {
return true
}
guard let version = Double(smuxVersion.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return false
}
return version >= 0
}
if normalizedName == "restls" {
let versionHint = opts["version-hint"]?.stringValue ?? ""
switch versionHint.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "tls12", "tls13":
return true
default:
return false
}
}
guard normalizedName == "obfs" else {
return true
}
guard let mode = opts["mode"]?.stringValue ?? opts["obfs"]?.stringValue else {
return false
}
switch mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "http", "tls":
return true
default:
return false
}
}
private static func shadowsocksClientFingerprintIsActive(_ value: String) -> Bool {
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return [
"chrome",
"firefox",
"safari",
"ios",
"android",
"edge",
"360",
"qq",
"random",
"chrome120",
"firefox120",
"safari16",
"chrome_psk",
"chrome_psk_shuffle",
"chrome_padding_psk_shuffle",
"chrome_pq",
"chrome_pq_psk",
"randomized",
].contains(normalized)
}
private static func supportedShadowsocksSmux(_ smux: StoredJSONValue?) -> Bool {
guard case .object(let object)? = smux else {
return true
}
guard object["enabled"]?.boolValue ?? false else {
return true
}
let protocolName = object["protocol"]?.stringValue?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() ?? "h2mux"
guard protocolName == "h2mux" || protocolName == "smux" || protocolName == "yamux" else {
return false
}
return true
}
}
private enum StoredProxySubscriptionNetworkTransport: Decodable, Hashable {
@ -247,6 +449,42 @@ private enum StoredJSONValue: Decodable, Hashable {
case array([StoredJSONValue])
case null
var stringValue: String? {
switch self {
case .string(let value):
return value
case .number(let value):
if value.rounded(.towardZero) == value {
return String(Int(value))
}
return String(value)
case .bool(let value):
return String(value)
default:
return nil
}
}
var boolValue: Bool? {
switch self {
case .bool(let value):
return value
case .string(let value):
switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "1", "true", "yes", "y":
return true
case "0", "false", "no", "n":
return false
default:
return nil
}
case .number(let value):
return value != 0
default:
return nil
}
}
init(from decoder: Swift.Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {