aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tunnel/src/main/java/com/wireguard/config
diff options
context:
space:
mode:
Diffstat (limited to 'tunnel/src/main/java/com/wireguard/config')
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Attribute.java58
-rw-r--r--tunnel/src/main/java/com/wireguard/config/BadConfigException.java118
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Config.java221
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetAddresses.java71
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetEndpoint.java125
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetNetwork.java76
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Interface.java355
-rw-r--r--tunnel/src/main/java/com/wireguard/config/ParseException.java44
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Peer.java306
9 files changed, 1374 insertions, 0 deletions
diff --git a/tunnel/src/main/java/com/wireguard/config/Attribute.java b/tunnel/src/main/java/com/wireguard/config/Attribute.java
new file mode 100644
index 00000000..1e9e25f0
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Attribute.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import java9.util.Optional;
+
+public final class Attribute {
+ private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
+ private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
+
+ private final String key;
+ private final String value;
+
+ private Attribute(final String key, final String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public static String join(final Iterable<?> values) {
+ final Iterator<?> it = values.iterator();
+ if (!it.hasNext()) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ sb.append(it.next());
+ while (it.hasNext()) {
+ sb.append(", ");
+ sb.append(it.next());
+ }
+ return sb.toString();
+ }
+
+ public static Optional<Attribute> parse(final CharSequence line) {
+ final Matcher matcher = LINE_PATTERN.matcher(line);
+ if (!matcher.matches())
+ return Optional.empty();
+ return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
+ }
+
+ public static String[] split(final CharSequence value) {
+ return LIST_SEPARATOR.split(value);
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
new file mode 100644
index 00000000..6d41b065
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+import com.wireguard.crypto.KeyFormatException;
+
+public class BadConfigException extends Exception {
+ private final Location location;
+ private final Reason reason;
+ private final Section section;
+ @Nullable private final CharSequence text;
+
+ private BadConfigException(final Section section, final Location location,
+ final Reason reason, @Nullable final CharSequence text,
+ @Nullable final Throwable cause) {
+ super(cause);
+ this.section = section;
+ this.location = location;
+ this.reason = reason;
+ this.text = text;
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final Reason reason, @Nullable final CharSequence text) {
+ this(section, location, reason, text, null);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final KeyFormatException cause) {
+ this(section, location, Reason.INVALID_KEY, null, cause);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ @Nullable final CharSequence text,
+ final NumberFormatException cause) {
+ this(section, location, Reason.INVALID_NUMBER, text, cause);
+ }
+
+ public BadConfigException(final Section section, final Location location,
+ final ParseException cause) {
+ this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public Section getSection() {
+ return section;
+ }
+
+ @Nullable
+ public CharSequence getText() {
+ return text;
+ }
+
+ public enum Location {
+ TOP_LEVEL(""),
+ ADDRESS("Address"),
+ ALLOWED_IPS("AllowedIPs"),
+ DNS("DNS"),
+ ENDPOINT("Endpoint"),
+ EXCLUDED_APPLICATIONS("ExcludedApplications"),
+ LISTEN_PORT("ListenPort"),
+ MTU("MTU"),
+ PERSISTENT_KEEPALIVE("PersistentKeepalive"),
+ PRE_SHARED_KEY("PresharedKey"),
+ PRIVATE_KEY("PrivateKey"),
+ PUBLIC_KEY("PublicKey");
+
+ private final String name;
+
+ Location(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public enum Reason {
+ INVALID_KEY,
+ INVALID_NUMBER,
+ INVALID_VALUE,
+ MISSING_ATTRIBUTE,
+ MISSING_SECTION,
+ MISSING_VALUE,
+ SYNTAX_ERROR,
+ UNKNOWN_ATTRIBUTE,
+ UNKNOWN_SECTION
+ }
+
+ public enum Section {
+ CONFIG("Config"),
+ INTERFACE("Interface"),
+ PEER("Peer");
+
+ private final String name;
+
+ Section(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java
new file mode 100644
index 00000000..62651b08
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Config.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
+ * sections (combined together), and zero or more "Peer" sections (treated individually).
+ * <p>
+ * Instances of this class are immutable.
+ */
+public final class Config {
+ private final Interface interfaze;
+ private final List<Peer> peers;
+
+ private Config(final Builder builder) {
+ interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
+ }
+
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link BadConfigException} if the input is not well-formed or contains data that cannot
+ * be parsed.
+ *
+ * @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final InputStream stream)
+ throws IOException, BadConfigException {
+ return parse(new BufferedReader(new InputStreamReader(stream)));
+ }
+
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link BadConfigException} if the input is not well-formed or contains data that cannot
+ * be parsed.
+ *
+ * @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final BufferedReader reader)
+ throws IOException, BadConfigException {
+ final Builder builder = new Builder();
+ final Collection<String> interfaceLines = new ArrayList<>();
+ final Collection<String> peerLines = new ArrayList<>();
+ boolean inInterfaceSection = false;
+ boolean inPeerSection = false;
+ @Nullable String line;
+ while ((line = reader.readLine()) != null) {
+ final int commentIndex = line.indexOf('#');
+ if (commentIndex != -1)
+ line = line.substring(0, commentIndex);
+ line = line.trim();
+ if (line.isEmpty())
+ continue;
+ if (line.startsWith("[")) {
+ // Consume all [Peer] lines read so far.
+ if (inPeerSection) {
+ builder.parsePeer(peerLines);
+ peerLines.clear();
+ }
+ if ("[Interface]".equalsIgnoreCase(line)) {
+ inInterfaceSection = true;
+ inPeerSection = false;
+ } else if ("[Peer]".equalsIgnoreCase(line)) {
+ inInterfaceSection = false;
+ inPeerSection = true;
+ } else {
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.UNKNOWN_SECTION, line);
+ }
+ } else if (inInterfaceSection) {
+ interfaceLines.add(line);
+ } else if (inPeerSection) {
+ peerLines.add(line);
+ } else {
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.UNKNOWN_SECTION, line);
+ }
+ }
+ if (inPeerSection)
+ builder.parsePeer(peerLines);
+ else if (!inInterfaceSection)
+ throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
+ Reason.MISSING_SECTION, null);
+ // Combine all [Interface] sections in the file.
+ builder.parseInterface(interfaceLines);
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Config))
+ return false;
+ final Config other = (Config) obj;
+ return interfaze.equals(other.interfaze) && peers.equals(other.peers);
+ }
+
+ /**
+ * Returns the interface section of the configuration.
+ *
+ * @return the interface configuration
+ */
+ public Interface getInterface() {
+ return interfaze;
+ }
+
+ /**
+ * Returns a list of the configuration's peer sections.
+ *
+ * @return a list of {@link Peer}s
+ */
+ public List<Peer> getPeers() {
+ return peers;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * interfaze.hashCode() + peers.hashCode();
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
+ * is identified by its interface's public key and the number of peers it has.
+ *
+ * @return a concise single-line identifier for the {@code Config}
+ */
+ @Override
+ public String toString() {
+ return "(Config " + interfaze + " (" + peers.size() + " peers))";
+ }
+
+ /**
+ * Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
+ * configuration file.
+ *
+ * @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("[Interface]\n").append(interfaze.toWgQuickString());
+ for (final Peer peer : peers)
+ sb.append("\n[Peer]\n").append(peer.toWgQuickString());
+ return sb.toString();
+ }
+
+ /**
+ * Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
+ *
+ * @return the {@code Config} represented as a series of "key=value" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(interfaze.toWgUserspaceString());
+ sb.append("replace_peers=true\n");
+ for (final Peer peer : peers)
+ sb.append(peer.toWgUserspaceString());
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set<Peer> peers = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private Interface interfaze;
+
+ public Builder addPeer(final Peer peer) {
+ peers.add(peer);
+ return this;
+ }
+
+ public Builder addPeers(final Collection<Peer> peers) {
+ this.peers.addAll(peers);
+ return this;
+ }
+
+ public Config build() {
+ if (interfaze == null)
+ throw new IllegalArgumentException("An [Interface] section is required");
+ return new Config(this);
+ }
+
+ public Builder parseInterface(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ return setInterface(Interface.parse(lines));
+ }
+
+ public Builder parsePeer(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ return addPeer(Peer.parse(lines));
+ }
+
+ public Builder setInterface(final Interface interfaze) {
+ this.interfaze = interfaze;
+ return this;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetAddresses.java b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
new file mode 100644
index 00000000..5303e27f
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for creating instances of {@link InetAddress}.
+ */
+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]?))$");
+
+ static {
+ Method m = null;
+ try {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
+ // noinspection JavaReflectionMemberAccess
+ m = InetAddress.class.getMethod("parseNumericAddress", String.class);
+ } catch (final Exception ignored) {
+ }
+ PARSER_METHOD = m;
+ }
+
+ private InetAddresses() { }
+
+ /**
+ * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
+ *
+ * @param address a string representing the IP address
+ * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
+ */
+ public static InetAddress parse(final String address) throws ParseException {
+ if (address.isEmpty())
+ throw new ParseException(InetAddress.class, address, "Empty address");
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
+ return android.net.InetAddresses.parseNumericAddress(address);
+ else if (PARSER_METHOD != null)
+ return (InetAddress) PARSER_METHOD.invoke(null, address);
+ else
+ throw new NoSuchMethodException("parseNumericAddress");
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException(InetAddress.class, address, e);
+ } catch (final Exception e) {
+ final Throwable cause = e.getCause();
+ // Re-throw parsing exceptions with the original type, as callers might try to catch
+ // them. On the other hand, callers cannot be expected to handle reflection failures.
+ if (cause instanceof IllegalArgumentException)
+ throw new ParseException(InetAddress.class, address, cause);
+ try {
+ if (WONT_TOUCH_RESOLVER.matcher(address).matches())
+ return InetAddress.getByName(address);
+ else
+ throw new ParseException(InetAddress.class, address, "Not an IP address");
+ } catch (final UnknownHostException f) {
+ throw new ParseException(InetAddress.class, address, f);
+ }
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
new file mode 100644
index 00000000..a442258e
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+import java9.util.Optional;
+
+
+/**
+ * An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
+ * <p>
+ * Instances of this class are externally immutable.
+ */
+public final class InetEndpoint {
+ private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
+ private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
+
+ private final String host;
+ private final boolean isResolved;
+ private final Object lock = new Object();
+ private final int port;
+ private Instant lastResolution = Instant.EPOCH;
+ @Nullable private InetEndpoint resolved;
+
+ private InetEndpoint(final String host, final boolean isResolved, final int port) {
+ this.host = host;
+ this.isResolved = isResolved;
+ this.port = port;
+ }
+
+ public static InetEndpoint parse(final String endpoint) throws ParseException {
+ if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
+ throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
+ final URI uri;
+ try {
+ uri = new URI("wg://" + endpoint);
+ } catch (final URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ if (uri.getPort() < 0 || uri.getPort() > 65535)
+ throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
+ try {
+ InetAddresses.parse(uri.getHost());
+ // Parsing ths host as a numeric address worked, so we don't need to do DNS lookups.
+ return new InetEndpoint(uri.getHost(), true, uri.getPort());
+ } catch (final ParseException ignored) {
+ // Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN.
+ return new InetEndpoint(uri.getHost(), false, uri.getPort());
+ }
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof InetEndpoint))
+ return false;
+ final InetEndpoint other = (InetEndpoint) obj;
+ return host.equals(other.host) && port == other.port;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
+ * to a numeric address. If the host is already numeric, the existing instance may be returned.
+ * Because this function may perform network I/O, it must not be called from the main thread.
+ *
+ * @return the resolved endpoint, or {@link Optional#empty()}
+ */
+ public Optional<InetEndpoint> getResolved() {
+ if (isResolved)
+ return Optional.of(this);
+ synchronized (lock) {
+ //TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
+ if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
+ try {
+ // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
+ final InetAddress[] candidates = InetAddress.getAllByName(host);
+ InetAddress address = candidates[0];
+ for (final InetAddress candidate : candidates) {
+ if (candidate instanceof Inet4Address) {
+ address = candidate;
+ break;
+ }
+ }
+ resolved = new InetEndpoint(address.getHostAddress(), true, port);
+ lastResolution = Instant.now();
+ } catch (final UnknownHostException e) {
+ resolved = null;
+ }
+ }
+ return Optional.ofNullable(resolved);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return host.hashCode() ^ port;
+ }
+
+ @Override
+ public String toString() {
+ final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
+ return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
new file mode 100644
index 00000000..f89322fd
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * An Internet network, denoted by its address and netmask
+ * <p>
+ * Instances of this class are immutable.
+ */
+public final class InetNetwork {
+ private final InetAddress address;
+ private final int mask;
+
+ private InetNetwork(final InetAddress address, final int mask) {
+ this.address = address;
+ this.mask = mask;
+ }
+
+ public static InetNetwork parse(final String network) throws ParseException {
+ final int slash = network.lastIndexOf('/');
+ final String maskString;
+ final int rawMask;
+ final String rawAddress;
+ if (slash >= 0) {
+ maskString = network.substring(slash + 1);
+ try {
+ rawMask = Integer.parseInt(maskString, 10);
+ } catch (final NumberFormatException ignored) {
+ throw new ParseException(Integer.class, maskString);
+ }
+ rawAddress = network.substring(0, slash);
+ } else {
+ maskString = "";
+ rawMask = -1;
+ rawAddress = network;
+ }
+ final InetAddress address = InetAddresses.parse(rawAddress);
+ final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
+ if (rawMask > maxMask)
+ throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
+ final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
+ return new InetNetwork(address, mask);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof InetNetwork))
+ return false;
+ final InetNetwork other = (InetNetwork) obj;
+ return address.equals(other.address) && mask == other.mask;
+ }
+
+ public InetAddress getAddress() {
+ return address;
+ }
+
+ public int getMask() {
+ return mask;
+ }
+
+ @Override
+ public int hashCode() {
+ return address.hashCode() ^ mask;
+ }
+
+ @Override
+ public String toString() {
+ return address.getHostAddress() + '/' + mask;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java
new file mode 100644
index 00000000..54944424
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Interface.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+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.Set;
+
+import java9.util.Lists;
+import java9.util.Optional;
+import java9.util.stream.Collectors;
+import java9.util.stream.StreamSupport;
+
+/**
+ * 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.
+ * <p>
+ * 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<InetNetwork> addresses;
+ private final Set<InetAddress> dnsServers;
+ private final Set<String> excludedApplications;
+ private final KeyPair keyPair;
+ private final Optional<Integer> listenPort;
+ private final Optional<Integer> mtu;
+
+ 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));
+ keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
+ listenPort = builder.listenPort;
+ mtu = builder.mtu;
+ }
+
+ /**
+ * 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<? extends CharSequence> 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 "listenport":
+ builder.parseListenPort(attribute.getValue());
+ break;
+ case "mtu":
+ builder.parseMtu(attribute.getValue());
+ break;
+ case "privatekey":
+ builder.parsePrivateKey(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)
+ && keyPair.equals(other.keyPair)
+ && listenPort.equals(other.listenPort)
+ && mtu.equals(other.mtu);
+ }
+
+ /**
+ * Returns the set of IP addresses assigned to the interface.
+ *
+ * @return a set of {@link InetNetwork}s
+ */
+ public Set<InetNetwork> 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<InetAddress> 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<String> getExcludedApplications() {
+ // The collection is already immutable.
+ return excludedApplications;
+ }
+
+ /**
+ * 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<Integer> getListenPort() {
+ return listenPort;
+ }
+
+ /**
+ * Returns the MTU used for the WireGuard interface.
+ *
+ * @return the MTU, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Integer> getMtu() {
+ return mtu;
+ }
+
+ @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 + keyPair.hashCode();
+ hash = 31 * hash + listenPort.hashCode();
+ hash = 31 * hash + mtu.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<String> dnsServerStrings = StreamSupport.stream(dnsServers)
+ .map(InetAddress::getHostAddress)
+ .collect(Collectors.toUnmodifiableList());
+ sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
+ }
+ if (!excludedApplications.isEmpty())
+ sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
+ listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
+ mtu.ifPresent(m -> sb.append("MTU = ").append(m).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'));
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set<InetNetwork> addresses = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<String> excludedApplications = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private KeyPair keyPair;
+ // Defaults to not present.
+ private Optional<Integer> listenPort = Optional.empty();
+ // Defaults to not present.
+ private Optional<Integer> mtu = Optional.empty();
+
+ public Builder addAddress(final InetNetwork address) {
+ addresses.add(address);
+ return this;
+ }
+
+ public Builder addAddresses(final Collection<InetNetwork> addresses) {
+ this.addresses.addAll(addresses);
+ return this;
+ }
+
+ public Builder addDnsServer(final InetAddress dnsServer) {
+ dnsServers.add(dnsServer);
+ return this;
+ }
+
+ public Builder addDnsServers(final Collection<? extends InetAddress> 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);
+ return new Interface(this);
+ }
+
+ public Builder excludeApplication(final String application) {
+ excludedApplications.add(application);
+ return this;
+ }
+
+ public Builder excludeApplications(final Collection<String> applications) {
+ excludedApplications.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(Lists.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 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.LISTEN_PORT,
+ Reason.INVALID_VALUE, String.valueOf(mtu));
+ this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
+ return this;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/ParseException.java b/tunnel/src/main/java/com/wireguard/config/ParseException.java
new file mode 100644
index 00000000..c79d1fa1
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/ParseException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+/**
+ */
+public class ParseException extends Exception {
+ private final Class<?> parsingClass;
+ private final CharSequence text;
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final String message, @Nullable final Throwable cause) {
+ super(message, cause);
+ this.parsingClass = parsingClass;
+ this.text = text;
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final String message) {
+ this(parsingClass, text, message, null);
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text,
+ @Nullable final Throwable cause) {
+ this(parsingClass, text, null, cause);
+ }
+
+ public ParseException(final Class<?> parsingClass, final CharSequence text) {
+ this(parsingClass, text, null, null);
+ }
+
+ public Class<?> getParsingClass() {
+ return parsingClass;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java
new file mode 100644
index 00000000..37fcfa69
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/Peer.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import androidx.annotation.Nullable;
+
+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 java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+
+import java9.util.Optional;
+
+/**
+ * Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
+ * and may optionally have several other attributes.
+ * <p>
+ * Instances of this class are immutable.
+ */
+public final class Peer {
+ private final Set<InetNetwork> allowedIps;
+ private final Optional<InetEndpoint> endpoint;
+ private final Optional<Integer> persistentKeepalive;
+ private final Optional<Key> preSharedKey;
+ private final Key publicKey;
+
+ private Peer(final Builder builder) {
+ // Defensively copy to ensure immutability even if the Builder is reused.
+ allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
+ endpoint = builder.endpoint;
+ persistentKeepalive = builder.persistentKeepalive;
+ preSharedKey = builder.preSharedKey;
+ publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
+ }
+
+ /**
+ * Parses an series of "KEY = VALUE" lines into a {@code Peer}. 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 public key attribute
+ * @return a {@code Peer} with all of its attributes set from {@code lines}
+ */
+ public static Peer parse(final Iterable<? extends CharSequence> lines)
+ throws BadConfigException {
+ final Builder builder = new Builder();
+ for (final CharSequence line : lines) {
+ final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
+ new BadConfigException(Section.PEER, Location.TOP_LEVEL,
+ Reason.SYNTAX_ERROR, line));
+ switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
+ case "allowedips":
+ builder.parseAllowedIPs(attribute.getValue());
+ break;
+ case "endpoint":
+ builder.parseEndpoint(attribute.getValue());
+ break;
+ case "persistentkeepalive":
+ builder.parsePersistentKeepalive(attribute.getValue());
+ break;
+ case "presharedkey":
+ builder.parsePreSharedKey(attribute.getValue());
+ break;
+ case "publickey":
+ builder.parsePublicKey(attribute.getValue());
+ break;
+ default:
+ throw new BadConfigException(Section.PEER, Location.TOP_LEVEL,
+ Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Peer))
+ return false;
+ final Peer other = (Peer) obj;
+ return allowedIps.equals(other.allowedIps)
+ && endpoint.equals(other.endpoint)
+ && persistentKeepalive.equals(other.persistentKeepalive)
+ && preSharedKey.equals(other.preSharedKey)
+ && publicKey.equals(other.publicKey);
+ }
+
+ /**
+ * Returns the peer's set of allowed IPs.
+ *
+ * @return the set of allowed IPs
+ */
+ public Set<InetNetwork> getAllowedIps() {
+ // The collection is already immutable.
+ return allowedIps;
+ }
+
+ /**
+ * Returns the peer's endpoint.
+ *
+ * @return the endpoint, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<InetEndpoint> getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Returns the peer's persistent keepalive.
+ *
+ * @return the persistent keepalive, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Integer> getPersistentKeepalive() {
+ return persistentKeepalive;
+ }
+
+ /**
+ * Returns the peer's pre-shared key.
+ *
+ * @return the pre-shared key, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<Key> getPreSharedKey() {
+ return preSharedKey;
+ }
+
+ /**
+ * Returns the peer's public key.
+ *
+ * @return the public key
+ */
+ public Key getPublicKey() {
+ return publicKey;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 1;
+ hash = 31 * hash + allowedIps.hashCode();
+ hash = 31 * hash + endpoint.hashCode();
+ hash = 31 * hash + persistentKeepalive.hashCode();
+ hash = 31 * hash + preSharedKey.hashCode();
+ hash = 31 * hash + publicKey.hashCode();
+ return hash;
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
+ * identified by its public key and (if known) its endpoint.
+ *
+ * @return a concise single-line identifier for the {@code Peer}
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("(Peer ");
+ sb.append(publicKey.toBase64());
+ endpoint.ifPresent(ep -> sb.append(" @").append(ep));
+ sb.append(')');
+ return sb.toString();
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
+ * configuration file.
+ *
+ * @return the {@code Peer} represented as a series of "Key = Value" lines
+ */
+ public String toWgQuickString() {
+ final StringBuilder sb = new StringBuilder();
+ if (!allowedIps.isEmpty())
+ sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
+ endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
+ persistentKeepalive.ifPresent(pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
+ preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
+ sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
+ return sb.toString();
+ }
+
+ /**
+ * Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
+ * that not all attributes are included in this representation.
+ *
+ * @return the {@code Peer} represented as a series of "key=value" lines
+ */
+ public String toWgUserspaceString() {
+ final StringBuilder sb = new StringBuilder();
+ // The order here is important: public_key signifies the beginning of a new peer.
+ sb.append("public_key=").append(publicKey.toHex()).append('\n');
+ for (final InetNetwork allowedIp : allowedIps)
+ sb.append("allowed_ip=").append(allowedIp).append('\n');
+ endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
+ preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
+ return sb.toString();
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // See wg(8)
+ private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
+
+ // Defaults to an empty set.
+ private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
+ // Defaults to not present.
+ private Optional<InetEndpoint> endpoint = Optional.empty();
+ // Defaults to not present.
+ private Optional<Integer> persistentKeepalive = Optional.empty();
+ // Defaults to not present.
+ private Optional<Key> preSharedKey = Optional.empty();
+ // No default; must be provided before building.
+ @Nullable private Key publicKey;
+
+ public Builder addAllowedIp(final InetNetwork allowedIp) {
+ allowedIps.add(allowedIp);
+ return this;
+ }
+
+ public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
+ this.allowedIps.addAll(allowedIps);
+ return this;
+ }
+
+ public Peer build() throws BadConfigException {
+ if (publicKey == null)
+ throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY,
+ Reason.MISSING_ATTRIBUTE, null);
+ return new Peer(this);
+ }
+
+ public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
+ try {
+ for (final String allowedIp : Attribute.split(allowedIps))
+ addAllowedIp(InetNetwork.parse(allowedIp));
+ return this;
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.PEER, Location.ALLOWED_IPS, e);
+ }
+ }
+
+ public Builder parseEndpoint(final String endpoint) throws BadConfigException {
+ try {
+ return setEndpoint(InetEndpoint.parse(endpoint));
+ } catch (final ParseException e) {
+ throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
+ }
+ }
+
+ public Builder parsePersistentKeepalive(final String persistentKeepalive)
+ throws BadConfigException {
+ try {
+ return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
+ } catch (final NumberFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
+ persistentKeepalive, e);
+ }
+ }
+
+ public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
+ try {
+ return setPreSharedKey(Key.fromBase64(preSharedKey));
+ } catch (final KeyFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
+ }
+ }
+
+ public Builder parsePublicKey(final String publicKey) throws BadConfigException {
+ try {
+ return setPublicKey(Key.fromBase64(publicKey));
+ } catch (final KeyFormatException e) {
+ throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
+ }
+ }
+
+ public Builder setEndpoint(final InetEndpoint endpoint) {
+ this.endpoint = Optional.of(endpoint);
+ return this;
+ }
+
+ public Builder setPersistentKeepalive(final int persistentKeepalive)
+ throws BadConfigException {
+ if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
+ throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
+ Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
+ this.persistentKeepalive = persistentKeepalive == 0 ?
+ Optional.empty() : Optional.of(persistentKeepalive);
+ return this;
+ }
+
+ public Builder setPreSharedKey(final Key preSharedKey) {
+ this.preSharedKey = Optional.of(preSharedKey);
+ return this;
+ }
+
+ public Builder setPublicKey(final Key publicKey) {
+ this.publicKey = publicKey;
+ return this;
+ }
+ }
+}