Skip to content

Commit 055e40d

Browse files
authored
Merge pull request #115 from kkebo/fix-apply-with-excluded-domains
fix: Apply with excluded domains
2 parents 832c3b8 + bf98ce1 commit 055e40d

File tree

5 files changed

+162
-71
lines changed

5 files changed

+162
-71
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// NEEvaluateConnectionRuleAction+Codable.swift
3+
// DNSecure
4+
//
5+
// Created by Kenta Kubo on 8/31/25.
6+
//
7+
8+
import NetworkExtension
9+
10+
extension NEEvaluateConnectionRuleAction: @retroactive Codable {}

DNSecure/Models/OnDemandRule.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// OnDemandRule.swift
3+
// DNSecure
4+
//
5+
// Created by Kenta Kubo on 8/31/25.
6+
//
7+
8+
import Foundation
9+
import NetworkExtension
10+
11+
struct OnDemandRule {
12+
var id = UUID()
13+
var name: String
14+
var action: NEOnDemandRuleAction = .connect
15+
var interfaceType: NEOnDemandRuleInterfaceType = .any
16+
var ssidMatch: [String] = []
17+
var dnsSearchDomainMatch: [String] = []
18+
var dnsServerAddressMatch: [String] = []
19+
var probeURL: URL?
20+
var excludedDomains: [String]?
21+
}
22+
23+
extension OnDemandRule: Identifiable {}
24+
25+
extension OnDemandRule: Equatable {}
26+
27+
extension OnDemandRule: Hashable {}
28+
29+
extension OnDemandRule: Codable {}
30+
31+
extension [OnDemandRule] {
32+
func toNEOnDemandRules() -> [NEOnDemandRule] {
33+
self.map { rule in
34+
switch rule.action {
35+
case .connect:
36+
let newRule = NEOnDemandRuleConnect()
37+
newRule.interfaceTypeMatch = rule.interfaceType
38+
if rule.interfaceType.isSSIDUsed {
39+
newRule.ssidMatch = rule.ssidMatch
40+
}
41+
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
42+
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
43+
newRule.probeURL = rule.probeURL
44+
return newRule
45+
case .disconnect:
46+
let newRule = NEOnDemandRuleDisconnect()
47+
newRule.interfaceTypeMatch = rule.interfaceType
48+
if rule.interfaceType.isSSIDUsed {
49+
newRule.ssidMatch = rule.ssidMatch
50+
}
51+
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
52+
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
53+
newRule.probeURL = rule.probeURL
54+
return newRule
55+
case .evaluateConnection:
56+
let newRule = NEOnDemandRuleEvaluateConnection()
57+
newRule.interfaceTypeMatch = rule.interfaceType
58+
if rule.interfaceType.isSSIDUsed {
59+
newRule.ssidMatch = rule.ssidMatch
60+
}
61+
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
62+
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
63+
newRule.probeURL = rule.probeURL
64+
newRule.connectionRules =
65+
switch rule.excludedDomains {
66+
case let domains? where !domains.isEmpty:
67+
[.init(matchDomains: domains, andAction: .neverConnect)]
68+
case _: []
69+
}
70+
return newRule
71+
case .ignore:
72+
let newRule = NEOnDemandRuleIgnore()
73+
newRule.interfaceTypeMatch = rule.interfaceType
74+
if rule.interfaceType.isSSIDUsed {
75+
newRule.ssidMatch = rule.ssidMatch
76+
}
77+
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
78+
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
79+
newRule.probeURL = rule.probeURL
80+
return newRule
81+
@unknown case _:
82+
preconditionFailure("Unexpected NEOnDemandRuleAction")
83+
}
84+
}
85+
}
86+
}

DNSecure/Models/Resolver.swift

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -106,77 +106,6 @@ extension Configuration: CustomStringConvertible {
106106
}
107107
}
108108

