Skip to content
Draft
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
21 changes: 19 additions & 2 deletions Sources/PartoutCore/Modules/DNSModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public struct DNSModule: Module, BuildableType, Hashable, Codable {

public let searchDomains: [Address]?

public let matchDomains: [Address]?

public let routesThroughVPN: Bool?

fileprivate init(
Expand All @@ -32,13 +34,15 @@ public struct DNSModule: Module, BuildableType, Hashable, Codable {
servers: [Address],
domainName: Address?,
searchDomains: [Address]?,
matchDomains: [Address]?,
routesThroughVPN: Bool?
) {
self.id = id
self.protocolType = protocolType
self.servers = servers
self.domainName = domainName
self.searchDomains = searchDomains
self.matchDomains = matchDomains
self.routesThroughVPN = routesThroughVPN
}

Expand All @@ -50,17 +54,16 @@ public struct DNSModule: Module, BuildableType, Hashable, Codable {
switch protocolType {
case .cleartext:
break

case .https(let url):
builder.protocolType = .https
builder.dohURL = url.absoluteString

case .tls(let hostname):
builder.protocolType = .tls
builder.dotHostname = hostname
}
builder.domainName = domainName?.rawValue
builder.searchDomains = searchDomains?.map(\.rawValue)
builder.matchDomains = matchDomains?.map(\.rawValue)
builder.routesThroughVPN = routesThroughVPN
return builder
}
Expand All @@ -82,6 +85,8 @@ extension DNSModule {

public var searchDomains: [String]?

public var matchDomains: [String]?

public var routesThroughVPN: Bool?

public static func empty() -> Self {
Expand All @@ -96,6 +101,7 @@ extension DNSModule {
dotHostname: String = "",
domainName: String? = nil,
searchDomains: [String]? = nil,
matchDomains: [String]? = nil,
routesThroughVPN: Bool? = nil
) {
self.id = id
Expand All @@ -105,6 +111,7 @@ extension DNSModule {
self.dotHostname = dotHostname
self.domainName = domainName
self.searchDomains = searchDomains
self.matchDomains = matchDomains
self.routesThroughVPN = routesThroughVPN
}

Expand Down Expand Up @@ -136,6 +143,15 @@ extension DNSModule {
}
return addr
}
let validMatchDomains = try matchDomains?.compactMap {
guard !$0.isEmpty else {
return nil as Address?
}
guard let addr = Address(rawValue: $0), !addr.isIPAddress else {
throw PartoutError.invalidFields(["matchDomains": $0])
}
return addr
}

let validProtocolType: ProtocolType
switch protocolType {
Expand All @@ -160,6 +176,7 @@ extension DNSModule {
servers: validServers,
domainName: validDomainName,
searchDomains: validSearchDomains,
matchDomains: validMatchDomains,
routesThroughVPN: routesThroughVPN
)
}
Expand Down
64 changes: 44 additions & 20 deletions Sources/PartoutOS/AppleNE/Modules/DNSModule+NE.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,76 @@ import NetworkExtension

extension DNSModule: NESettingsApplying {
public func apply(_ ctx: PartoutLoggerContext, to settings: inout NEPacketTunnelNetworkSettings) {
var dnsSettings: NEDNSSettings?
let dnsSettings: NEDNSSettings
let rawServers = servers.map(\.rawValue)

// Former DNS settings are always overridden, even with empty servers
switch protocolType {
case .cleartext:
if !rawServers.isEmpty {
dnsSettings = NEDNSSettings(servers: rawServers)
pp_log(ctx, .os, .info, "\t\tServers: \(servers.map { $0.asSensitiveAddress(ctx) })")
} else {
pp_log(ctx, .os, .info, "\t\tServers: empty")
guard !rawServers.isEmpty else {
pp_log(ctx, .os, .info, "\t\tSkip DNS settings, cleartext requires non-empty servers")
return
}

dnsSettings = NEDNSSettings(servers: rawServers)
pp_log(ctx, .os, .info, "\t\tServers: \(servers.map { $0.asSensitiveAddress(ctx) })")
case .https(let url):
let specificSettings = NEDNSOverHTTPSSettings(servers: rawServers)
specificSettings.serverURL = url
dnsSettings = specificSettings
pp_log(ctx, .os, .info, "\t\tServers: \(servers.map { $0.asSensitiveAddress(ctx) })")
pp_log(ctx, .os, .info, "\t\tDoH URL: \(url.absoluteString.asSensitiveAddress(ctx))")

case .tls(let hostname):
let specificSettings = NEDNSOverTLSSettings(servers: rawServers)
specificSettings.serverName = hostname
dnsSettings = specificSettings
pp_log(ctx, .os, .info, "\t\tServers: \(servers.map { $0.asSensitiveAddress(ctx) })")
pp_log(ctx, .os, .info, "\t\tDoT hostname: \(hostname.asSensitiveAddress(ctx))")

@unknown default:
break
}

if dnsSettings != nil {
domainName.map {
dnsSettings?.domainName = $0.rawValue
pp_log(ctx, .os, .info, "\t\tDomain: \($0.asSensitiveAddress(ctx))")
// Search domains
domainName.map {
dnsSettings.domainName = $0.rawValue
pp_log(ctx, .os, .info, "\t\tDomain: \($0.asSensitiveAddress(ctx))")
}
searchDomains.map {
var list = $0.map(\.rawValue)
// Assume domain name to be first search domain
if let domainName {
list.insert(domainName.rawValue, at: 0)
}
searchDomains.map {
guard !$0.isEmpty else {
return
guard !list.isEmpty else { return }
dnsSettings.searchDomains = list
pp_log(ctx, .os, .info, "\t\tSearch domains: \(list.map { $0.asSensitiveAddress(ctx) })")
}

// This is why we guard before setting .matchDomains:
// https://git.zx2c4.com/wireguard-apple/commit/?id=20bdf46792905de8862ae7641e50e0f9f99ec946
//
let searchDomains = dnsSettings.searchDomains ?? []
let canSetMatchDomains = !dnsSettings.servers.isEmpty || !searchDomains.isEmpty
if canSetMatchDomains {
//
// Credit for .matchDomains:
// https://github.com/WireGuard/wireguard-apple/pull/11
//
if let matchDomains, !matchDomains.isEmpty {
dnsSettings.matchDomains = matchDomains.map(\.rawValue)
// True if any search domain is in match domains
dnsSettings.matchDomainsNoSearch = !matchDomains.contains {
searchDomains.contains($0.rawValue)
}
dnsSettings?.searchDomains = $0.map(\.rawValue)
pp_log(ctx, .os, .info, "\t\tSearch domains: \($0.map { $0.asSensitiveAddress(ctx) })")
} else {
// Add "" so that all DNS queries must first go through the tunnel's DNS.
// NEDNSSettings.searchDomains does not work so we add the searches to
// matchDomains, which does work.
dnsSettings.matchDomains = [""] + searchDomains
dnsSettings.matchDomainsNoSearch = false
}
} else {
pp_log(ctx, .os, .info, "\t\tSkip DNS settings")
}

// Commit to tunnel settings
settings.dnsSettings = dnsSettings
}
}
8 changes: 5 additions & 3 deletions Sources/PartoutOS/AppleNE/Modules/Profile+NE.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ extension Profile {
// 4. configure DNS for domain-based routing

if let dnsSettings = neSettings.dnsSettings {

// route DNS through VPN first unless no servers provided
if !dnsSettings.servers.isEmpty {
// Route DNS through VPN first unless:
// - No servers provided
// - .matchDomains is not configured
// This is a fallback as it *SHOULD* be accomplished by DNSModule+NE
if !dnsSettings.servers.isEmpty, dnsSettings.matchDomains == nil {
neSettings.dnsSettings?.matchDomains = [""]
}
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/PartoutOSTests/AppleNE/NESettingsApplyingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ struct NESettingsApplyingTests {
module.apply(.global, to: &sut)

let dnsSettings = try #require(sut.dnsSettings)
let expSearchDomains = ([module.domainName] + (module.searchDomains ?? [])).map(\.?.rawValue)
#expect(dnsSettings.dnsProtocol == .cleartext)
#expect(dnsSettings.servers == module.servers.map(\.rawValue))
#expect(dnsSettings.domainName == module.domainName?.rawValue)
#expect(dnsSettings.searchDomains == module.searchDomains?.map(\.rawValue))
#expect(dnsSettings.searchDomains == expSearchDomains)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct ProfileNetworkSettingsTests {
activatingModules: true
).build().networkSettings(with: nil)

#expect(sut.dnsSettings?.matchDomains == [""])
#expect(sut.dnsSettings?.matchDomains == ["", "domain.com"])
}

// MARK: With remote info
Expand Down
4 changes: 4 additions & 0 deletions scripts/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ components:
"$ref": "#/components/schemas/Address"
id:
"$ref": "#/components/schemas/UniqueID"
matchDomains:
items:
"$ref": "#/components/schemas/Address"
type: array
protocolType:
"$ref": "#/components/schemas/DNSModule.ProtocolType"
routesThroughVPN:
Expand Down