diff options
Diffstat (limited to 'tunnel/src/main/java/com/wireguard/config/Interface.java')
-rw-r--r-- | tunnel/src/main/java/com/wireguard/config/Interface.java | 355 |
1 files changed, 355 insertions, 0 deletions
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; + } + } +} |