mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-03 17:43:44 +00:00
refactor(runtime): make deployment metadata references explicit
This commit is contained in:
@@ -74,10 +74,11 @@ struct RuntimeFileStore {
|
||||
let intent = PublishedRuntimeIntent(
|
||||
sha256: contract.sha256, configurationSHA256: ProxyACLDocument.digest(configurationData),
|
||||
summary: Redactor.documentSummary(contract.document))
|
||||
_ = try create(JSONEncoder().encode(intent), kind: "config", suffix: "intent.json")
|
||||
let intentMetadata = try create(
|
||||
JSONEncoder().encode(intent), kind: "config", suffix: "intent.json")
|
||||
let snapshot = RuntimeSupervision(
|
||||
version: 1, generation: generation, deploymentID: UUID().uuidString,
|
||||
configuration: configuration,
|
||||
version: 2, generation: generation, deploymentID: UUID().uuidString,
|
||||
configuration: configuration, intentMetadata: intentMetadata,
|
||||
locals: encoded.locals.map {
|
||||
.init(
|
||||
inboundProtocol: $0.inboundProtocol, localAddress: $0.localAddress,
|
||||
@@ -194,7 +195,8 @@ struct RuntimeFileStore {
|
||||
}
|
||||
|
||||
private func publishedIntent(_ snapshot: RuntimeSupervision) -> PublishedRuntimeIntent? {
|
||||
guard let data = try? Data(contentsOf: directoryURL.appendingPathComponent(snapshot.intentFile))
|
||||
guard
|
||||
let data = try? Data(contentsOf: directoryURL.appendingPathComponent(snapshot.intentMetadata))
|
||||
else { return nil }
|
||||
return try? JSONDecoder().decode(PublishedRuntimeIntent.self, from: data)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ struct RuntimeSupervision: Codable, Equatable, Sendable {
|
||||
let generation: UInt64
|
||||
let deploymentID: String
|
||||
let configuration: String
|
||||
/// GUI 私有意图摘要的同代文件;Agent 只保留引用,不读取正文。
|
||||
let intentMetadata: String
|
||||
let locals: [Endpoint]
|
||||
let ipv6Only: Bool?
|
||||
let authenticationID: String?
|
||||
@@ -35,13 +37,8 @@ struct RuntimeSupervision: Codable, Equatable, Sendable {
|
||||
let acl: ACL?
|
||||
let aclVariants: [String: ACL]
|
||||
|
||||
/// GUI 私有意图摘要的同代文件;Agent 只保留引用,不读取正文。
|
||||
var intentFile: String {
|
||||
configuration.replacingOccurrences(of: "-sslocal.json", with: "-intent.json")
|
||||
}
|
||||
|
||||
var retainedFiles: Set<String> {
|
||||
Set([configuration, intentFile] + authenticationFiles + aclVariants.values.map(\.file))
|
||||
Set([configuration, intentMetadata] + authenticationFiles + aclVariants.values.map(\.file))
|
||||
.union(acl.map { [$0.file] } ?? [])
|
||||
}
|
||||
|
||||
@@ -51,9 +48,11 @@ struct RuntimeSupervision: Codable, Equatable, Sendable {
|
||||
}
|
||||
|
||||
var hasValidStructure: Bool {
|
||||
guard version == 1, generation > 0, UUID(uuidString: deploymentID) != nil,
|
||||
configuration.hasPrefix("config-"), configuration.hasSuffix("-sslocal.json"),
|
||||
guard version == 2, generation > 0, UUID(uuidString: deploymentID) != nil,
|
||||
configuration.hasPrefix("config-"), intentMetadata.hasPrefix("config-"),
|
||||
RuntimePublication.generation(of: configuration) == generation,
|
||||
RuntimePublication.generation(of: intentMetadata) == generation,
|
||||
configuration != intentMetadata,
|
||||
authenticationFiles.allSatisfy({ $0.hasPrefix("auth-") && $0.hasSuffix(".json") }),
|
||||
aclVariants.values.allSatisfy({ $0.file.hasPrefix("acl-") && $0.file.hasSuffix(".ini") }),
|
||||
authenticationID.map({ UUID(uuidString: $0) != nil }) ?? true,
|
||||
|
||||
@@ -97,7 +97,7 @@ struct RuntimeConvergenceTests {
|
||||
let name =
|
||||
resource == "configuration"
|
||||
? snapshot.configuration
|
||||
: resource == "intent" ? snapshot.intentFile : try #require(snapshot.acl?.file)
|
||||
: resource == "intent" ? snapshot.intentMetadata : try #require(snapshot.acl?.file)
|
||||
try FileManager.default.removeItem(at: fixture.runtime.directory.appendingPathComponent(name))
|
||||
let count = fixture.agent.registerCount
|
||||
let restarted = fixture.reopeningController()
|
||||
|
||||
@@ -18,6 +18,99 @@ struct RuntimePublicationTests {
|
||||
}
|
||||
|
||||
extension RuntimePublicationTests {
|
||||
@Test func opaqueConfigurationUsesExplicitMetadataThroughSelectionAndRetirement() throws {
|
||||
let runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
|
||||
defer { try? FileManager.default.removeItem(at: runtime.directory) }
|
||||
let store = RuntimeFileStore(fileURL: runtime.contract)
|
||||
let document = ProxyRuntimeFixture.makeDocument()
|
||||
try store.write(document)
|
||||
let original = try #require(RuntimePublication.load(at: store.supervisionFileURL))
|
||||
let bytes = try Data(contentsOf: store.supervisionFileURL)
|
||||
var object = try #require(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
|
||||
let prefix = String(format: "config-%020llu-", original.generation)
|
||||
let configuration = prefix + "payload.bin"
|
||||
let metadata = prefix + "gui-metadata.dat"
|
||||
try FileManager.default.moveItem(
|
||||
at: runtime.directory.appendingPathComponent(original.intentMetadata),
|
||||
to: runtime.directory.appendingPathComponent(metadata))
|
||||
let payload = Data([0x00, 0xff, 0x80, 0x01])
|
||||
try AtomicFileWriter.write(payload, to: runtime.directory.appendingPathComponent(configuration))
|
||||
object["configuration"] = configuration
|
||||
object["intentMetadata"] = metadata
|
||||
try AtomicFileWriter.write(
|
||||
JSONSerialization.data(withJSONObject: object), to: store.supervisionFileURL)
|
||||
|
||||
let selected = try #require(RuntimePublication.load(at: store.supervisionFileURL))
|
||||
#expect(store.publishedSummary()?.contains("servers=1") == true)
|
||||
try RuntimePublication.select(
|
||||
selected, configurationLink: runtime.contract, aclLink: store.aclFileURL)
|
||||
#expect(try Data(contentsOf: runtime.contract) == payload)
|
||||
try store.write(document)
|
||||
let next = try #require(RuntimePublication.load(at: store.supervisionFileURL))
|
||||
try RuntimePublication.retire(
|
||||
before: next, publishedAt: store.supervisionFileURL, protecting: selected.retainedFiles)
|
||||
for name in [configuration, metadata] {
|
||||
#expect(
|
||||
FileManager.default.fileExists(atPath: runtime.directory.appendingPathComponent(name).path))
|
||||
}
|
||||
try RuntimePublication.select(
|
||||
next, configurationLink: runtime.contract, aclLink: store.aclFileURL)
|
||||
try RuntimePublication.retire(
|
||||
before: next, publishedAt: store.supervisionFileURL, protecting: [])
|
||||
for name in [configuration, metadata] {
|
||||
#expect(
|
||||
!FileManager.default.fileExists(atPath: runtime.directory.appendingPathComponent(name).path)
|
||||
)
|
||||
}
|
||||
#expect(try store.contains(PreparedRuntimeContract(document)))
|
||||
}
|
||||
|
||||
@Test func metadataReferenceIsRequiredAndMustBeDistinctAndFromTheSameGeneration() throws {
|
||||
let runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
|
||||
defer { try? FileManager.default.removeItem(at: runtime.directory) }
|
||||
let store = RuntimeFileStore(fileURL: runtime.contract)
|
||||
try store.write(ProxyRuntimeFixture.makeDocument())
|
||||
let snapshot = try #require(RuntimePublication.load(at: store.supervisionFileURL))
|
||||
let bytes = try Data(contentsOf: store.supervisionFileURL)
|
||||
let invalidReferences: [String?] = [
|
||||
nil, "../outside.json", snapshot.configuration,
|
||||
String(format: "config-%020llu-intent.json", snapshot.generation - 1),
|
||||
String(format: "config-%020llu-intent.json", snapshot.generation + 1),
|
||||
String(format: "auth-%020llu-intent.json", snapshot.generation),
|
||||
]
|
||||
for reference in invalidReferences {
|
||||
var object = try #require(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
|
||||
object["intentMetadata"] = reference
|
||||
try AtomicFileWriter.write(
|
||||
JSONSerialization.data(withJSONObject: object), to: store.supervisionFileURL)
|
||||
#expect(RuntimePublication.metadata(at: store.supervisionFileURL) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func missingOrSymlinkedMetadataPreventsDeploymentSelection() throws {
|
||||
let runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
|
||||
defer { try? FileManager.default.removeItem(at: runtime.directory) }
|
||||
let store = RuntimeFileStore(fileURL: runtime.contract)
|
||||
try store.write(ProxyRuntimeFixture.makeDocument())
|
||||
let snapshot = try #require(RuntimePublication.load(at: store.supervisionFileURL))
|
||||
let metadataURL = runtime.directory.appendingPathComponent(snapshot.intentMetadata)
|
||||
try FileManager.default.removeItem(at: metadataURL)
|
||||
#expect(RuntimePublication.load(at: store.supervisionFileURL) == nil)
|
||||
#expect(throws: (any Error).self) {
|
||||
try RuntimePublication.select(
|
||||
snapshot, configurationLink: runtime.contract, aclLink: store.aclFileURL)
|
||||
}
|
||||
try FileManager.default.createSymbolicLink(
|
||||
at: metadataURL,
|
||||
withDestinationURL: runtime.directory.appendingPathComponent(snapshot.configuration))
|
||||
#expect(RuntimePublication.load(at: store.supervisionFileURL) == nil)
|
||||
#expect(throws: (any Error).self) {
|
||||
try RuntimePublication.select(
|
||||
snapshot, configurationLink: runtime.contract, aclLink: store.aclFileURL)
|
||||
}
|
||||
#expect(!FileManager.default.fileExists(atPath: runtime.contract.path))
|
||||
}
|
||||
|
||||
@Test func routingIntentChangesEvenWhenUpstreamJSONDoesNot() throws {
|
||||
let runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
|
||||
defer { try? FileManager.default.removeItem(at: runtime.directory) }
|
||||
@@ -123,7 +216,9 @@ extension RuntimePublicationTests {
|
||||
let store = RuntimeFileStore(fileURL: runtime.contract)
|
||||
try store.write(ProxyRuntimeFixture.makeDocument())
|
||||
let bytes = try Data(contentsOf: store.supervisionFileURL)
|
||||
for (key, value) in [("version", 99 as Any), ("configuration", "../outside.json" as Any)] {
|
||||
for (key, value) in [
|
||||
("version", 1 as Any), ("version", 99 as Any), ("configuration", "../outside.json" as Any),
|
||||
] {
|
||||
var object = try #require(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
|
||||
object[key] = value
|
||||
try AtomicFileWriter.write(
|
||||
|
||||
@@ -44,7 +44,7 @@ extension RuntimeFileStore {
|
||||
let data = try? Data(contentsOf: directoryURL.appendingPathComponent(snapshot.configuration)),
|
||||
let configuration = try? JSONDecoder().decode(SslocalConfigurationDocument.self, from: data),
|
||||
let intentData = try? Data(
|
||||
contentsOf: directoryURL.appendingPathComponent(snapshot.intentFile)),
|
||||
contentsOf: directoryURL.appendingPathComponent(snapshot.intentMetadata)),
|
||||
let intent = try? JSONDecoder().decode(PublishedRuntimeIntent.self, from: intentData)
|
||||
else { return nil }
|
||||
return PublishedRuntimeConfiguration(
|
||||
|
||||
@@ -30,6 +30,10 @@ GUI 不得重新引用退出保留集合的旧文件;准备文件必须先获
|
||||
|
||||
配置、认证和 ACL 文件名均包含 20 位十进制代次。认证身份使用随机 UUID;只在凭据变化时换身份和文件,不公开凭据摘要。GUI 为每份配置准备同代 `config-<generation>-intent.json`,保存 GUI 私有意图摘要、最终配置字节摘要与脱敏汇总,用于配置损坏后仍能识别失败意图并独立提供诊断事实;Agent 只保留该引用,不读取正文。该 GUI 私有摘要不进入监管资料、回执或诊断。
|
||||
|
||||
监管资料格式第 2 版以必填的 `configuration` 和 `intentMetadata` 分别记录配置与 GUI 私有元数据的具体文件引用,不通过配置文件名推导元数据位置。两个引用必须不同,均以 `config-` 开头并属于本次部署代次;监管校验不限定这两个文件的格式后缀。当前发布端继续生成 `sslocal.json`、`intent.json` 后缀,稳定链接仍为 `sslocal-active.json`。认证与 ACL 的命名和复用规则保持不变。元数据与配置同样必须是已存在的普通文件,缺失时整份部署不可用。发布与回收只依赖显式引用、受管文件名和代次,不解析配置正文。
|
||||
|
||||
本次格式变更不提供旧监管资料兼容、迁移或残留处理;开发环境切换前已停止后台进程并清理旧运行产物。未实现密文输入、密钥交接或通用资源框架。
|
||||
|
||||
`sslocal.pid` 保留尚未确认退出的子进程身份;Agent 崩溃后若该 PID 仍存在,则拒绝启动第二个实例并报告失败。无法确认退出的进程和保守 PID 复用阻断需要用户检查并显式重试。没有可靠活跃准备者证据时,崩溃遗留的未来代文件暂不回收。
|
||||
|
||||
## 失败与验证
|
||||
|
||||
@@ -10,4 +10,4 @@ GUI 的内存运行意图与 sslocal JSON 输出分离。上游配置只包含
|
||||
|
||||
LAN 认证仍写入现有受保护的明文认证文件。发布 implementation 比较本次内存生成的字节与当前监管资料引用的认证文件,相同则复用文件与随机认证身份,不同或不可读则重新准备。此比较不读取 sslocal 配置,也不增加凭据摘要、版本状态或完整秘密快照。未来认证文件是否加密仍开放;若编码形态变化,在发布 implementation 内调整比较方式,不提前引入通用加密框架。
|
||||
|
||||
本决定补充 ADR-0038,沿用其代次、锁、不可变发布、稳定链接、ACL 复用与清理规则,以及 ADR-0037 的失败处理。本次不实施加密、密钥交接、sslocal 应用确认协议或文件引用格式改造。当前回执仍不表示 sslocal 已确认应用重载。
|
||||
本决定补充 ADR-0038,沿用其代次、锁、不可变发布、稳定链接、ACL 复用与清理规则,以及 ADR-0037 的失败处理。本次不实施加密、密钥交接或 sslocal 应用确认协议。后续文件引用格式改造见 ADR-0038 的第 2 版监管资料:配置与私有元数据采用显式引用,监管校验不依赖配置格式后缀。当前回执仍不表示 sslocal 已确认应用重载。
|
||||
|
||||
Reference in New Issue
Block a user