Skip to content

Commit

Permalink
Enable "split DNS" configurations for an interface
Browse files Browse the repository at this point in the history
By adding a tilde prefix to a domain name entry in the DNS= line, the
domain is interpreted as a "matching domain" for DNS routing instead of
a "search domain."  This corresponds to setting a non-empty
NEDNSSettings.matchDomains property for the network tunnel.  Using tilde
as a prefix is borrowed from systemd-resolved's equivalent usage.

If one or more match domains are specified, then the specified DNS
resolvers are only used for those matching domains instead of acting as
the first resolver before the system's primary DNS resolvers.

Signed-off-by: Stephen Larew <stephen@slarew.net>
  • Loading branch information
slarew authored and Stephen Larew committed Jun 27, 2021
1 parent 13b7204 commit 6ebc356
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 4 deletions.
5 changes: 5 additions & 0 deletions Sources/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
Expand Up @@ -136,6 +136,7 @@ extension TunnelConfiguration {
if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
var dnsLine = interface.dns.map { $0.stringRepresentation }
dnsLine.append(contentsOf: interface.dnsSearch)
dnsLine.append(contentsOf: interface.dnsMatchDomains.map { "~" + $0 })
let dnsString = dnsLine.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
Expand Down Expand Up @@ -191,15 +192,19 @@ extension TunnelConfiguration {
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
var dnsSearch = [String]()
var dnsMatchDomains = [String]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let dnsServer = DNSServer(from: dnsServerString) {
dnsServers.append(dnsServer)
} else if dnsServerString.first == "~" && !dnsServerString.dropFirst().isEmpty {
dnsMatchDomains.append(String(dnsServerString.dropFirst()))
} else {
dnsSearch.append(dnsServerString)
}
}
interface.dns = dnsServers
interface.dnsSearch = dnsSearch
interface.dnsMatchDomains = dnsMatchDomains
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else {
Expand Down
Expand Up @@ -75,6 +75,7 @@ extension TunnelConfiguration {
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
interfaceConfiguration?.dns = base?.interface.dns ?? []
interfaceConfiguration?.dnsSearch = base?.interface.dnsSearch ?? []
interfaceConfiguration?.dnsMatchDomains = base?.interface.dnsMatchDomains ?? []
interfaceConfiguration?.mtu = base?.interface.mtu

if let interfaceConfiguration = interfaceConfiguration {
Expand Down
7 changes: 6 additions & 1 deletion Sources/WireGuardApp/UI/TunnelViewModel.swift
Expand Up @@ -139,9 +139,10 @@ class TunnelViewModel {
if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu)
}
if !config.dns.isEmpty || !config.dnsSearch.isEmpty {
if !config.dns.isEmpty || !config.dnsSearch.isEmpty || !config.dnsMatchDomains.isEmpty {
var dns = config.dns.map { $0.stringRepresentation }
dns.append(contentsOf: config.dnsSearch)
dns.append(contentsOf: config.dnsMatchDomains.map { "~" + $0 })
scratchpad[.dns] = dns.joined(separator: ", ")
}
return scratchpad
Expand Down Expand Up @@ -197,15 +198,19 @@ class TunnelViewModel {
if let dnsString = scratchpad[.dns] {
var dnsServers = [DNSServer]()
var dnsSearch = [String]()
var dnsMatchDomains = [String]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let dnsServer = DNSServer(from: dnsServerString) {
dnsServers.append(dnsServer)
} else if dnsServerString.first == "~" && !dnsServerString.dropFirst().isEmpty {
dnsMatchDomains.append(String(dnsServerString.dropFirst()))
} else {
dnsSearch.append(dnsServerString)
}
}
config.dns = dnsServers
config.dnsSearch = dnsSearch
config.dnsMatchDomains = dnsMatchDomains
}

guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
Expand Down
9 changes: 8 additions & 1 deletion Sources/WireGuardApp/UI/macOS/View/highlighter.c
Expand Up @@ -121,6 +121,13 @@ static bool is_valid_hostname(string_span_t s)
return num_digit != num_entity;
}

static bool is_valid_dns_match_hostname(string_span_t s)
{
if (!s.len || s.s[0] != '~')
return false;
return is_valid_hostname((string_span_t){ s.s + 1, s.len - 1});
}

static bool is_valid_ipv4(string_span_t s)
{
for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
Expand Down Expand Up @@ -448,7 +455,7 @@ static void highlight_multivalue_value(struct highlight_span_array *ret, const s
case DNS:
if (is_valid_ipv4(s) || is_valid_ipv6(s))
append_highlight_span(ret, parent.s, s, HighlightIP);
else if (is_valid_hostname(s))
else if (is_valid_hostname(s) || is_valid_dns_match_hostname(s))
append_highlight_span(ret, parent.s, s, HighlightHost);
else
append_highlight_span(ret, parent.s, s, HighlightError);
Expand Down
4 changes: 3 additions & 1 deletion Sources/WireGuardKit/InterfaceConfiguration.swift
Expand Up @@ -11,6 +11,7 @@ public struct InterfaceConfiguration {
public var mtu: UInt16?
public var dns = [DNSServer]()
public var dnsSearch = [String]()
public var dnsMatchDomains = [String]()

public init(privateKey: PrivateKey) {
self.privateKey = privateKey
Expand All @@ -27,6 +28,7 @@ extension InterfaceConfiguration: Equatable {
lhs.listenPort == rhs.listenPort &&
lhs.mtu == rhs.mtu &&
lhs.dns == rhs.dns &&
lhs.dnsSearch == rhs.dnsSearch
lhs.dnsSearch == rhs.dnsSearch &&
lhs.dnsMatchDomains == rhs.dnsMatchDomains
}
}
7 changes: 6 additions & 1 deletion Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift
Expand Up @@ -88,7 +88,12 @@ class PacketTunnelSettingsGenerator {
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
dnsSettings.searchDomains = tunnelConfiguration.interface.dnsSearch
if !tunnelConfiguration.interface.dns.isEmpty {
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
if tunnelConfiguration.interface.dnsMatchDomains.isEmpty {
// All DNS queries must first go through the tunnel's DNS
dnsSettings.matchDomains = [""]
} else {
dnsSettings.matchDomains = tunnelConfiguration.interface.dnsMatchDomains
}
}
networkSettings.dnsSettings = dnsSettings
}
Expand Down

11 comments on commit 6ebc356

@mdriessen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also having trouble with my search domain not working when using a split tunnel. Is there any way I could test these changes? I don't have an Apple Developer account that can build this project. Is a build of this commit downloadable somewhere or has there been any progress for merging this into upstream?

@slarew
Copy link
Owner Author

@slarew slarew commented on 6ebc356 Oct 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will see if I can get a build to you. I haven't formally proposed this patch to upstream yet. It's been on my todo list for too long now.

@slarew
Copy link
Owner Author

@slarew slarew commented on 6ebc356 Oct 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I cannot build a version to publish for general distribution. Network Extensions packaged as app extensions cannot be distributed outside the app store due to Apple's limitations. If the network extension was packaged as a system extension, then I believe I could sign with my developer ID for general distribution. Alas, I don't have the time right now to patch the wireguard app to use the system extension feature. Moreover, that would bump the minimum system version to 10.15 which may be undesirable for upstream.

@mdriessen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for replying so quickly! I was also unable to build this project with the free developer account due to those extensions. The mailinglist WireGuard uses is also another hurdle to send in bug reports so I thought I'd reach out this way.

My only concern for this to be merged upstream is that it introduces a new configuration semantic; ~matchdomain. They will likely want to keep the configuration the same for all clients. I did some reading on this (can't test anything unfortunately) and I think the problem lies in this line like anothers have mentioned;

dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS

This works fine if I'm not using Split DNS (AllowedIPs = 0.0.0.0/0) but doesn't work when I only route some internal IP's. That's probably why someone else used this solution;

dnsSettings.matchDomains = [""] + dnsSettings.searchDomains // All DNS queries must first go through the tunnel's DNS

The solution I'll try to suggest to upstream is using the tunnel's DNS only when all traffic is routed through the tunnel or as a matchDomain when only some traffic is routed. I think that behaviour makes more sense without changing semantics. As far as I understand, the matchDomains will also be used as searchDomain (see https://developer.apple.com/documentation/networkextension/nednssettings/1406735-matchdomainsnosearch).

let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
if tunnelConfiguration.interface.addresses.contains("0.0.0.0/0") { // Not sure if this works in Swift
    dnsSettings.searchDomains = tunnelConfiguration.interface.dnsSearch
    if !tunnelConfiguration.interface.dns.isEmpty {
        dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
    }
} else if !tunnelConfiguration.interface.dns.isEmpty {
    dnsSettings.matchDomains = tunnelConfiguration.interface.dnsSearch // Use the tunnel's DNS only for the given domains
}

I'm not sure if this code works for both our use cases. Any suggestions before I try to post this on the mailinglist?

@mdriessen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed you posted the patch on the mailinglist. Thank you for trying to resolve this upstream! Much appreciated!

@mdriessen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to let you know, I posted a message to the mailinglist but it is still awaiting approval by a moderator. Those mailing lists are really a pain to use!


Hello Andrew,

I just want to chime in here and say that I think the current
implementation of search domains is simply not working the way it
should on the MacOS client.

My use case is pretty common, an internal DNS server that has entries
for internal servers. I defined a search domain in the WireGuard
configuration; DNS = 10.13.13.1 mydomain.internal. The search domain
is for convenience, so I can just use the servername instead of
servername.mydomain.internal. Now this works fine when I route all the
traffic through the VPN (AllowedIPs = 0.0.0.0/0) but the search domain
is completely ignored when I only route the traffic I need to
(AllowedIPs = 10.13.13.0/24 192.168.0.0/24).

I don't think this is a configuration error on my side. The DNS
responds fine when using servername.mydomain.internal. This problem is
even mentioned in the "WireGuard macOS & iOS TODO List"
9. matchDomains=[“”] doesn’t do what the documentation says.
Specifically, DNS servers are not used if allowed IPs isn’t 0.0.0.0/0.

The description isn't 100% accurate (or outdated), the DNS server is
used but the search domain isn't being set on the primary resolver.
Some have solved this issue by adding the search domains to the list
of matchDomains; dnsSettings.matchDomains = [""] +
dnsSettings.searchDomains. But that way the DNS server specified in
WireGuard is still the primary resolver for all DNS queries.

Here is a link on how OpenVPN handles this and I think it's how it
should work when not using AllowedIPs 0.0.0.0/0.
https://openvpn.net/faq/how-does-ios-interpret-pushed-dns-servers-and-search-domains/
On a split-tunnel, where redirect-gateway is not pushed by the server,
and at least one pushed DNS server is present:

  • route all DNS requests through pushed DNS server(s) if no added
    search domains.
  • route DNS requests for added search domains only, if at least one
    added search domain.

Yours sincerely,
Matty

@classicmac
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this gets added to the release version soon. Has anyone found a workaround in the interim?

@jaymefSO
Copy link

@jaymefSO jaymefSO commented on 6ebc356 Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the work that has been done here. I've recently started evaluating switching to Wireguard from OpenVPN and have run into the same issue with search domains on MacOS which is blocking me currently. It's now 2023 and it doesn't seem like this has ever been resolved, or am I wrong?

When routing a specific subnet through wireguard and DNS short names do not work, even if I add search domain suffix to wireguard config like x.x.x.x, company.internal for ex. as it's not added to the primary resolver. It does seem to work if I route all traffic through the VPN i.e. 0.0.0.0/0 (which is not ideal for our use case).

@mdriessen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s been a while since I ran into this issue, but I think no progress has been made since. This client isn’t really being actively maintained anymore.

I believe the search domains work when you activate the VPN connection with the wg-quick command. That command is part of the wireguard-tools package, installable with Homebrew.

@slarew
Copy link
Owner Author

@slarew slarew commented on 6ebc356 Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the most part, I don't actively use this fork anymore. I had hoped it would get merged into the main wireguard-apple project, but there wasn't a lot of movement on that project at the time. I haven't paid close attention to actual commit activity in wireguard-apple, but my observation from the wireguard mailing list is that a lot of iOS and macOS work is stalled.

@jaymefSO
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crappy, appreciate the updates. Maybe I will need to re-evaluate our internal procedures for using DNS/short names and implement a better strategy. Wireguard looks like a great alternative to OpenVPN but the MacOS client does not seem very robust to say the least.

Please sign in to comment.