feat(apple): pass-through search domain to VPN resolver config (#8421)

In order to have the system expand search domains for us, we need to set
a very peculiar combination of configuration options in the
`NEDNSSettings` of the VPN configuration:

- We need to include our search domains in the list of `matchDomains`
- We need to set `matchDomainsNoSearch = false`
- We need to set the `searchDomains` field

Technically, we don't even need to set `searchDomains` by itself.
Reading the docs in more detail for the `matchDomainsNoSearch` flag
explains why:

> A Boolean that specifies if the domains in the matchDomains list
should not be appended to the resolver’s list of search domains.

The double-negative here is confusing but essentially, what this says
is:

> If false, append the list of match domains to the resolver's search
domains.

That is exactly what we want. We want a search domain of e.g.
`example.com` to append to the list of search domains for the primary
resolver of non-scoped DNS queries.

I tested without setting `searchDomains` and it does still work: The
system will still expand the domain for us und send us a FQDN query of
e.g. `foo.example.com`. However, I figured not setting `searchDomains`
at all is quite confusing so I left it in there.

Related: #8410 (Fixes it for MacOS)

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: thomas <firezone@firezones-MacBook-Air.fritz.box>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Thomas Eizinger
2025-03-13 17:08:27 +11:00
committed by GitHub
parent d133ee84b7
commit 58d241f705
5 changed files with 40 additions and 7 deletions

View File

@@ -88,6 +88,7 @@ mod ffi {
&self,
tunnelAddressIPv4: String,
tunnelAddressIPv6: String,
searchDomain: Option<String>,
dnsAddresses: String,
routeListv4: String,
routeListv6: String,
@@ -128,7 +129,7 @@ impl Callbacks for CallbackHandler {
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_addresses: Vec<IpAddr>,
_search_domain: Option<DomainName>,
search_domain: Option<DomainName>,
route_list_v4: Vec<Ipv4Network>,
route_list_v6: Vec<Ipv6Network>,
) {
@@ -141,6 +142,7 @@ impl Callbacks for CallbackHandler {
self.inner.on_set_interface_config(
tunnel_address_v4.to_string(),
tunnel_address_v6.to_string(),
search_domain.map(|s| s.to_string()),
dns_addresses,
route_list_4,
route_list_6,

View File

@@ -363,6 +363,7 @@ extension Adapter: CallbackHandlerDelegate {
public func onSetInterfaceConfig(
tunnelAddressIPv4: String,
tunnelAddressIPv6: String,
searchDomain: String?,
dnsAddresses: [String],
routeListv4: String,
routeListv6: String
@@ -393,6 +394,7 @@ extension Adapter: CallbackHandlerDelegate {
networkSettings.dnsAddresses = dnsAddresses
networkSettings.routes4 = routes4
networkSettings.routes6 = routes6
networkSettings.setSearchDomain(domain: searchDomain)
networkSettings.apply()
}
@@ -474,7 +476,7 @@ extension Adapter {
let semaphore = DispatchSemaphore(value: 0)
// Set tunnel's matchDomains to a dummy string that will never match any name
networkSettings.matchDomains = ["firezone-fd0020211111"]
networkSettings.setDummyMatchDomain()
// Call apply to populate /etc/resolv.conf with the system's default resolvers
networkSettings.apply {
@@ -484,7 +486,7 @@ extension Adapter {
resolvers = BindResolvers().getservers().map(BindResolvers.getnameinfo)
// Restore connlib's DNS resolvers
networkSettings.matchDomains = [""]
networkSettings.clearDummyMatchDomain()
networkSettings.apply { semaphore.signal() }
}

View File

@@ -20,6 +20,7 @@ public protocol CallbackHandlerDelegate: AnyObject {
func onSetInterfaceConfig(
tunnelAddressIPv4: String,
tunnelAddressIPv6: String,
searchDomain: String?,
dnsAddresses: [String],
routeListv4: String,
routeListv6: String
@@ -34,6 +35,7 @@ public class CallbackHandler {
func onSetInterfaceConfig(
tunnelAddressIPv4: RustString,
tunnelAddressIPv6: RustString,
searchDomain: RustString?,
dnsAddresses: RustString,
routeListv4: RustString,
routeListv6: RustString
@@ -43,6 +45,7 @@ public class CallbackHandler {
CallbackHandler.onSetInterfaceConfig:
IPv4: \(tunnelAddressIPv4.toString())
IPv6: \(tunnelAddressIPv6.toString())
SearchDomain: \(String(describing: (searchDomain?.toString())))
DNS: \(dnsAddresses.toString())
IPv4 routes: \(routeListv4.toString())
IPv6 routes: \(routeListv6.toString())
@@ -57,6 +60,7 @@ public class CallbackHandler {
delegate?.onSetInterfaceConfig(
tunnelAddressIPv4: tunnelAddressIPv4.toString(),
tunnelAddressIPv6: tunnelAddressIPv6.toString(),
searchDomain: searchDomain?.toString(),
dnsAddresses: dnsArray,
routeListv4: routeListv4.toString(),
routeListv6: routeListv6.toString()

View File

@@ -1,6 +1,6 @@
@_cdecl("__swift_bridge__$CallbackHandler$on_set_interface_config")
func __swift_bridge__CallbackHandler_on_set_interface_config (_ this: UnsafeMutableRawPointer, _ tunnelAddressIPv4: UnsafeMutableRawPointer, _ tunnelAddressIPv6: UnsafeMutableRawPointer, _ dnsAddresses: UnsafeMutableRawPointer, _ routeListv4: UnsafeMutableRawPointer, _ routeListv6: UnsafeMutableRawPointer) {
Unmanaged<CallbackHandler>.fromOpaque(this).takeUnretainedValue().onSetInterfaceConfig(tunnelAddressIPv4: RustString(ptr: tunnelAddressIPv4), tunnelAddressIPv6: RustString(ptr: tunnelAddressIPv6), dnsAddresses: RustString(ptr: dnsAddresses), routeListv4: RustString(ptr: routeListv4), routeListv6: RustString(ptr: routeListv6))
func __swift_bridge__CallbackHandler_on_set_interface_config (_ this: UnsafeMutableRawPointer, _ tunnelAddressIPv4: UnsafeMutableRawPointer, _ tunnelAddressIPv6: UnsafeMutableRawPointer, _ searchDomain: UnsafeMutableRawPointer?, _ dnsAddresses: UnsafeMutableRawPointer, _ routeListv4: UnsafeMutableRawPointer, _ routeListv6: UnsafeMutableRawPointer) {
Unmanaged<CallbackHandler>.fromOpaque(this).takeUnretainedValue().onSetInterfaceConfig(tunnelAddressIPv4: RustString(ptr: tunnelAddressIPv4), tunnelAddressIPv6: RustString(ptr: tunnelAddressIPv6), searchDomain: { let val = searchDomain; if val != nil { return RustString(ptr: val!) } else { return nil } }(), dnsAddresses: RustString(ptr: dnsAddresses), routeListv4: RustString(ptr: routeListv4), routeListv6: RustString(ptr: routeListv6))
}
@_cdecl("__swift_bridge__$CallbackHandler$on_update_resources")

View File

@@ -23,12 +23,36 @@ class NetworkSettings {
public var dnsAddresses: [String] = []
public var routes4: [NEIPv4Route] = []
public var routes6: [NEIPv6Route] = []
public var matchDomains: [String] = [""]
// Private to ensure we append the search domain if we set it.
private var matchDomains: [String] = [""]
private var searchDomains: [String] = [""]
init(packetTunnelProvider: PacketTunnelProvider?) {
self.packetTunnelProvider = packetTunnelProvider
}
func setSearchDomain(domain: String?) {
guard let domain = domain else {
self.matchDomains = [""]
self.searchDomains = [""]
return;
}
self.matchDomains = ["", domain]
self.searchDomains = [domain]
}
func setDummyMatchDomain() {
self.matchDomains = ["firezone-fd0020211111"]
}
func clearDummyMatchDomain() {
self.matchDomains = [""]
self.matchDomains.append(contentsOf: self.searchDomains)
}
func apply(completionHandler: (() -> Void)? = nil) {
// We don't really know the connlib gateway IP address at this point, but just using 127.0.0.1 is okay
// because the OS doesn't really need this IP address.
@@ -46,7 +70,8 @@ class NetworkSettings {
ipv4Settings.includedRoutes = routes4
ipv6Settings.includedRoutes = routes6
dnsSettings.matchDomains = matchDomains
dnsSettings.matchDomainsNoSearch = true
dnsSettings.searchDomains = searchDomains
dnsSettings.matchDomainsNoSearch = false
tunnelNetworkSettings.ipv4Settings = ipv4Settings
tunnelNetworkSettings.ipv6Settings = ipv6Settings
tunnelNetworkSettings.dnsSettings = dnsSettings