Add custom on-demand rules support

This commit is contained in:
世界
2025-12-30 00:18:43 +08:00
parent 6de410c3f7
commit 58b5e9d732
13 changed files with 1310 additions and 59 deletions
+230
View File
@@ -0,0 +1,230 @@
import Foundation
import NetworkExtension
public enum OnDemandRuleAction: Int, Codable, CaseIterable, Identifiable {
case connect = 1
case disconnect = 2
case evaluateConnection = 3
case ignore = 4
public var id: Int { rawValue }
public var name: String {
switch self {
case .connect:
return NSLocalizedString("Connect", comment: "")
case .disconnect:
return NSLocalizedString("Disconnect", comment: "")
case .evaluateConnection:
return NSLocalizedString("Evaluate Connection", comment: "")
case .ignore:
return NSLocalizedString("Ignore", comment: "")
}
}
public var actionDescription: String {
switch self {
case .connect:
return NSLocalizedString("Start the VPN connection when conditions match.", comment: "")
case .disconnect:
return NSLocalizedString("Stop the VPN connection when conditions match.", comment: "")
case .evaluateConnection:
return NSLocalizedString("Evaluate the destination host before deciding to connect.", comment: "")
case .ignore:
return NSLocalizedString("Leave the VPN connection in its current state.", comment: "")
}
}
}
public enum OnDemandRuleInterfaceType: Int, Codable, Identifiable {
case any = 0
#if os(macOS) || os(tvOS)
case ethernet = 1
#endif
case wifi = 2
#if os(iOS)
case cellular = 3
#endif
public var id: Int { rawValue }
public var name: String {
switch self {
case .any:
return NSLocalizedString("Any", comment: "")
#if os(macOS) || os(tvOS)
case .ethernet:
return NSLocalizedString("Ethernet", comment: "")
#endif
case .wifi:
return NSLocalizedString("Wi-Fi", comment: "")
#if os(iOS)
case .cellular:
return NSLocalizedString("Cellular", comment: "")
#endif
}
}
public static var availableCases: [OnDemandRuleInterfaceType] {
#if os(iOS)
return [.any, .wifi, .cellular]
#elseif os(macOS)
return [.any, .ethernet, .wifi]
#elseif os(tvOS)
return [.any, .ethernet, .wifi]
#endif
}
}
public enum EvaluateConnectionRuleAction: Int, Codable, CaseIterable, Identifiable {
case connectIfNeeded = 1
case neverConnect = 2
public var id: Int { rawValue }
public var name: String {
switch self {
case .connectIfNeeded:
return NSLocalizedString("Connect If Needed", comment: "")
case .neverConnect:
return NSLocalizedString("Never Connect", comment: "")
}
}
}
public struct EvaluateConnectionRule: Codable, Identifiable, Hashable {
public var id = UUID()
public var action: EvaluateConnectionRuleAction = .connectIfNeeded
public var matchDomains: [String] = []
public var useDNSServers: [String] = []
public var probeURL: String = ""
private enum CodingKeys: String, CodingKey {
case id
case action
case matchDomains
case useDNSServers
case probeURL
}
public init() {}
public init(action: EvaluateConnectionRuleAction, matchDomains: [String], useDNSServers: [String] = [], probeURL: String = "") {
self.action = action
self.matchDomains = matchDomains
self.useDNSServers = useDNSServers
self.probeURL = probeURL
}
func toNERule() -> NEEvaluateConnectionRule {
let neAction: NEEvaluateConnectionRuleAction
switch action {
case .connectIfNeeded:
neAction = .connectIfNeeded
case .neverConnect:
neAction = .neverConnect
}
let rule = NEEvaluateConnectionRule(matchDomains: matchDomains, andAction: neAction)
if !useDNSServers.isEmpty {
rule.useDNSServers = useDNSServers
}
if !probeURL.isEmpty, let url = URL(string: probeURL) {
rule.probeURL = url
}
return rule
}
}
public struct OnDemandRule: Codable, Identifiable, Hashable {
public var id = UUID()
public var action: OnDemandRuleAction = .connect
public var interfaceType: OnDemandRuleInterfaceType = .any
public var ssidMatch: [String] = []
public var dnsSearchDomainMatch: [String] = []
public var dnsServerAddressMatch: [String] = []
public var probeURL: String = ""
public var connectionRules: [EvaluateConnectionRule] = []
private enum CodingKeys: String, CodingKey {
case id
case action
case interfaceType
case ssidMatch
case dnsSearchDomainMatch
case dnsServerAddressMatch
case probeURL
case connectionRules
}
public init() {}
public init(
action: OnDemandRuleAction,
interfaceType: OnDemandRuleInterfaceType = .any,
ssidMatch: [String] = [],
dnsSearchDomainMatch: [String] = [],
dnsServerAddressMatch: [String] = [],
probeURL: String = "",
connectionRules: [EvaluateConnectionRule] = []
) {
self.action = action
self.interfaceType = interfaceType
self.ssidMatch = ssidMatch
self.dnsSearchDomainMatch = dnsSearchDomainMatch
self.dnsServerAddressMatch = dnsServerAddressMatch
self.probeURL = probeURL
self.connectionRules = connectionRules
}
func toNERule() -> NEOnDemandRule {
let rule: NEOnDemandRule
switch action {
case .connect:
rule = NEOnDemandRuleConnect()
case .disconnect:
rule = NEOnDemandRuleDisconnect()
case .ignore:
rule = NEOnDemandRuleIgnore()
case .evaluateConnection:
let evalRule = NEOnDemandRuleEvaluateConnection()
let validRules = connectionRules.filter { !$0.matchDomains.isEmpty }
if !validRules.isEmpty {
evalRule.connectionRules = validRules.map { $0.toNERule() }
}
rule = evalRule
}
switch interfaceType {
case .any:
rule.interfaceTypeMatch = .any
#if os(macOS) || os(tvOS)
case .ethernet:
rule.interfaceTypeMatch = .ethernet
#endif
case .wifi:
rule.interfaceTypeMatch = .wiFi
#if os(iOS)
case .cellular:
rule.interfaceTypeMatch = .cellular
#endif
}
if !ssidMatch.isEmpty {
rule.ssidMatch = ssidMatch
}
if !dnsSearchDomainMatch.isEmpty {
rule.dnsSearchDomainMatch = dnsSearchDomainMatch
}
if !dnsServerAddressMatch.isEmpty {
rule.dnsServerAddressMatch = dnsServerAddressMatch
}
if !probeURL.isEmpty, let url = URL(string: probeURL) {
rule.probeURL = url
}
return rule
}
}