Merge pull request #115 from kkebo/fix-apply-with-excluded-domains

fix: Apply with excluded domains
This commit is contained in:
Kenta Kubo 2025-09-05 00:52:35 +09:00 committed by GitHub
commit 055e40d2cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 162 additions and 71 deletions

View file

@ -0,0 +1,10 @@
//
// NEEvaluateConnectionRuleAction+Codable.swift
// DNSecure
//
// Created by Kenta Kubo on 8/31/25.
//
import NetworkExtension
extension NEEvaluateConnectionRuleAction: @retroactive Codable {}

View file

@ -0,0 +1,86 @@
//
// OnDemandRule.swift
// DNSecure
//
// Created by Kenta Kubo on 8/31/25.
//
import Foundation
import NetworkExtension
struct OnDemandRule {
var id = UUID()
var name: String
var action: NEOnDemandRuleAction = .connect
var interfaceType: NEOnDemandRuleInterfaceType = .any
var ssidMatch: [String] = []
var dnsSearchDomainMatch: [String] = []
var dnsServerAddressMatch: [String] = []
var probeURL: URL?
var excludedDomains: [String]?
}
extension OnDemandRule: Identifiable {}
extension OnDemandRule: Equatable {}
extension OnDemandRule: Hashable {}
extension OnDemandRule: Codable {}
extension [OnDemandRule] {
func toNEOnDemandRules() -> [NEOnDemandRule] {
self.map { rule in
switch rule.action {
case .connect:
let newRule = NEOnDemandRuleConnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .disconnect:
let newRule = NEOnDemandRuleDisconnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .evaluateConnection:
let newRule = NEOnDemandRuleEvaluateConnection()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
newRule.connectionRules =
switch rule.excludedDomains {
case let domains? where !domains.isEmpty:
[.init(matchDomains: domains, andAction: .neverConnect)]
case _: []
}
return newRule
case .ignore:
let newRule = NEOnDemandRuleIgnore()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
@unknown case _:
preconditionFailure("Unexpected NEOnDemandRuleAction")
}
}
}
}

View file

@ -106,77 +106,6 @@ extension Configuration: CustomStringConvertible {
}
}
struct OnDemandRule {
var id = UUID()
var name: String
var action: NEOnDemandRuleAction = .connect
var interfaceType: NEOnDemandRuleInterfaceType = .any
var ssidMatch: [String] = []
var dnsSearchDomainMatch: [String] = []
var dnsServerAddressMatch: [String] = []
var probeURL: URL?
}
extension OnDemandRule: Identifiable {}
extension OnDemandRule: Equatable {}
extension OnDemandRule: Hashable {}
extension OnDemandRule: Codable {}
extension Array where Self.Element == OnDemandRule {
func toNEOnDemandRules() -> [NEOnDemandRule] {
self.lazy
.map { rule in
switch rule.action {
case .connect:
let newRule = NEOnDemandRuleConnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .disconnect:
let newRule = NEOnDemandRuleDisconnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .evaluateConnection:
let newRule = NEOnDemandRuleEvaluateConnection()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .ignore:
let newRule = NEOnDemandRuleIgnore()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.isSSIDUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
default:
preconditionFailure("Unexpected NEOnDemandRuleAction")
}
}
}
}
struct Resolver {
var id = UUID()
var name: String

View file

@ -0,0 +1,49 @@
import SwiftUI
struct ExcludedDomainsView {
@Binding var domains: [String]
}
extension ExcludedDomainsView: View {
var body: some View {
Form {
Section {
ForEach(0..<self.domains.count, id: \.self) { i in
LazyTextField(
"Domain",
// self.$rule.excludedDomains[i] causes crash on deletion
text: .init(
get: { self.domains[i] },
set: { self.domains[i] = $0 }
)
)
.textContentType(.URL)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
.onDelete { self.domains.remove(atOffsets: $0) }
.onMove { self.domains.move(fromOffsets: $0, toOffset: $1) }
Button("Add Domain") {
self.domains.append("")
}
} footer: {
Text(
"Each domain is matched against the destination hostname using suffix matching, and each label in the domain must match an entire label in the hostname. For example, the domain `example.com` will match the hostname `www.example.com` but not `www.anotherexample.com`."
)
}
}
.navigationTitle("Excluded Domains")
.toolbar {
EditButton()
}
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var domains = ["example.com"]
NavigationStack {
ExcludedDomainsView(domains: $domains)
}
}

View file

@ -67,6 +67,23 @@ extension RuleView: View {
Text($0.description)
}
}
if self.rule.action == .evaluateConnection {
NavigationLink {
ExcludedDomainsView(
domains: .init(
get: { self.rule.excludedDomains ?? [] },
set: { self.rule.excludedDomains = $0 }
)
)
} label: {
HStack {
Text("Excluded Domains")
Spacer()
Text("\(self.rule.excludedDomains?.count ?? 0)")
.foregroundStyle(.secondary)
}
}
}
}
}
.navigationTitle(self.rule.name)