mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-30 22:23:43 +00:00
fix(runtime): converge proxy runtime only when contract bytes change
Every catalog commit re-expanded the active target and ran the full deploy cycle unconditionally: the status face flashed back to starting, the launch health gate re-probed both local inbounds, and system proxy converged again — even when the derived contract was byte-identical to what was already deployed. Remark edits went further: remarks were part of SslocalServerDocument, so renaming the active server rewrote the contract and signalled sslocal for a change with no runtime effect. The reload predicate is now contract byte equality, shared by both layers (CONTEXT.md: a committed edit that leaves the runtime configuration file's bytes unchanged triggers no runtime convergence work). remarks leave the contract — upstream treats them as optional metadata and the GUI resolves display names from the catalog — so remark-only edits and no-op re-saves derive byte-identical documents. The catalogDidCommit deploy path skips write, signal, health gate and system proxy converge when ProxyRuntimePlan.actions returns empty (registered agent, live wrapper, on-disk contract equals the derived document); disk drift or a dead wrapper keeps the self-healing deploy. Catalog revalidation (candidate skips, target invalidation) still runs on every commit, and explicit intents (activation, switches, settings, launch resync) are untouched. The remark-edit runtime test — which encoded the old behavior as the example of a value change — is replaced by four catalog-convergence cases: remark-only edit and outside-subtree edit stay untouched, group member and active server value edits rewrite and signal. Also repairs the smoke suite, which failed on HEAD before this change: its ACL documents used a sslocal-active.acl path while RuntimeFileStore.write validates against acl-active.ini, so every ACL-bearing case threw before spawning the wrapper. Smoke ACL paths now bind to the store-validated location, and the suite passes 11/11 against real sslocal, confirming upstream accepts the contract without remarks.
This commit is contained in:
+2
-1
@@ -31,7 +31,7 @@
|
||||
- **Proxy runtime**: The per-user background host process, independent of the GUI's lifetime, that runs the external tunnel service and loads the optional generated ACL sidecar.
|
||||
- **Legacy configuration**: The server list and preferences persisted by the frozen implementation in `Legacy/`.
|
||||
- **Legacy import**: The user-confirmed, read-only translation of a Legacy configuration's server records into a separate manual group in 2.0. It migrates no preferences, activates no target, and neither starts proxying nor alters the Legacy source.
|
||||
- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target. Fields NG2 omits are left to the bundled `sslocal` defaults; the file is not the user-managed server configuration or subscription document.
|
||||
- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target. It carries only connection-effective values (endpoints, credentials, encryption, plugin, listeners, ACL); user-visible display names are catalog metadata and never enter the file. Fields NG2 omits are left to the bundled `sslocal` defaults; the file is not the user-managed server configuration or subscription document.
|
||||
- **Sensitive information**: Server passwords, plugin options (sensitive in their entirety), and nonempty user-provided remote URLs, including subscription URLs.
|
||||
- **Credential exposure boundary**: This product reduces accidental disclosure and exposure to other user accounts, but does not guarantee protection against a compromised same-user process, root access, or APFS snapshots and backups; it does not promise secure erasure.
|
||||
- **Credential reference**: A non-secret association from a server configuration, subscription, or remote-URL setting to its durable credential; the credential value is kept outside the configuration tree and resolved only when needed.
|
||||
@@ -60,6 +60,7 @@
|
||||
- Activating a server selects that one server. Activating a group recursively expands activation-candidate server leaves in explicit child order and passes them to the external proxy service; invalid leaves are omitted and surfaced, while a group with no activation candidate rejects activation atomically.
|
||||
- The active target remains the selected node ID and is not silently replaced by a descendant chosen by `sslocal`. If it is deleted, has no activation candidate, or is otherwise invalid, the target is cleared and proxying stops rather than falling back silently.
|
||||
- After a committed edit to the active target's subtree, the target is re-expanded immediately: a non-empty set of activation candidates updates the runtime atomically; no activation candidate clears the target and stops proxying without fallback.
|
||||
- A committed catalog edit that leaves the runtime configuration file's bytes unchanged — including edits outside the active target's subtree, remark-only changes, and no-op re-saves — triggers no runtime convergence work: no contract write, no reload signal, no restart of the external tunnel service, and no launch health re-probe. Catalog revalidation (activation-candidate skips, target invalidation) still happens on every commit. Within an active group subtree, a connection-value change — including member order — does change the runtime file and converges.
|
||||
- Manual nodes may move between the catalog root and manual groups, carrying their subtree and child order without changing identity or ownership; cross-source moves, shared parents, and cycles are rejected.
|
||||
- Manual catalog ownership is independent of proxy mode: removing the manual proxy mode does not remove manual groups, manual servers, or records imported into a manual group.
|
||||
- Empty groups may persist and remain editable, but cannot be activated. Deleting a server configuration or an empty manual group takes one user confirmation. Deleting a non-empty manual group recursively deletes its descendants and takes a second confirmation that names the subtree scale and that the subtree's server credentials are removed.
|
||||
|
||||
@@ -13,6 +13,12 @@ extension ProxyRuntimeController {
|
||||
guard settings.agentEnabled else {
|
||||
return .revalidated
|
||||
}
|
||||
if runtimeAlreadyConverged(with: configuration.document) {
|
||||
RuntimeLog.emit(.contractUnchanged)
|
||||
return Self.syncOutcome(
|
||||
agentState: state, systemProxyState: systemProxyState,
|
||||
skippedServers: configuration.skippedServers)
|
||||
}
|
||||
await deploy(configuration.document)
|
||||
return Self.syncOutcome(
|
||||
agentState: state, systemProxyState: systemProxyState,
|
||||
@@ -36,6 +42,35 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 目录提交去抖(CONTEXT.md「有效值未变的提交不做运行时收敛」):派生文档
|
||||
/// (含模式/ACL 合并)与磁盘契约逐字节相等、agent 已注册且 wrapper 存活、
|
||||
/// 控制器处于健康运行态时,本次提交对运行时无事可做——不写契约、不发信号、
|
||||
/// 不闪 starting、不重走健康门与系统代理收敛。判定复用计划层语义
|
||||
/// (`ProxyRuntimePlan.actions` 返回空 = 幂等跳过):磁盘漂移或 wrapper 失踪
|
||||
/// 时动作序列自然非空,走完整 deploy 自愈。目录重校验(跳过名单、目标失效
|
||||
/// 清除)已在 `reexpand` 完成,不受此门影响。
|
||||
private func runtimeAlreadyConverged(with sourceDocument: SslocalRuntimeDocument) -> Bool {
|
||||
switch state {
|
||||
case .running, .firewallBlocked:
|
||||
break
|
||||
case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed:
|
||||
return false
|
||||
}
|
||||
let document: SslocalRuntimeDocument
|
||||
do {
|
||||
document = try runtimeDocument(sourceDocument, for: proxyMode)
|
||||
} catch {
|
||||
// 规则快照缺失/损坏:交完整 deploy 如实呈现,不静默当作已收敛。
|
||||
return false
|
||||
}
|
||||
let actions = ProxyRuntimePlan.actions(
|
||||
intent: .run(document),
|
||||
agentStatus: agent.status,
|
||||
wrapper: wrapperState(),
|
||||
contractOnDisk: runtimeFileStore.readData())
|
||||
return actions.isEmpty
|
||||
}
|
||||
|
||||
/// 部署后的控制器状态 → 结构化收敛结果:健康通过(含防火墙受阻的运行态)
|
||||
/// 视为已收敛;系统代理写入失败携带点名细节,待应用不算目录收敛失败。
|
||||
private static func syncOutcome(
|
||||
|
||||
@@ -221,7 +221,6 @@ struct ActivationStateMachine: Equatable, Sendable {
|
||||
return .success(
|
||||
SslocalServerDocument(
|
||||
id: leafID.rawValue,
|
||||
remarks: fields.remark,
|
||||
server: fields.address.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
serverPort: fields.port,
|
||||
password: password,
|
||||
|
||||
@@ -164,11 +164,11 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable {
|
||||
}()
|
||||
}
|
||||
|
||||
/// 上游 `servers[]` 条目。id/remarks 携带叶子身份与显示名,供上游稳定标识;
|
||||
/// 上游 `servers[]` 条目。id 携带叶子身份,供上游稳定标识;显示名是目录
|
||||
/// 元数据,不进契约(CONTEXT.md「Runtime configuration file」)。
|
||||
/// 插件字段在无插件时整体省略(D10)。
|
||||
struct SslocalServerDocument: Codable, Equatable, Sendable {
|
||||
let id: String
|
||||
let remarks: String
|
||||
let server: String
|
||||
let serverPort: Int
|
||||
let password: String
|
||||
@@ -177,7 +177,7 @@ struct SslocalServerDocument: Codable, Equatable, Sendable {
|
||||
let pluginOpts: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, remarks, server
|
||||
case id, server
|
||||
case serverPort = "server_port"
|
||||
case password, method, plugin
|
||||
case pluginOpts = "plugin_opts"
|
||||
|
||||
@@ -30,7 +30,6 @@ final class ActivationStateMachineTests: XCTestCase {
|
||||
XCTAssertEqual(configuration.document.servers.count, 1)
|
||||
let entry = configuration.document.servers[0]
|
||||
XCTAssertEqual(entry.id, server.rawValue)
|
||||
XCTAssertEqual(entry.remarks, "香港 01")
|
||||
XCTAssertEqual(entry.server, "203.0.113.7")
|
||||
XCTAssertEqual(entry.serverPort, 8388)
|
||||
XCTAssertEqual(entry.method, "aes-256-gcm")
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import XCTest
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
|
||||
/// 目录提交 → 运行时收敛语义(D3/D5 与 CONTEXT.md「有效值未变的提交不做
|
||||
/// 运行时收敛」):与主测试类同构扩展,按既有模式独立成文件。
|
||||
extension ProxyRuntimeControllerTests {
|
||||
// MARK: 目录重展开消费(D3/D5)
|
||||
|
||||
/// 模拟 wrapper 在跑(pid 指向本测试进程,kill(pid, 0) 判活通过)。
|
||||
private func simulateRunningWrapper() throws {
|
||||
try Data("\(ProcessInfo.processInfo.processIdentifier)".utf8).write(to: runtime.pidFile)
|
||||
}
|
||||
|
||||
private func committedCatalog() throws -> ConfigurationCatalog {
|
||||
try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
}
|
||||
|
||||
private func replacedFields(
|
||||
_ fields: ServerFields, address: String? = nil, remark: String? = nil
|
||||
) -> ServerFields {
|
||||
ServerFields(
|
||||
address: address ?? fields.address,
|
||||
port: fields.port,
|
||||
encryptionMethod: fields.encryptionMethod,
|
||||
passwordRef: fields.passwordRef,
|
||||
remark: remark ?? fields.remark,
|
||||
pluginProgram: fields.pluginProgram,
|
||||
pluginOptionsRef: fields.pluginOptionsRef)
|
||||
}
|
||||
|
||||
func testCatalogValueEditOfActiveTargetRewritesAndSignalsRunningWrapper() async throws {
|
||||
let seeded = try makeSeededCatalog()
|
||||
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||||
try await controller.activate(seeded.server)
|
||||
await controller.setAgentEnabled(true)
|
||||
try simulateRunningWrapper()
|
||||
|
||||
// 修改激活服务器的连接地址(契约有效值变化),提交目录变更。
|
||||
var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
let fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog)
|
||||
try catalog.updateServer(
|
||||
seeded.server, with: replacedFields(fields, address: "203.0.113.9"))
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
||||
|
||||
// 协调器生产适配入口(issue #40):以刚提交的内存快照重展开。
|
||||
_ = await controller.catalogDidCommit(snapshot: try committedCatalog())
|
||||
|
||||
XCTAssertEqual(controller.state, .running)
|
||||
let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument())
|
||||
XCTAssertEqual(onDisk.servers.first?.server, "203.0.113.9", "运行时文档已原子更新")
|
||||
let signalsSent = signals.signalsSent.filter { $0.signal != 0 }
|
||||
XCTAssertEqual(signalsSent.count, 1, "代理运行中:重写后应发一次 SIGUSR1")
|
||||
XCTAssertEqual(signalsSent.first?.signal, SIGUSR1)
|
||||
}
|
||||
|
||||
func testCatalogRemarkOnlyEditOfActiveTargetKeepsRuntimeUntouched() async throws {
|
||||
let seeded = try makeSeededCatalog()
|
||||
let probe = ProxyRuntimeFixture.FakeProbe.reachable()
|
||||
let controller = makeController(probe: probe)
|
||||
try await controller.activate(seeded.server)
|
||||
await controller.setAgentEnabled(true)
|
||||
try simulateRunningWrapper()
|
||||
let contractDataBefore = try Data(contentsOf: runtime.contract)
|
||||
|
||||
// 修改激活服务器的备注:显示名是目录元数据,不进契约 → 零收敛
|
||||
// (不写盘、不发信号、不闪 starting、不重走健康门)。
|
||||
var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
let fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog)
|
||||
try catalog.updateServer(
|
||||
seeded.server, with: replacedFields(fields, remark: "新加坡 01"))
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
||||
|
||||
let outcome = await controller.catalogDidCommit(snapshot: try committedCatalog())
|
||||
|
||||
XCTAssertEqual(outcome, .converged(skippedServers: []))
|
||||
XCTAssertEqual(controller.state, .running, "状态面原地不动,不闪回 starting")
|
||||
XCTAssertEqual(try Data(contentsOf: runtime.contract), contractDataBefore, "契约逐字节未变")
|
||||
XCTAssertEqual(
|
||||
signals.signalsSent.filter { $0.signal != 0 }.count, 0, "不向 wrapper 发信号")
|
||||
XCTAssertEqual(probe.callCount, 2, "不重走健康门(初始部署探测过 SOCKS+HTTP 各一次)")
|
||||
}
|
||||
|
||||
func testCatalogValueEditOutsideActiveSubtreeKeepsRuntimeUntouched() async throws {
|
||||
var catalog = ConfigurationCatalog()
|
||||
let activeServer = try ActivationFixture.addPlainServer(
|
||||
"香港 01", in: &catalog, credentials: credentials)
|
||||
let idleServer = try ActivationFixture.addPlainServer(
|
||||
"备用 02", in: &catalog, credentials: credentials)
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
||||
let probe = ProxyRuntimeFixture.FakeProbe.reachable()
|
||||
let controller = makeController(probe: probe)
|
||||
try await controller.activate(activeServer)
|
||||
await controller.setAgentEnabled(true)
|
||||
try simulateRunningWrapper()
|
||||
let contractDataBefore = try Data(contentsOf: runtime.contract)
|
||||
|
||||
// 修改目标子树之外服务器的连接地址:派生契约不变 → 零收敛。
|
||||
var working = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
let fields = try ActivationFixture.serverFields(of: idleServer, in: working)
|
||||
try working.updateServer(idleServer, with: replacedFields(fields, address: "198.51.100.9"))
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: working))
|
||||
|
||||
let outcome = await controller.catalogDidCommit(snapshot: try committedCatalog())
|
||||
|
||||
XCTAssertEqual(outcome, .converged(skippedServers: []))
|
||||
XCTAssertEqual(controller.state, .running, "状态面原地不动,不闪回 starting")
|
||||
XCTAssertEqual(try Data(contentsOf: runtime.contract), contractDataBefore, "契约逐字节未变")
|
||||
XCTAssertEqual(signals.signalsSent.filter { $0.signal != 0 }.count, 0, "不向 wrapper 发信号")
|
||||
XCTAssertEqual(probe.callCount, 2, "不重走健康门")
|
||||
}
|
||||
|
||||
func testCatalogGroupMemberValueEditRewritesAndSignalsRunningWrapper() async throws {
|
||||
var catalog = ConfigurationCatalog()
|
||||
let group = try catalog.addGroup("分组")
|
||||
let member = try ActivationFixture.addPlainServer(
|
||||
"香港 01", to: group, in: &catalog, credentials: credentials)
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
||||
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||||
try await controller.activate(group)
|
||||
await controller.setAgentEnabled(true)
|
||||
try simulateRunningWrapper()
|
||||
|
||||
// 激活目标是分组:成员连接值变化是活动子树的真实值变化 → 正常收敛。
|
||||
var working = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
let fields = try ActivationFixture.serverFields(of: member, in: working)
|
||||
try working.updateServer(member, with: replacedFields(fields, address: "203.0.113.9"))
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: working))
|
||||
|
||||
_ = await controller.catalogDidCommit(snapshot: try committedCatalog())
|
||||
|
||||
XCTAssertEqual(controller.state, .running)
|
||||
let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument())
|
||||
XCTAssertEqual(onDisk.servers.first?.server, "203.0.113.9", "成员值变化进入运行时文档")
|
||||
XCTAssertEqual(signals.signalsSent.filter { $0.signal != 0 }.count, 1, "转发一次 SIGUSR1")
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ final class ProxyRuntimeControllerTests: XCTestCase {
|
||||
var runtime: ProxyRuntimeFixture.TemporaryRuntime!
|
||||
var catalogFileURL: URL!
|
||||
var activationFileURL: URL!
|
||||
private var credentials: InMemoryCredentialStore!
|
||||
var credentials: InMemoryCredentialStore!
|
||||
var agent: ProxyRuntimeFixture.FakeLaunchAgent!
|
||||
var systemProxy: ProxyRuntimeFixture.FakeSystemProxy!
|
||||
var signals: SignalRecorder!
|
||||
@@ -267,40 +267,4 @@ final class ProxyRuntimeControllerTests: XCTestCase {
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: runtime.pidFile.path))
|
||||
}
|
||||
|
||||
// MARK: 目录重展开消费(D3/D5)
|
||||
|
||||
func testCatalogEditOfActiveTargetRewritesAndSignalsRunningWrapper() async throws {
|
||||
let seeded = try makeSeededCatalog()
|
||||
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||||
try await controller.activate(seeded.server)
|
||||
await controller.setAgentEnabled(true)
|
||||
// 模拟 wrapper 在跑:pid 指向本测试进程(kill(pid, 0) 判活通过)。
|
||||
try Data("\(ProcessInfo.processInfo.processIdentifier)".utf8).write(to: runtime.pidFile)
|
||||
|
||||
// 修改服务器备注(文档内容随之变化),提交目录变更。
|
||||
var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
var fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog)
|
||||
fields = ServerFields(
|
||||
address: fields.address,
|
||||
port: fields.port,
|
||||
encryptionMethod: fields.encryptionMethod,
|
||||
passwordRef: fields.passwordRef,
|
||||
remark: "新加坡 01",
|
||||
pluginProgram: fields.pluginProgram,
|
||||
pluginOptionsRef: fields.pluginOptionsRef)
|
||||
try catalog.updateServer(seeded.server, with: fields)
|
||||
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
||||
|
||||
// 协调器生产适配入口(issue #40):以刚提交的内存快照重展开。
|
||||
let committed = try CatalogFileStore(fileURL: catalogFileURL).load().catalog
|
||||
_ = await controller.catalogDidCommit(snapshot: committed)
|
||||
|
||||
XCTAssertEqual(controller.state, .running)
|
||||
let expectedRemark = "新加坡 01"
|
||||
let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument())
|
||||
XCTAssertEqual(onDisk.servers.first?.remarks, expectedRemark, "运行时文档已原子更新")
|
||||
let signalsSent = signals.signalsSent.filter { $0.signal != 0 }
|
||||
XCTAssertEqual(signalsSent.count, 1, "代理运行中:重写后应发一次 SIGUSR1")
|
||||
XCTAssertEqual(signalsSent.first?.signal, SIGUSR1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,6 @@ enum ProxyRuntimeFixture {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "server-1",
|
||||
remarks: "香港 01",
|
||||
server: serverAddress,
|
||||
serverPort: 8388,
|
||||
password: password,
|
||||
|
||||
@@ -19,7 +19,6 @@ final class RuntimeDocumentTests: XCTestCase {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "server-1",
|
||||
remarks: "香港 01",
|
||||
server: "203.0.113.7",
|
||||
serverPort: 8388,
|
||||
password: "resolved-password",
|
||||
@@ -56,7 +55,8 @@ final class RuntimeDocumentTests: XCTestCase {
|
||||
XCTAssertEqual(servers.count, 1)
|
||||
let server = servers[0]
|
||||
XCTAssertEqual(server["id"] as? String, "server-1", "叶子身份进入文档,供诊断与稳定标识")
|
||||
XCTAssertEqual(server["remarks"] as? String, "香港 01")
|
||||
XCTAssertFalse(
|
||||
server.keys.contains("remarks"), "显示名是目录元数据,不进契约(只承载连接有效值)")
|
||||
XCTAssertEqual(server["server"] as? String, "203.0.113.7")
|
||||
XCTAssertEqual(server["server_port"] as? Int, 8388)
|
||||
XCTAssertEqual(server["password"] as? String, "resolved-password")
|
||||
@@ -92,7 +92,7 @@ final class RuntimeDocumentTests: XCTestCase {
|
||||
let document = SslocalRuntimeDocument(
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "server-1", remarks: "test", server: "example.com", serverPort: 8388,
|
||||
id: "server-1", server: "example.com", serverPort: 8388,
|
||||
password: "pw", method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
|
||||
],
|
||||
listen: SslocalListenSettings())
|
||||
|
||||
@@ -276,7 +276,7 @@ final class RuntimeFileStoreTests: XCTestCase {
|
||||
method: String = "aes-256-gcm"
|
||||
) -> SslocalServerDocument {
|
||||
SslocalServerDocument(
|
||||
id: id, remarks: "", server: address, serverPort: port, password: "pw",
|
||||
id: id, server: address, serverPort: port, password: "pw",
|
||||
method: method, plugin: nil, pluginOpts: nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ extension RealSslocalSmokeTests {
|
||||
let document = SslocalRuntimeDocument(
|
||||
servers: [],
|
||||
listen: listen,
|
||||
acl: .direct(at: workDir.appendingPathComponent("sslocal-active.acl")))
|
||||
acl: .direct(at: aclFileURL))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
let wrapper = try launchWrapper(document)
|
||||
defer {
|
||||
@@ -60,7 +60,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "global-smoke-server",
|
||||
remarks: "global-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -69,7 +68,7 @@ extension RealSslocalSmokeTests {
|
||||
pluginOpts: nil)
|
||||
],
|
||||
listen: listen,
|
||||
acl: .global(at: workDir.appendingPathComponent("sslocal-active.acl")))
|
||||
acl: .global(at: aclFileURL))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
let wrapper = try launchWrapper(document)
|
||||
defer {
|
||||
@@ -108,7 +107,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "rule-smoke-server",
|
||||
remarks: "rule-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -118,7 +116,7 @@ extension RealSslocalSmokeTests {
|
||||
],
|
||||
listen: listen,
|
||||
acl: .rule(
|
||||
at: workDir.appendingPathComponent("sslocal-active.acl"),
|
||||
at: aclFileURL,
|
||||
defaultAction: .proxyWhenUnmatched,
|
||||
rules: chinaRules))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
@@ -150,7 +148,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "cidr-smoke-server",
|
||||
remarks: "cidr-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -160,7 +157,7 @@ extension RealSslocalSmokeTests {
|
||||
],
|
||||
listen: listen,
|
||||
acl: .rule(
|
||||
at: workDir.appendingPathComponent("sslocal-active.acl"),
|
||||
at: aclFileURL,
|
||||
defaultAction: .proxyWhenUnmatched,
|
||||
rules: try chinaCIDRSmokeRules()))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
|
||||
@@ -29,7 +29,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "custom-smoke-server",
|
||||
remarks: "custom-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -39,7 +38,7 @@ extension RealSslocalSmokeTests {
|
||||
],
|
||||
listen: listen,
|
||||
acl: .rule(
|
||||
at: workDir.appendingPathComponent("sslocal-active.acl"),
|
||||
at: aclFileURL,
|
||||
defaultAction: .proxyWhenUnmatched,
|
||||
rules: customRules))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
@@ -127,7 +126,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "custom-direct-smoke-server",
|
||||
remarks: "custom-direct-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -137,7 +135,7 @@ extension RealSslocalSmokeTests {
|
||||
],
|
||||
listen: listen,
|
||||
acl: .rule(
|
||||
at: workDir.appendingPathComponent("sslocal-active.acl"),
|
||||
at: aclFileURL,
|
||||
defaultAction: .directWhenUnmatched,
|
||||
rules: customRules))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
|
||||
@@ -26,7 +26,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "gfw-smoke-server",
|
||||
remarks: "gfw-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -36,7 +35,7 @@ extension RealSslocalSmokeTests {
|
||||
],
|
||||
listen: listen,
|
||||
acl: .rule(
|
||||
at: workDir.appendingPathComponent("sslocal-active.acl"),
|
||||
at: aclFileURL,
|
||||
defaultAction: .directWhenUnmatched,
|
||||
rules: gfwRules))
|
||||
XCTAssertTrue(document.isWellFormed)
|
||||
@@ -65,7 +64,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "priority-smoke-server",
|
||||
remarks: "priority-smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: fakeSSServer.port,
|
||||
password: "smoke-password",
|
||||
@@ -117,7 +115,7 @@ extension RealSslocalSmokeTests {
|
||||
8.8.8.0/24
|
||||
"""
|
||||
let acl = ProxyACLDocument(
|
||||
path: workDir.appendingPathComponent("sslocal-active.acl").standardizedFileURL.path,
|
||||
path: aclFileURL.standardizedFileURL.path,
|
||||
summary: "priority-probe",
|
||||
content: aclContent)
|
||||
XCTAssertTrue(acl.isWellFormed)
|
||||
|
||||
@@ -27,7 +27,6 @@ extension RealSslocalSmokeTests {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "smoke-plugin-server",
|
||||
remarks: "smoke-plugin",
|
||||
server: "127.0.0.1",
|
||||
serverPort: 1, // 远端必然拒绝:只验证本地链路与插件进程边界
|
||||
password: "smoke-password",
|
||||
|
||||
@@ -24,6 +24,12 @@ final class RealSslocalSmokeTests: XCTestCase {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
/// `RuntimeFileStore.write` 校验的 ACL sidecar 路径:ACL 文档的 `path` 必须
|
||||
/// 严格等于它,否则契约写入即抛「ACL sidecar path or digest is invalid」。
|
||||
var aclFileURL: URL {
|
||||
RuntimeFileStore(fileURL: contractURL).aclFileURL
|
||||
}
|
||||
|
||||
private var sslocalURL: URL {
|
||||
get throws {
|
||||
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/sslocal")
|
||||
@@ -144,7 +150,6 @@ final class RealSslocalSmokeTests: XCTestCase {
|
||||
servers: [
|
||||
SslocalServerDocument(
|
||||
id: "smoke-server",
|
||||
remarks: "smoke",
|
||||
server: "127.0.0.1",
|
||||
serverPort: 1, // 远端必然拒绝:只验证本地握手与 SSLOCAL 配置格式
|
||||
password: "smoke-password",
|
||||
|
||||
Reference in New Issue
Block a user