ui,tunnel: support DNS search domains

wg-quick has supported this for a while, but not the config layer, and
not the Go backend, so wire this all up.

Requested-by: Alexis Geoffrey <alexis.geoffrey97@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2021-09-25 22:22:09 -06:00
parent 32fc760053
commit 3935a369b8
7 changed files with 89 additions and 8 deletions

View File

@ -274,6 +274,9 @@ public final class GoBackend implements Backend {
for (final InetAddress addr : config.getInterface().getDnsServers()) for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress()); builder.addDnsServer(addr.getHostAddress());
for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains())
builder.addSearchDomain(dnsSearchDomain);
boolean sawDefaultRoute = false; boolean sawDefaultRoute = false;
for (final Peer peer : config.getPeers()) { for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps()) { for (final InetNetwork addr : peer.getAllowedIps()) {

View File

@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
public final class InetAddresses { public final class InetAddresses {
@Nullable private static final Method PARSER_METHOD; @Nullable private static final Method PARSER_METHOD;
private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$"); private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
static { static {
Method m = null; Method m = null;
@ -38,6 +39,16 @@ public final class InetAddresses {
private InetAddresses() { private InetAddresses() {
} }
/**
* Determines whether input is a valid DNS hostname.
*
* @param maybeHostname a string that is possibly a DNS hostname
* @return whether or not maybeHostname is a valid DNS hostname
*/
public static boolean isHostname(final CharSequence maybeHostname) {
return VALID_HOSTNAME.matcher(maybeHostname).matches();
}
/** /**
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups. * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
* *

View File

@ -40,6 +40,7 @@ public final class Interface {
private final Set<InetNetwork> addresses; private final Set<InetNetwork> addresses;
private final Set<InetAddress> dnsServers; private final Set<InetAddress> dnsServers;
private final Set<String> dnsSearchDomains;
private final Set<String> excludedApplications; private final Set<String> excludedApplications;
private final Set<String> includedApplications; private final Set<String> includedApplications;
private final KeyPair keyPair; private final KeyPair keyPair;
@ -50,6 +51,7 @@ public final class Interface {
// Defensively copy to ensure immutability even if the Builder is reused. // Defensively copy to ensure immutability even if the Builder is reused.
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses)); addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers)); dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
dnsSearchDomains = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsSearchDomains));
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications)); excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications)); includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key"); keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
@ -108,6 +110,7 @@ public final class Interface {
final Interface other = (Interface) obj; final Interface other = (Interface) obj;
return addresses.equals(other.addresses) return addresses.equals(other.addresses)
&& dnsServers.equals(other.dnsServers) && dnsServers.equals(other.dnsServers)
&& dnsSearchDomains.equals(other.dnsSearchDomains)
&& excludedApplications.equals(other.excludedApplications) && excludedApplications.equals(other.excludedApplications)
&& includedApplications.equals(other.includedApplications) && includedApplications.equals(other.includedApplications)
&& keyPair.equals(other.keyPair) && keyPair.equals(other.keyPair)
@ -135,6 +138,16 @@ public final class Interface {
return dnsServers; return dnsServers;
} }
/**
* Returns the set of DNS search domains associated with the interface.
*
* @return a set of strings
*/
public Set<String> getDnsSearchDomains() {
// The collection is already immutable.
return dnsSearchDomains;
}
/** /**
* Returns the set of applications excluded from using the interface. * Returns the set of applications excluded from using the interface.
* *
@ -222,6 +235,7 @@ public final class Interface {
sb.append("Address = ").append(Attribute.join(addresses)).append('\n'); sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
if (!dnsServers.isEmpty()) { if (!dnsServers.isEmpty()) {
final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList()); final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
dnsServerStrings.addAll(dnsSearchDomains);
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n'); sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
} }
if (!excludedApplications.isEmpty()) if (!excludedApplications.isEmpty())
@ -254,6 +268,8 @@ public final class Interface {
// Defaults to an empty set. // Defaults to an empty set.
private final Set<InetAddress> dnsServers = new LinkedHashSet<>(); private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
// Defaults to an empty set. // Defaults to an empty set.
private final Set<String> dnsSearchDomains = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<String> excludedApplications = new LinkedHashSet<>(); private final Set<String> excludedApplications = new LinkedHashSet<>();
// Defaults to an empty set. // Defaults to an empty set.
private final Set<String> includedApplications = new LinkedHashSet<>(); private final Set<String> includedApplications = new LinkedHashSet<>();
@ -284,6 +300,16 @@ public final class Interface {
return this; return this;
} }
public Builder addDnsSearchDomain(final String dnsSearchDomain) {
dnsSearchDomains.add(dnsSearchDomain);
return this;
}
public Builder addDnsSearchDomains(final Collection<String> dnsSearchDomains) {
this.dnsSearchDomains.addAll(dnsSearchDomains);
return this;
}
public Interface build() throws BadConfigException { public Interface build() throws BadConfigException {
if (keyPair == null) if (keyPair == null)
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
@ -326,8 +352,15 @@ public final class Interface {
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException { public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
try { try {
for (final String dnsServer : Attribute.split(dnsServers)) for (final String dnsServer : Attribute.split(dnsServers)) {
try {
addDnsServer(InetAddresses.parse(dnsServer)); addDnsServer(InetAddresses.parse(dnsServer));
} catch (final ParseException e) {
if (e.getParsingClass() != InetAddress.class || !InetAddresses.isHostname(dnsServer))
throw e;
addDnsSearchDomain(dnsServer);
}
}
return this; return this;
} catch (final ParseException e) { } catch (final ParseException e) {
throw new BadConfigException(Section.INTERFACE, Location.DNS, e); throw new BadConfigException(Section.INTERFACE, Location.DNS, e);

View File

@ -152,6 +152,12 @@ object BindingAdapters {
view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else "" view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
} }
@JvmStatic
@BindingAdapter("android:text")
fun setStringSetText(view: TextView, strings: Iterable<String?>?) {
view.text = if (strings != null) Attribute.join(strings) else ""
}
@JvmStatic @JvmStatic
fun tryParseInt(s: String?): Int { fun tryParseInt(s: String?): Int {
if (s == null) if (s == null)

View File

@ -81,7 +81,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
constructor(other: Interface) { constructor(other: Interface) {
addresses = Attribute.join(other.addresses) addresses = Attribute.join(other.addresses)
val dnsServerStrings = other.dnsServers.map { it.hostAddress } val dnsServerStrings = other.dnsServers.map { it.hostAddress }.plus(other.dnsSearchDomains)
dnsServers = Attribute.join(dnsServerStrings) dnsServers = Attribute.join(dnsServerStrings)
excludedApplications.addAll(other.excludedApplications) excludedApplications.addAll(other.excludedApplications)
includedApplications.addAll(other.includedApplications) includedApplications.addAll(other.includedApplications)

View File

@ -167,8 +167,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/dns_servers" android:contentDescription="@string/dns_servers"
android:nextFocusUp="@id/addresses_text" android:nextFocusUp="@id/addresses_text"
android:nextFocusDown="@id/listen_port_text" android:nextFocusDown="@id/dns_search_domains_text"
android:nextFocusForward="@id/listen_port_text" android:nextFocusForward="@id/dns_search_domains_text"
android:onClick="@{ClipboardUtils::copyTextView}" android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.dnsServers}" android:text="@{config.interface.dnsServers}"
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
@ -176,6 +176,33 @@
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
tools:text="8.8.8.8, 8.8.4.4" /> tools:text="8.8.8.8, 8.8.4.4" />
<TextView
android:id="@+id/dns_search_domains_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_search_domain_text"
android:text="@string/dns_search_domains"
android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
<TextView
android:id="@+id/dns_search_domains_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_search_domains"
android:nextFocusUp="@id/dns_servers_text"
android:nextFocusDown="@id/listen_port_text"
android:nextFocusForward="@id/listen_port_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.dnsSearchDomains}"
android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label"
tools:text="zx2c4.com" />
<TextView <TextView
android:id="@+id/listen_port_label" android:id="@+id/listen_port_label"
android:layout_width="0dp" android:layout_width="0dp"
@ -187,7 +214,7 @@
app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5" app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" />
<TextView <TextView
android:id="@+id/listen_port_text" android:id="@+id/listen_port_text"
@ -196,7 +223,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/listen_port" android:contentDescription="@string/listen_port"
android:nextFocusRight="@id/mtu_text" android:nextFocusRight="@id/mtu_text"
android:nextFocusUp="@id/dns_servers_text" android:nextFocusUp="@id/dns_search_domains_text"
android:nextFocusDown="@id/applications_text" android:nextFocusDown="@id/applications_text"
android:nextFocusForward="@id/mtu_text" android:nextFocusForward="@id/mtu_text"
android:onClick="@{ClipboardUtils::copyTextView}" android:onClick="@{ClipboardUtils::copyTextView}"
@ -220,7 +247,7 @@
app:layout_constraintHorizontal_weight="0.5" app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintLeft_toRightOf="@id/listen_port_label" app:layout_constraintLeft_toRightOf="@id/listen_port_label"
app:layout_constraintStart_toEndOf="@id/listen_port_label" app:layout_constraintStart_toEndOf="@id/listen_port_label"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" />
<TextView <TextView
android:id="@+id/mtu_text" android:id="@+id/mtu_text"

View File

@ -109,6 +109,7 @@
<string name="disable_config_export_title">Disable config exporting</string> <string name="disable_config_export_title">Disable config exporting</string>
<string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string> <string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string>
<string name="dns_servers">DNS servers</string> <string name="dns_servers">DNS servers</string>
<string name="dns_search_domains">Search domains</string>
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="endpoint">Endpoint</string> <string name="endpoint">Endpoint</string>
<string name="error_down">Error bringing down tunnel: %s</string> <string name="error_down">Error bringing down tunnel: %s</string>