/* * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.config; import com.wireguard.config.BadConfigException.Location; import com.wireguard.config.BadConfigException.Reason; import com.wireguard.config.BadConfigException.Section; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; import com.wireguard.crypto.KeyPair; import java.net.InetAddress; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import androidx.annotation.Nullable; /** * Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must * have a private key (used to initialize a {@code KeyPair}), and may optionally have several other * attributes. *

* Instances of this class are immutable. */ public final class Interface { private static final int MAX_UDP_PORT = 65535; private static final int MIN_UDP_PORT = 0; private final Set addresses; private final Set dnsServers; private final Set excludedApplications; private final Set includedApplications; private final KeyPair keyPair; private final Optional listenPort; private final Optional mtu; private final Optional jc; private final Optional jmin; private final Optional jmax; private final Optional s1; private final Optional s2; private final Optional h1; private final Optional h2; private final Optional h3; private final Optional h4; private Interface(final Builder builder) { // 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)); 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"); listenPort = builder.listenPort; mtu = builder.mtu; jc = builder.jc; jmax = builder.jmax; jmin = builder.jmin; s1 = builder.s1; s2 = builder.s2; h1 = builder.h1; h2 = builder.h2; h3 = builder.h3; h4 = builder.h4; } /** * Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws * {@link ParseException} if the input is not well-formed or contains unknown attributes. * * @param lines An iterable sequence of lines, containing at least a private key attribute * @return An {@code Interface} with all of the attributes from {@code lines} set */ public static Interface parse(final Iterable lines) throws BadConfigException { final Builder builder = new Builder(); for (final CharSequence line : lines) { final Attribute attribute = Attribute.parse(line).orElseThrow(() -> new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL, Reason.SYNTAX_ERROR, line)); switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) { case "address": builder.parseAddresses(attribute.getValue()); break; case "dns": builder.parseDnsServers(attribute.getValue()); break; case "excludedapplications": builder.parseExcludedApplications(attribute.getValue()); break; case "includedapplications": builder.parseIncludedApplications(attribute.getValue()); break; case "listenport": builder.parseListenPort(attribute.getValue()); break; case "mtu": builder.parseMtu(attribute.getValue()); break; case "privatekey": builder.parsePrivateKey(attribute.getValue()); break; case "jc": builder.parseJc(attribute.getValue()); break; case "jmin": builder.parseJmin(attribute.getValue()); break; case "jmax": builder.parseJmax(attribute.getValue()); break; case "s1": builder.parseS1(attribute.getValue()); break; case "s2": builder.parseS2(attribute.getValue()); break; case "h1": builder.parseH1(attribute.getValue()); break; case "h2": builder.parseH2(attribute.getValue()); break; case "h3": builder.parseH3(attribute.getValue()); break; case "h4": builder.parseH4(attribute.getValue()); break; default: throw new BadConfigException( Section.INTERFACE, Location.TOP_LEVEL, Reason.UNKNOWN_ATTRIBUTE, attribute.getKey()); } } return builder.build(); } @Override public boolean equals(final Object obj) { if (!(obj instanceof Interface)) return false; final Interface other = (Interface) obj; return addresses.equals(other.addresses) && dnsServers.equals(other.dnsServers) && excludedApplications.equals(other.excludedApplications) && includedApplications.equals(other.includedApplications) && keyPair.equals(other.keyPair) && listenPort.equals(other.listenPort) && mtu.equals(other.mtu) && jc.equals(other.jc) && jmin.equals(other.jmin) && jmax.equals(other.jmax) && s1.equals(other.s1) && s2.equals(other.s2) && h1.equals(other.h1) && h2.equals(other.h2) && h3.equals(other.h3) && h4.equals(other.h4); } /** * Returns the set of IP addresses assigned to the interface. * * @return a set of {@link InetNetwork}s */ public Set getAddresses() { // The collection is already immutable. return addresses; } /** * Returns the set of DNS servers associated with the interface. * * @return a set of {@link InetAddress}es */ public Set getDnsServers() { // The collection is already immutable. return dnsServers; } /** * Returns the set of applications excluded from using the interface. * * @return a set of package names */ public Set getExcludedApplications() { // The collection is already immutable. return excludedApplications; } /** * Returns the set of applications included exclusively for using the interface. * * @return a set of package names */ public Set getIncludedApplications() { // The collection is already immutable. return includedApplications; } /** * Returns the public/private key pair used by the interface. * * @return a key pair */ public KeyPair getKeyPair() { return keyPair; } /** * Returns the UDP port number that the WireGuard interface will listen on. * * @return a UDP port number, or {@code Optional.empty()} if none is configured */ public Optional getListenPort() { return listenPort; } /** * Returns the MTU used for the WireGuard interface. * * @return the MTU, or {@code Optional.empty()} if none is configured */ public Optional getMtu() { return mtu; } public Optional getJc() { return jc; } public Optional getJmin() { return jmin; } public Optional getJmax() { return jmax; } public Optional getS1() { return s1; } public Optional getS2() { return s2; } public Optional getH1() { return h1; } public Optional getH2() { return h2; } public Optional getH3() { return h3; } public Optional getH4() { return h4; } @Override public int hashCode() { int hash = 1; hash = 31 * hash + addresses.hashCode(); hash = 31 * hash + dnsServers.hashCode(); hash = 31 * hash + excludedApplications.hashCode(); hash = 31 * hash + includedApplications.hashCode(); hash = 31 * hash + keyPair.hashCode(); hash = 31 * hash + listenPort.hashCode(); hash = 31 * hash + mtu.hashCode(); hash = 31 * hash + jc.hashCode(); hash = 31 * hash + jmin.hashCode(); hash = 31 * hash + jmax.hashCode(); hash = 31 * hash + s1.hashCode(); hash = 31 * hash + s2.hashCode(); hash = 31 * hash + h1.hashCode(); hash = 31 * hash + h2.hashCode(); hash = 31 * hash + h3.hashCode(); hash = 31 * hash + h4.hashCode(); return hash; } /** * Converts the {@code Interface} into a string suitable for debugging purposes. The {@code * Interface} is identified by its public key and (if set) the port used for its UDP socket. * * @return A concise single-line identifier for the {@code Interface} */ @Override public String toString() { final StringBuilder sb = new StringBuilder("(Interface "); sb.append(keyPair.getPublicKey().toBase64()); listenPort.ifPresent(lp -> sb.append(" @").append(lp)); sb.append(')'); return sb.toString(); } /** * Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick} * configuration file. * * @return The {@code Interface} represented as a series of "Key = Value" lines */ public String toWgQuickString() { final StringBuilder sb = new StringBuilder(); if (!addresses.isEmpty()) sb.append("Address = ").append(Attribute.join(addresses)).append('\n'); if (!dnsServers.isEmpty()) { final List dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList()); sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n'); } if (!excludedApplications.isEmpty()) sb.append("ExcludedApplications = ") .append(Attribute.join(excludedApplications)) .append('\n'); if (!includedApplications.isEmpty()) sb.append("IncludedApplications = ") .append(Attribute.join(includedApplications)) .append('\n'); listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n')); mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n')); jc.ifPresent(t_jc -> sb.append("Jc = ").append(t_jc).append('\n')); jmin.ifPresent(t_jmin -> sb.append("Jmin = ").append(t_jmin).append('\n')); jmax.ifPresent(t_jmax -> sb.append("Jmax = ").append(t_jmax).append('\n')); s1.ifPresent(t_s1 -> sb.append("S1 = ").append(t_s1).append('\n')); s2.ifPresent(t_s2 -> sb.append("S2 = ").append(t_s2).append('\n')); h1.ifPresent(t_h1 -> sb.append("H1 = ").append(t_h1).append('\n')); h2.ifPresent(t_h2 -> sb.append("H2 = ").append(t_h2).append('\n')); h3.ifPresent(t_h3 -> sb.append("H3 = ").append(t_h3).append('\n')); h4.ifPresent(t_h4 -> sb.append("H4 = ").append(t_h4).append('\n')); sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n'); return sb.toString(); } /** * Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API. * Note that not all attributes are included in this representation. * * @return the {@code Interface} represented as a series of "KEY=VALUE" lines */ public String toWgUserspaceString() { final StringBuilder sb = new StringBuilder(); sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n'); listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n')); jc.ifPresent(t_jc -> sb.append("jc=").append(t_jc).append('\n')); jmin.ifPresent(t_jmin -> sb.append("jmin=").append(t_jmin).append('\n')); jmax.ifPresent(t_jmax -> sb.append("jmax=").append(t_jmax).append('\n')); s1.ifPresent(t_s1 -> sb.append("s1=").append(t_s1).append('\n')); s2.ifPresent(t_s2 -> sb.append("s2=").append(t_s2).append('\n')); h1.ifPresent(t_h1 -> sb.append("h1=").append(t_h1).append('\n')); h2.ifPresent(t_h2 -> sb.append("h2=").append(t_h2).append('\n')); h3.ifPresent(t_h3 -> sb.append("h3=").append(t_h3).append('\n')); h4.ifPresent(t_h4 -> sb.append("h4=").append(t_h4).append('\n')); return sb.toString(); } @SuppressWarnings("UnusedReturnValue") public static final class Builder { // Defaults to an empty set. private final Set addresses = new LinkedHashSet<>(); // Defaults to an empty set. private final Set dnsServers = new LinkedHashSet<>(); // Defaults to an empty set. private final Set excludedApplications = new LinkedHashSet<>(); // Defaults to an empty set. private final Set includedApplications = new LinkedHashSet<>(); // No default; must be provided before building. @Nullable private KeyPair keyPair; // Defaults to not present. private Optional listenPort = Optional.empty(); // Defaults to not present. private Optional mtu = Optional.empty(); private Optional jc = Optional.empty(); private Optional jmin = Optional.empty(); private Optional jmax = Optional.empty(); private Optional s1 = Optional.empty(); private Optional s2 = Optional.empty(); private Optional h1 = Optional.empty(); private Optional h2 = Optional.empty(); private Optional h3 = Optional.empty(); private Optional h4 = Optional.empty(); public Builder addAddress(final InetNetwork address) { addresses.add(address); return this; } public Builder addAddresses(final Collection addresses) { this.addresses.addAll(addresses); return this; } public Builder addDnsServer(final InetAddress dnsServer) { dnsServers.add(dnsServer); return this; } public Builder addDnsServers(final Collection dnsServers) { this.dnsServers.addAll(dnsServers); return this; } public Interface build() throws BadConfigException { if (keyPair == null) throw new BadConfigException( Section.INTERFACE, Location.PRIVATE_KEY, Reason.MISSING_ATTRIBUTE, null); if (!includedApplications.isEmpty() && !excludedApplications.isEmpty()) throw new BadConfigException( Section.INTERFACE, Location.INCLUDED_APPLICATIONS, Reason.INVALID_KEY, null); return new Interface(this); } public Builder excludeApplication(final String application) { excludedApplications.add(application); return this; } public Builder excludeApplications(final Collection applications) { excludedApplications.addAll(applications); return this; } public Builder includeApplication(final String application) { includedApplications.add(application); return this; } public Builder includeApplications(final Collection applications) { includedApplications.addAll(applications); return this; } public Builder parseAddresses(final CharSequence addresses) throws BadConfigException { try { for (final String address : Attribute.split(addresses)) addAddress(InetNetwork.parse(address)); return this; } catch (final ParseException e) { throw new BadConfigException(Section.INTERFACE, Location.ADDRESS, e); } } public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException { try { for (final String dnsServer : Attribute.split(dnsServers)) addDnsServer(InetAddresses.parse(dnsServer)); return this; } catch (final ParseException e) { throw new BadConfigException(Section.INTERFACE, Location.DNS, e); } } public Builder parseExcludedApplications(final CharSequence apps) { return excludeApplications(List.of(Attribute.split(apps))); } public Builder parseIncludedApplications(final CharSequence apps) { return includeApplications(List.of(Attribute.split(apps))); } public Builder parseListenPort(final String listenPort) throws BadConfigException { try { return setListenPort(Integer.parseInt(listenPort)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e); } } public Builder parseMtu(final String mtu) throws BadConfigException { try { return setMtu(Integer.parseInt(mtu)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e); } } public Builder parseJc(final String jc) throws BadConfigException { try { return setJc(Integer.parseInt(jc)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.JC, jc, e); } } public Builder parseJmax(final String jmax) throws BadConfigException { try { return setJmax(Integer.parseInt(jmax)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.JMAX, jmax, e); } } public Builder parseJmin(final String jmin) throws BadConfigException { try { return setJmin(Integer.parseInt(jmin)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.JMIN, jmin, e); } } public Builder parseS1(final String s1) throws BadConfigException { try { return setS1(Integer.parseInt(s1)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.S1, s1, e); } } public Builder parseS2(final String s2) throws BadConfigException { try { return setS2(Integer.parseInt(s2)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.S2, s2, e); } } public Builder parseH1(final String h1) throws BadConfigException { try { return setH1(Long.parseLong(h1)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.H1, h1, e); } } public Builder parseH2(final String h2) throws BadConfigException { try { return setH2(Long.parseLong(h2)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.H2, h2, e); } } public Builder parseH3(final String h3) throws BadConfigException { try { return setH3(Long.parseLong(h3)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.H3, h3, e); } } public Builder parseH4(final String h4) throws BadConfigException { try { return setH4(Long.parseLong(h4)); } catch (final NumberFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.H4, h4, e); } } public Builder parsePrivateKey(final String privateKey) throws BadConfigException { try { return setKeyPair(new KeyPair(Key.fromBase64(privateKey))); } catch (final KeyFormatException e) { throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e); } } public Builder setKeyPair(final KeyPair keyPair) { this.keyPair = keyPair; return this; } public Builder setListenPort(final int listenPort) throws BadConfigException { if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT) throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, Reason.INVALID_VALUE, String.valueOf(listenPort)); this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort); return this; } public Builder setMtu(final int mtu) throws BadConfigException { if (mtu < 0) throw new BadConfigException( Section.INTERFACE, Location.MTU, Reason.INVALID_VALUE, String.valueOf(mtu)); this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu); return this; } public Builder setJc(final int jc) throws BadConfigException { if (jc < 0) throw new BadConfigException( Section.INTERFACE, Location.JC, Reason.INVALID_VALUE, String.valueOf(jc)); this.jc = Optional.of(jc); return this; } public Builder setJmin(final int jmin) throws BadConfigException { if (jmin < 0) throw new BadConfigException( Section.INTERFACE, Location.JMIN, Reason.INVALID_VALUE, String.valueOf(jmin)); this.jmin = Optional.of(jmin); return this; } public Builder setJmax(final int jmax) throws BadConfigException { if (jmax < 0) throw new BadConfigException( Section.INTERFACE, Location.JMAX, Reason.INVALID_VALUE, String.valueOf(jmax)); this.jmax = Optional.of(jmax); return this; } public Builder setS1(final int s1) throws BadConfigException { if (s1 < 0) throw new BadConfigException( Section.INTERFACE, Location.S1, Reason.INVALID_VALUE, String.valueOf(s1)); this.s1 = Optional.of(s1); return this; } public Builder setS2(final int s2) throws BadConfigException { if (s2 < 0) throw new BadConfigException( Section.INTERFACE, Location.S2, Reason.INVALID_VALUE, String.valueOf(s2)); this.s2 = Optional.of(s2); return this; } public Builder setH1(final long h1) throws BadConfigException { if (h1 < 0) throw new BadConfigException( Section.INTERFACE, Location.H1, Reason.INVALID_VALUE, String.valueOf(h1)); this.h1 = Optional.of(h1); return this; } public Builder setH2(final long h2) throws BadConfigException { if (h2 < 0) throw new BadConfigException( Section.INTERFACE, Location.H2, Reason.INVALID_VALUE, String.valueOf(h2)); this.h2 = Optional.of(h2); return this; } public Builder setH3(final long h3) throws BadConfigException { if (h3 < 0) throw new BadConfigException( Section.INTERFACE, Location.H3, Reason.INVALID_VALUE, String.valueOf(h3)); this.h3 = Optional.of(h3); return this; } public Builder setH4(final long h4) throws BadConfigException { if (h4 < 0) throw new BadConfigException( Section.INTERFACE, Location.H4, Reason.INVALID_VALUE, String.valueOf(h4)); this.h4 = Optional.of(h4); return this; } } }