Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4101,6 +4101,7 @@ enum ConfigurationEditorCopy {
case .general: "General Configuration (Configuration Chapter)"
case .dns: "DNS"
case .sniffer: "Domain Sniffing (Configuration Chapter)"
case .mitm: "MITM Interception (Configuration Chapter)"
case .inbound: "Inbounds (Configuration Chapter)"
case .proxies: "Proxies (Configuration Chapter)"
case .proxyProviders: "Proxy Providers (Configuration Chapter)"
Expand Down Expand Up @@ -4131,6 +4132,7 @@ enum ConfigurationEditorCopy {
switch section {
case .dns: "dns"
case .sniffer: "sniffer"
case .mitm: "mitm"
default: section.rawValue
}
}
Expand All @@ -4142,10 +4144,13 @@ enum ConfigurationEditorCopy {



static func value(of row: ConfigurationOutlineRow, snifferEnabled: Bool? = nil) -> ClashDisplayText? {
static func value(of row: ConfigurationOutlineRow, snifferEnabled: Bool? = nil,
mitmEnabled: Bool? = nil) -> ClashDisplayText? {
switch row.section {
case .sniffer:
return snifferEnabled.map { .copy($0 ? "On" : "Off") }
case .mitm:
return mitmEnabled.map { .copy($0 ? "On" : "Off") }
case .overrideScript:
return nil
default:
Expand All @@ -4170,6 +4175,14 @@ enum ConfigurationEditorCopy {
}
}

@MainActor static func mitmEnabled(in session: ConfigurationEditorSession) -> Bool? {
switch session.settingsValue(at: ["mitm", "enable"]) {
case .scalar(let token)?: return token == "true" ? true : token == "false" ? false : nil
case .string(let text)?: return text == "true" ? true : text == "false" ? false : nil
default: return nil
}
}


static func summary(of value: OrderedJSON) -> String {
switch value {
Expand Down
6 changes: 4 additions & 2 deletions apple/ClashClient/Sources/ConfigUI/ConfigurationOnePage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,9 @@ struct ConfigurationPageSections: View {
valueRow(ConfigurationEditorCopy.title(of: row.section),
value: row.section == .overrideScript ? overrideScriptName
: ConfigurationEditorCopy.value(of: row, snifferEnabled: row.section == .sniffer
? ConfigurationEditorCopy.snifferEnabled(in: session) : nil))
? ConfigurationEditorCopy.snifferEnabled(in: session) : nil,
mitmEnabled: row.section == .mitm
? ConfigurationEditorCopy.mitmEnabled(in: session) : nil))
}
.accessibilityIdentifier("configuration-editor.section.\(row.section.rawValue)")
}
Expand Down Expand Up @@ -915,7 +917,7 @@ enum ConfigurationSectionRoute: Equatable {



case .general, .dns, .sniffer, .inbound, .ntp, .experimental: return .settings([])
case .general, .dns, .sniffer, .mitm, .inbound, .ntp, .experimental: return .settings([])
case .tunnels: return .objectList(["tunnels"])
case .customRules: return .customRules
case .overrideScript: return .overrideScript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ public enum ConfigurationFieldValidation {

static let trieDomainPaths: Set<String> = [
"sniffer.skip-domain", "sniffer.force-domain", "dns.fake-ip-filter", "dns.fallback-filter.domain",
"mitm.skip-domain",
]


static let ipMatcherPaths: Set<String> = ["sniffer.skip-src-address", "sniffer.skip-dst-address"]
static let ipMatcherPaths: Set<String> = ["sniffer.skip-src-address", "sniffer.skip-dst-address", "mitm.skip-dst-address"]



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public extension ConfigurationFieldCatalog {



static let auditedSwitchPaths: Set<String> = ["sniffer.enable", "ntp.enable", "tuic-server.enable", "iptables.enable"]
static let auditedSwitchPaths: Set<String> = ["sniffer.enable", "mitm.enable", "ntp.enable", "tuic-server.enable", "iptables.enable"]
static let auditedSwitchSuffixes = [
".smux.enabled", ".smux.brutal-opts.enabled", ".ech-opts.enable", ".realm-opts.enable", ".ss-opts.enabled",
".transport-layer-padding.enabled", ".mux-option.brutal.enabled", ".shadow-tls.enable", ".res-tls.enable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation


public enum ConfigurationOutlineSection: String, CaseIterable, Sendable {
case general, dns, sniffer, inbound, proxies, proxyProviders, proxyGroups, rules, ruleProviders, subRules
case general, dns, sniffer, mitm, inbound, proxies, proxyProviders, proxyGroups, rules, ruleProviders, subRules
case tunnels, ntp, experimental, unknown


Expand Down Expand Up @@ -87,8 +87,9 @@ public enum ConfigurationOutline {
("dns", .dns), ("hosts", .dns),

("sniffer", .sniffer),



("mitm", .mitm),

("port", .inbound), ("socks-port", .inbound), ("mixed-port", .inbound), ("redir-port", .inbound),
("tproxy-port", .inbound), ("inbound-tfo", .inbound), ("inbound-mptcp", .inbound), ("tun", .inbound),
("listeners", .inbound), ("ss-config", .inbound), ("vmess-config", .inbound), ("tuic-server", .inbound),
Expand Down Expand Up @@ -119,15 +120,15 @@ public enum ConfigurationOutline {


public static func sections(for form: ConfigurationOutlineForm) -> [ConfigurationOutlineSection] {
[.general, .dns, .sniffer, .inbound, .proxies, .proxyProviders, .proxyGroups, .rules,
[.general, .dns, .sniffer, .mitm, .inbound, .proxies, .proxyProviders, .proxyGroups, .rules,
.ruleProviders, .subRules, .tunnels, .ntp, .experimental, .unknown, .customRules, .overrideScript]
}




public static func showsDefaults(_ section: ConfigurationOutlineSection) -> Bool {
[.general, .dns, .sniffer, .inbound, .ntp, .experimental].contains(section)
[.general, .dns, .sniffer, .mitm, .inbound, .ntp, .experimental].contains(section)
}


Expand Down Expand Up @@ -223,6 +224,10 @@ public enum ConfigurationOutline {
let sniffer = value("sniffer")
let protocols = field(sniffer, "sniff") ?? field(sniffer, "sniffing")
return .init(section: section, count: size(protocols), enabled: isTrue(field(sniffer, "enable")))
case .mitm:
let mitm = value("mitm")
let policies = field(mitm, "rewrite")
return .init(section: section, count: size(policies), enabled: isTrue(field(mitm, "enable")))
case .unknown:
return .init(section: section, count: Set(pairs.map(\.key).filter { sectionIndex[$0] == nil }).count)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ public enum UpstreamListDefault {
"sniffer.skip-dst-address": PinnedDefault([]),
"sniffer.port-whitelist": PinnedDefault([]),
"sniffer.sniffing": PinnedDefault([]),
"mitm.hosts": PinnedDefault([]),
"mitm.skip-domain": PinnedDefault([]),
"mitm.skip-dst-address": PinnedDefault([]),
"mitm.https-ports": PinnedDefault([]),
"mitm.http-ports": PinnedDefault([]),
"mitm.rewrite": PinnedDefault([]),
]

public static func pinned(for keyPath: String) -> PinnedDefault<[String]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public enum UpstreamBoolDefault {
"disable-keep-alive": .init(false),
"ntp.enable": .init(false),
"sniffer.enable": .init(false),
"mitm.enable": .init(false),
"sniffer.force-dns-mapping": .init(true),
"sniffer.parse-pure-ip": .init(true),
"sniffer.override-destination": .init(true),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"mitm": {
"title": {"en": "MITM Interception", "zh-Hans": "MITM 拦截", "zh-Hant": "MITM 攔截"},
"summary": {"en": "Terminates TLS on hosts you control, rewrites requests, and forwards them through the normal rule path.",
"zh-Hans": "对自己掌控的域名终结 TLS,改写请求后仍走正常的规则链路。",
"zh-Hant": "對自己掌控的網域終結 TLS,改寫請求後仍走正常的規則鏈路。"}
},
"mitm.enable": {
"title": {"en": "MITM Interception", "zh-Hans": "启用 MITM 拦截", "zh-Hant": "啟用 MITM 攔截"},
"summary": {"en": "Gated connections get a locally generated certificate. Install and trust the CA from the app before enabling.",
"zh-Hans": "被拦截的连接会收到本地生成的证书。启用前请先在应用内安装并信任 CA。",
"zh-Hant": "被攔截的連線會收到本地產生的憑證。啟用前請先在 App 內安裝並信任 CA。"}
},
"mitm.store-dir": {
"title": {"en": "CA Store Directory", "zh-Hans": "CA 存储目录", "zh-Hant": "CA 儲存目錄"},
"summary": {"en": "Persists the generated CA so restarts keep the same trust profile. Empty regenerates on every launch.",
"zh-Hans": "持久化生成的 CA,重启后保持同一个信任配置。留空则每次启动重新生成。",
"zh-Hant": "持久化產生的 CA,重啟後保持同一個信任設定。留空則每次啟動重新產生。"}
},
"mitm.hosts": {
"title": {"en": "Intercept Hosts", "zh-Hans": "拦截的域名", "zh-Hant": "攔截的網域"},
"summary": {"en": "Domains whose TLS is terminated. +.example.com covers every name under it.",
"zh-Hans": "终结这些域名的 TLS。+.example.com 覆盖其下所有子域。",
"zh-Hant": "終結這些網域的 TLS。+.example.com 涵蓋其下所有子網域。"},
"unset": {"en": "None", "zh-Hans": "无", "zh-Hant": "無"},
"example": "+.example.com"
},
"mitm.skip-domain": {
"title": {"en": "Never Intercept · Domain", "zh-Hans": "强制不拦截 · 域名", "zh-Hant": "強制不攔截 · 網域"},
"summary": {"en": "These domains are never intercepted, even inside the host gates.",
"zh-Hans": "这些域名永远不拦截,即使命中上方的域名列表。",
"zh-Hant": "這些網域一律不攔截,即使命中上方的網域清單。"},
"unset": {"en": "None", "zh-Hans": "无", "zh-Hant": "無"},
"example": "auth.example.com"
},
"mitm.skip-dst-address": {
"title": {"en": "Never Intercept · Destination Range", "zh-Hans": "强制不拦截 · 目标网段", "zh-Hant": "強制不攔截 · 目標網段"},
"summary": {"en": "Connections to these ranges are never intercepted.",
"zh-Hans": "发往这些网段的连接不拦截。",
"zh-Hant": "發往這些網段的連線不攔截。"},
"unset": {"en": "None", "zh-Hans": "无", "zh-Hant": "無"},
"example": "10.0.0.0/8"
},
"mitm.https-ports": {
"title": {"en": "HTTPS Ports", "zh-Hans": "HTTPS 端口", "zh-Hant": "HTTPS 連接埠"},
"summary": {"en": "Destination ports intercepted as HTTPS. Defaults to 443.",
"zh-Hans": "按 HTTPS 拦截的目标端口。默认 443。",
"zh-Hant": "按 HTTPS 攔截的目標連接埠。預設 443。"},
"unset": {"en": "443", "zh-Hans": "443", "zh-Hant": "443"},
"example": "443, 8443-8444"
},
"mitm.http-ports": {
"title": {"en": "HTTP Ports", "zh-Hans": "HTTP 端口", "zh-Hant": "HTTP 連接埠"},
"summary": {"en": "Destination ports intercepted as plain HTTP. Defaults to 80.",
"zh-Hans": "按明文 HTTP 拦截的目标端口。默认 80。",
"zh-Hant": "按明文 HTTP 攔截的目標連接埠。預設 80。"},
"unset": {"en": "80", "zh-Hans": "80", "zh-Hant": "80"},
"example": "80, 8080-8880"
},
"mitm.rewrite": {
"title": {"en": "Rewrite Policies", "zh-Hans": "重写策略", "zh-Hant": "改寫策略"},
"summary": {"en": "Ordered; the first matching policy wins.",
"zh-Hans": "按顺序匹配,首个命中的生效。",
"zh-Hant": "依序比對,首個命中的生效。"},
"unset": {"en": "None", "zh-Hans": "无", "zh-Hant": "無"}
},
"mitm.rewrite[].url-pattern": {
"title": {"en": "URL Pattern", "zh-Hans": "URL 匹配", "zh-Hant": "URL 對應"},
"summary": {"en": "Matches the host and its subdomains under /api/; a bare /api/ matches any gated host.",
"zh-Hans": "匹配该主机及其子域的 /api/ 路径;仅填 /api/ 则匹配所有已拦截主机。",
"zh-Hant": "符合該主機及其子網域的 /api/ 路徑;僅填 /api/ 則符合所有已攔截主機。"}
},
"mitm.rewrite[].redirect": {
"title": {"en": "Redirect To", "zh-Hans": "重定向到", "zh-Hant": "重新導向到"},
"summary": {"en": "Answers 302 without dialing. A bare host keeps the original path and query.",
"zh-Hans": "直接返回 302,不建立上游连接。仅填主机名时保留原路径与查询参数。",
"zh-Hant": "直接回傳 302,不建立上游連線。僅填主機名稱時保留原路徑與查詢參數。"}
},
"mitm.rewrite[].reject": {
"title": {"en": "Reject With", "zh-Hans": "拒绝并返回", "zh-Hant": "拒絕並回傳"},
"summary": {"en": "Answers this 4xx status without dialing, for example 404.",
"zh-Hans": "直接返回该 4xx 状态码,不建立上游连接,例如 404。",
"zh-Hant": "直接回傳該 4xx 狀態碼,不建立上游連線,例如 404。"}
},
"mitm.rewrite[].headers": {
"title": {"en": "Header Rules", "zh-Hans": "请求头改写", "zh-Hant": "請求標頭改寫"},
"summary": {"en": "Applied to the request before it leaves for the upstream.",
"zh-Hans": "在请求发往上游之前执行。",
"zh-Hant": "在請求送往上游之前執行。"}
},
"mitm.rewrite[].headers[].action": {
"title": {"en": "Action", "zh-Hans": "动作", "zh-Hant": "動作"},
"summary": {"en": "set replaces the header, add appends a value, remove deletes it.",
"zh-Hans": "set 替换该请求头,add 追加一个值,remove 删除。",
"zh-Hant": "set 取代該標頭,add 附加一個值,remove 刪除。"}
},
"mitm.rewrite[].headers[].name": {
"title": {"en": "Header", "zh-Hans": "请求头名称", "zh-Hant": "標頭名稱"}
},
"mitm.rewrite[].headers[].value": {
"title": {"en": "Value", "zh-Hans": "值", "zh-Hant": "值"}
},
"mitm.rewrite[].headers[].value-prefix": {
"title": {"en": "Value Prefix", "zh-Hans": "值前缀", "zh-Hant": "值字首"},
"summary": {"en": "Prepended to the value the client already sends, for example “Bearer ”.",
"zh-Hans": "拼在客户端已发送的值前面,例如 “Bearer ”。",
"zh-Hant": "接在用戶端已傳送的值前面,例如 “Bearer ”。"}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,67 @@
"*"
]
}
],
"mitm": [
{
"id": "basic",
"title": {
"en": "Basics",
"zh-Hans": "基本",
"zh-Hant": "基本"
},
"footer": {
"en": "Terminates TLS for the gated hosts and answers from the rewritten request. Install and trust the CA first.",
"zh-Hans": "对拦截域名终结 TLS,按重写策略应答。请先安装并信任 CA。",
"zh-Hant": "對攔截網域終結 TLS,依改寫策略應答。請先安裝並信任 CA。"
},
"keys": [
"enable",
"store-dir"
]
},
{
"id": "gates",
"title": {
"en": "Scope",
"zh-Hans": "拦截范围",
"zh-Hant": "攔截範圍"
},
"keys": [
"hosts",
"skip-domain",
"skip-dst-address",
"https-ports",
"http-ports"
]
},
{
"id": "rewrite",
"title": {
"en": "Rewrite Policies",
"zh-Hans": "重写策略",
"zh-Hant": "改寫策略"
},
"footer": {
"en": "Ordered rules: the first match answers redirect/reject or edits headers, then the request leaves through the normal proxy.",
"zh-Hans": "按顺序匹配:首个命中的执行重定向、拒绝或改写请求头,之后请求仍走正常代理。",
"zh-Hant": "依序比對:首個命中的執行重新導向、拒絕或改寫請求標頭,之後請求仍走正常代理。"
},
"keys": [
"rewrite"
]
},
{
"id": "more",
"title": {
"en": "More Settings",
"zh-Hans": "更多设置",
"zh-Hant": "更多設定"
},
"keys": [
"*"
]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ public struct ClashMacConfigurationInspector: View {



case .general, .dns, .sniffer, .inbound, .ntp, .experimental:
case .general, .dns, .sniffer, .mitm, .inbound, .ntp, .experimental:
guard let editing = actions.settingsSection else { return actions.outlineDestination(section) }
let known = ConfigurationOutline.knownTopLevelKeys.filter { ConfigurationOutline.section(forTopLevelKey: $0) == section }
return .page {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ struct ClashMacConfigurationOutlineCard: View {
case .general: return .copy("General Configuration (Configuration Chapter)")
case .dns: return .copy("DNS")
case .sniffer: return .copy("Domain Sniffing (Configuration Chapter)")
case .mitm: return .copy("MITM Interception (Configuration Chapter)")
case .inbound: return .copy("Inbounds (Configuration Chapter)")
case .proxies: return .copy("Proxies (Configuration Chapter)")
case .proxyProviders: return .copy("Proxy Providers (Configuration Chapter)")
Expand Down Expand Up @@ -352,6 +353,8 @@ struct ClashMacConfigurationOutlineCard: View {
switch (row.section, row.enabled) {
case (.sniffer, false?): return (.copy("Off"), nil)
case (.sniffer, true?): return (.copy("On"), nil)
case (.mitm, false?): return (.copy("Off"), nil)
case (.mitm, true?): return (.copy("On"), nil)
default: return (.verbatim(""), nil)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ struct ClashMacSearchField: Hashable, Identifiable {

static func index(_ catalog: ConfigurationFieldCatalog, copy: ConfigurationFieldCopy, locale: Locale,
lanShareKeys: Set<String> = []) -> [ClashMacSearchField] {
let pages: Set<ConfigurationOutlineSection> = [.general, .dns, .sniffer, .inbound, .ntp, .experimental]
let pages: Set<ConfigurationOutlineSection> = [.general, .dns, .sniffer, .mitm, .inbound, .ntp, .experimental]
return catalog.fields.compactMap { field in
guard !field.path.contains("["), !field.path.contains("*") else { return nil }
let parts = field.path.split(separator: ".").map(String.init)
Expand Down