diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java index dfe217a3..701b0715 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -274,6 +274,9 @@ public final class GoBackend implements Backend { for (final InetAddress addr : config.getInterface().getDnsServers()) builder.addDnsServer(addr.getHostAddress()); + for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains()) + builder.addSearchDomain(dnsSearchDomain); + boolean sawDefaultRoute = false; for (final Peer peer : config.getPeers()) { for (final InetNetwork addr : peer.getAllowedIps()) { diff --git a/tunnel/src/main/java/com/wireguard/config/InetAddresses.java b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java index 573c522d..32a8be9e 100644 --- a/tunnel/src/main/java/com/wireguard/config/InetAddresses.java +++ b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; public final class InetAddresses { @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 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 { Method m = null; @@ -38,6 +39,16 @@ public final class 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. * diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java index 01bb3699..1ece3b10 100644 --- a/tunnel/src/main/java/com/wireguard/config/Interface.java +++ b/tunnel/src/main/java/com/wireguard/config/Interface.java @@ -40,6 +40,7 @@ public final class Interface { private final Set addresses; private final Set dnsServers; + private final Set dnsSearchDomains; private final Set excludedApplications; private final Set includedApplications; private final KeyPair keyPair; @@ -50,6 +51,7 @@ public final class Interface { // Defensively copy to ensure immutability even if the Builder is reused. addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses)); dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers)); + dnsSearchDomains = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsSearchDomains)); excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications)); includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications)); keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key"); @@ -108,6 +110,7 @@ public final class Interface { final Interface other = (Interface) obj; return addresses.equals(other.addresses) && dnsServers.equals(other.dnsServers) + && dnsSearchDomains.equals(other.dnsSearchDomains) && excludedApplications.equals(other.excludedApplications) && includedApplications.equals(other.includedApplications) && keyPair.equals(other.keyPair) @@ -135,6 +138,16 @@ public final class Interface { return dnsServers; } + /** + * Returns the set of DNS search domains associated with the interface. + * + * @return a set of strings + */ + public Set getDnsSearchDomains() { + // The collection is already immutable. + return dnsSearchDomains; + } + /** * 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'); if (!dnsServers.isEmpty()) { final List dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList()); + dnsServerStrings.addAll(dnsSearchDomains); sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n'); } if (!excludedApplications.isEmpty()) @@ -254,6 +268,8 @@ public final class Interface { // Defaults to an empty set. private final Set dnsServers = new LinkedHashSet<>(); // Defaults to an empty set. + private final Set dnsSearchDomains = new LinkedHashSet<>(); + // Defaults to an empty set. private final Set excludedApplications = new LinkedHashSet<>(); // Defaults to an empty set. private final Set includedApplications = new LinkedHashSet<>(); @@ -284,6 +300,16 @@ public final class Interface { return this; } + public Builder addDnsSearchDomain(final String dnsSearchDomain) { + dnsSearchDomains.add(dnsSearchDomain); + return this; + } + + public Builder addDnsSearchDomains(final Collection dnsSearchDomains) { + this.dnsSearchDomains.addAll(dnsSearchDomains); + return this; + } + public Interface build() throws BadConfigException { if (keyPair == null) throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, @@ -326,8 +352,15 @@ public final class Interface { public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException { try { - for (final String dnsServer : Attribute.split(dnsServers)) - addDnsServer(InetAddresses.parse(dnsServer)); + for (final String dnsServer : Attribute.split(dnsServers)) { + try { + addDnsServer(InetAddresses.parse(dnsServer)); + } catch (final ParseException e) { + if (e.getParsingClass() != InetAddress.class || !InetAddresses.isHostname(dnsServer)) + throw e; + addDnsSearchDomain(dnsServer); + } + } return this; } catch (final ParseException e) { throw new BadConfigException(Section.INTERFACE, Location.DNS, e); diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt index adc42e7b..e5ff4bc9 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -152,6 +152,12 @@ object BindingAdapters { view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else "" } + @JvmStatic + @BindingAdapter("android:text") + fun setStringSetText(view: TextView, strings: Iterable?) { + view.text = if (strings != null) Attribute.join(strings) else "" + } + @JvmStatic fun tryParseInt(s: String?): Int { if (s == null) diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt index bd2a9831..16af043c 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt @@ -81,7 +81,7 @@ class InterfaceProxy : BaseObservable, Parcelable { constructor(other: Interface) { 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) excludedApplications.addAll(other.excludedApplications) includedApplications.addAll(other.includedApplications) diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index 16bc2ddb..8e34f082 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -167,8 +167,8 @@ android:layout_height="wrap_content" android:contentDescription="@string/dns_servers" android:nextFocusUp="@id/addresses_text" - android:nextFocusDown="@id/listen_port_text" - android:nextFocusForward="@id/listen_port_text" + android:nextFocusDown="@id/dns_search_domains_text" + android:nextFocusForward="@id/dns_search_domains_text" android:onClick="@{ClipboardUtils::copyTextView}" android:text="@{config.interface.dnsServers}" 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" tools:text="8.8.8.8, 8.8.4.4" /> + + + + + app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" /> + app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" /> Disable config exporting Disabling config exporting makes private keys less accessible DNS servers + Search domains Edit Endpoint Error bringing down tunnel: %s