109-
struct OnDemandRule {
110-
var id = UUID()
111-
var name: String
112-
var action: NEOnDemandRuleAction = .connect
113-
var interfaceType: NEOnDemandRuleInterfaceType = .any
114-
var ssidMatch: [String] = []
115-
var dnsSearchDomainMatch: [String] = []
116-
var dnsServerAddressMatch: [String] = []
117-
var probeURL: URL?
118-
}
119-
120-
extension OnDemandRule: Identifiable {}
121-
122-
extension OnDemandRule: Equatable {}
123-
124-
extension OnDemandRule: Hashable {}
125-
126-
extension OnDemandRule: Codable {}
127-
128-
extension Array where Self.Element == OnDemandRule {
129-
func toNEOnDemandRules() -> [NEOnDemandRule] {
130-
self.lazy
131-
.map { rule in
132-
switch rule.action {
133-
case .connect:
134-
let newRule = NEOnDemandRuleConnect()
135-
newRule.interfaceTypeMatch = rule.interfaceType
136-
if rule.interfaceType.isSSIDUsed {
137-
newRule.ssidMatch = rule.ssidMatch
138-
}
139-
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
140-
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
141-
newRule.probeURL = rule.probeURL
142-
return newRule
143-
case .disconnect:
144-
let newRule = NEOnDemandRuleDisconnect()
145-
newRule.interfaceTypeMatch = rule.interfaceType
146-
if rule.interfaceType.isSSIDUsed {
147-
newRule.ssidMatch = rule.ssidMatch
148-
}
149-
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
150-
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
151-
newRule.probeURL = rule.probeURL
152-
return newRule
153-
case .evaluateConnection:
154-
let newRule = NEOnDemandRuleEvaluateConnection()
155-
newRule.interfaceTypeMatch = rule.interfaceType
156-
if rule.interfaceType.isSSIDUsed {
157-
newRule.ssidMatch = rule.ssidMatch
158-
}
159-
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
160-
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
161-
newRule.probeURL = rule.probeURL
162-
return newRule
163-
case .ignore:
164-
let newRule = NEOnDemandRuleIgnore()
165-
newRule.interfaceTypeMatch = rule.interfaceType
166-
if rule.interfaceType.isSSIDUsed {
167-
newRule.ssidMatch = rule.ssidMatch
168-
}
169-
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
170-
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
171-
newRule.probeURL = rule.probeURL
172-
return newRule
173-
default:
174-
preconditionFailure("Unexpected NEOnDemandRuleAction")
175-
}
176-
}
177-
}
178-
}
179-
180109
struct Resolver {
181110
var id = UUID()
182111
var name: String
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import SwiftUI
2+
3+
struct ExcludedDomainsView {
4+
@Binding var domains: [String]
5+
}
6+
7+
extension ExcludedDomainsView: View {
8+
var body: some View {
9+
Form {
10+
Section {
11+
ForEach(0..<self.domains.count, id: \.self) { i in
12+
LazyTextField(
13+
"Domain",
14+
// self.$rule.excludedDomains[i] causes crash on deletion
15+
text: .init(
16+
get: { self.domains[i] },
17+
set: { self.domains[i] = $0 }
18+
)
19+
)
20+
.textContentType(.URL)
21+
.keyboardType(.URL)
22+
.textInputAutocapitalization(.never)
23+
.autocorrectionDisabled()
24+
}
25+
.onDelete { self.domains.remove(atOffsets: $0) }
26+
.onMove { self.domains.move(fromOffsets: $0, toOffset: $1) }
27+
Button("Add Domain") {
28+
self.domains.append("")
29+
}
30+
} footer: {
31+
Text(
32+
"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`."
33+
)
34+
}
35+
}
36+
.navigationTitle("Excluded Domains")
37+
.toolbar {
38+
EditButton()
39+
}
40+
}
41+
}
42+
43+
@available(iOS 17, *)
44+
#Preview {
45+
@Previewable @State var domains = ["example.com"]
46+
NavigationStack {
47+
ExcludedDomainsView(domains: $domains)
48+
}
49+
}

DNSecure/Views/RuleView.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ extension RuleView: View {
6767
Text($0.description)
6868
}
6969
}
70+
if self.rule.action == .evaluateConnection {
71+
NavigationLink {
72+
ExcludedDomainsView(
73+
domains: .init(
74+
get: { self.rule.excludedDomains ?? [] },
75+
set: { self.rule.excludedDomains = $0 }
76+
)
77+
)
78+
} label: {
79+
HStack {
80+
Text("Excluded Domains")
81+
Spacer()
82+
Text("\(self.rule.excludedDomains?.count ?? 0)")
83+
.foregroundStyle(.secondary)
84+
}
85+
}
86+
}
7087
}
7188
}
7289
.navigationTitle(self.rule.name)

0 commit comments

Comments
 (0)