Remodel the Model
- The configuration and crypto model is now entirely independent of Android classes other than Nullable and TextUtils. - Model classes are immutable and use builders that enforce the appropriate optional/required attributes. - The Android config proxies (for Parcelable and databinding) are moved to the Android side of the codebase, and are designed to be safe for two-way databinding. This allows proper observability in TunnelDetailFragment. - Various robustness fixes and documentation updates to helper classes. Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
committed by
Jason A. Donenfeld
parent
a264f7ab36
commit
d1e85633fb
@@ -1,94 +1,49 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* The set of valid attributes for an interface or peer in a WireGuard configuration file.
|
||||
*/
|
||||
import java9.util.Optional;
|
||||
|
||||
public enum Attribute {
|
||||
ADDRESS("Address"),
|
||||
ALLOWED_IPS("AllowedIPs"),
|
||||
DNS("DNS"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
ENDPOINT("Endpoint"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
PRESHARED_KEY("PresharedKey"),
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
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 static final String[] EMPTY_LIST = new String[0];
|
||||
private static final Map<String, Attribute> KEY_MAP;
|
||||
private static final Pattern LIST_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
|
||||
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
static {
|
||||
KEY_MAP = new HashMap<>(Attribute.values().length);
|
||||
for (final Attribute key : Attribute.values()) {
|
||||
KEY_MAP.put(key.token.toLowerCase(), key);
|
||||
}
|
||||
private Attribute(final String key, final String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private final Pattern pattern;
|
||||
private final String token;
|
||||
|
||||
Attribute(final String token) {
|
||||
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
||||
this.token = token;
|
||||
public static String join(final Iterable<?> values) {
|
||||
return TextUtils.join(", ", values);
|
||||
}
|
||||
|
||||
public static <T> String iterableToString(final Iterable<T> iterable) {
|
||||
return TextUtils.join(", ", iterable);
|
||||
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)));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute match(final CharSequence line) {
|
||||
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
|
||||
public static String[] split(final CharSequence value) {
|
||||
return LIST_SEPARATOR.split(value);
|
||||
}
|
||||
|
||||
public static String[] stringToList(@Nullable final String string) {
|
||||
if (TextUtils.isEmpty(string))
|
||||
return EMPTY_LIST;
|
||||
return LIST_SEPARATOR_PATTERN.split(string.trim());
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String composeWith(@Nullable final Object value) {
|
||||
return String.format("%s = %s%n", token, value);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String composeWith(final int value) {
|
||||
return String.format("%s = %d%n", token, value);
|
||||
}
|
||||
|
||||
public <T> String composeWith(final Iterable<T> value) {
|
||||
return String.format("%s = %s%n", token, iterableToString(value));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String parse(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String[] parseList(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? stringToList(matcher.group(1)) : null;
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,170 +5,193 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.ObservableArrayList;
|
||||
import android.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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 a wg-quick configuration file, its name, and its connection state.
|
||||
* 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;
|
||||
|
||||
public class Config {
|
||||
private final Interface interfaceSection = new Interface();
|
||||
private List<Peer> peers = new ArrayList<>();
|
||||
|
||||
public static Config from(final String string) throws IOException {
|
||||
return from(new BufferedReader(new StringReader(string)));
|
||||
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));
|
||||
}
|
||||
|
||||
public static Config from(final InputStream stream) throws IOException {
|
||||
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
public static Config from(final BufferedReader reader) throws IOException {
|
||||
final Config config = new Config();
|
||||
final Context context = Application.get();
|
||||
Peer currentPeer = null;
|
||||
String line;
|
||||
boolean inInterfaceSection = false;
|
||||
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 ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = null;
|
||||
inInterfaceSection = true;
|
||||
} else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = new Peer();
|
||||
config.peers.add(currentPeer);
|
||||
inInterfaceSection = false;
|
||||
} else if (inInterfaceSection) {
|
||||
config.interfaceSection.parse(line);
|
||||
} else if (currentPeer != null) {
|
||||
currentPeer.parse(line);
|
||||
} else {
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, line));
|
||||
/**
|
||||
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
|
||||
* {@link ParseException} if the input is not well-formed or contains unparseable sections.
|
||||
*
|
||||
* @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration file
|
||||
* @return a {@code Config} instance representing the supplied configuration
|
||||
*/
|
||||
public static Config parse(final InputStream stream) throws IOException, ParseException {
|
||||
final Builder builder = new Builder();
|
||||
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
|
||||
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 ParseException("top level", line, "Unknown section name");
|
||||
}
|
||||
} else if (inInterfaceSection) {
|
||||
interfaceLines.add(line);
|
||||
} else if (inPeerSection) {
|
||||
peerLines.add(line);
|
||||
} else {
|
||||
throw new ParseException("top level", line, "Expected [Interface] or [Peer]");
|
||||
}
|
||||
}
|
||||
if (inPeerSection)
|
||||
builder.parsePeer(peerLines);
|
||||
else if (!inInterfaceSection)
|
||||
throw new ParseException("top level", "", "Empty configuration");
|
||||
// Combine all [Interface] sections in the file.
|
||||
builder.parseInterface(interfaceLines);
|
||||
}
|
||||
if (!inInterfaceSection && currentPeer == null) {
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
|
||||
}
|
||||
return config;
|
||||
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 interfaceSection;
|
||||
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() {
|
||||
final StringBuilder sb = new StringBuilder().append(interfaceSection);
|
||||
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').append(peer);
|
||||
sb.append("\n[Peer]\n").append(peer.toWgQuickString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
private final Interface.Observable observableInterface;
|
||||
private final ObservableList<Peer.Observable> observablePeers;
|
||||
@Nullable private String name;
|
||||
@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 Observable(@Nullable final Config parent, @Nullable final String name) {
|
||||
this.name = name;
|
||||
|
||||
observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
if (parent != null) {
|
||||
for (final Peer peer : parent.getPeers())
|
||||
observablePeers.add(new Peer.Observable(peer));
|
||||
}
|
||||
public Builder addPeer(final Peer peer) {
|
||||
peers.add(peer);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
name = in.readString();
|
||||
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
|
||||
public Builder addPeers(final Collection<Peer> peers) {
|
||||
this.peers.addAll(peers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void commitData(final Config parent) {
|
||||
observableInterface.commitData(parent.interfaceSection);
|
||||
final List<Peer> newPeers = new ArrayList<>(observablePeers.size());
|
||||
for (final Peer.Observable observablePeer : observablePeers) {
|
||||
final Peer peer = new Peer();
|
||||
observablePeer.commitData(peer);
|
||||
newPeers.add(peer);
|
||||
}
|
||||
parent.peers = newPeers;
|
||||
notifyChange();
|
||||
public Config build() {
|
||||
return new Config(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
public Builder parseInterface(final Iterable<? extends CharSequence> lines) throws ParseException {
|
||||
return setInterface(Interface.parse(lines));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Interface.Observable getInterfaceSection() {
|
||||
return observableInterface;
|
||||
public Builder parsePeer(final Iterable<? extends CharSequence> lines) throws ParseException {
|
||||
return addPeer(Peer.parse(lines));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name == null ? "" : name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public ObservableList<Peer.Observable> getPeers() {
|
||||
return observablePeers;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(observableInterface, flags);
|
||||
dest.writeTypedList(observablePeers);
|
||||
public Builder setInterface(final Interface interfaze) {
|
||||
this.interfaze = interfaze;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,22 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
/**
|
||||
* Utility methods for creating instances of {@link InetAddress}.
|
||||
*/
|
||||
public final class InetAddresses {
|
||||
private static final Method PARSER_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
// This method is only present on Android.
|
||||
// noinspection JavaReflectionMemberAccess
|
||||
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -30,13 +31,23 @@ public final class InetAddresses {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
public static InetAddress parse(@Nullable final String address) {
|
||||
if (address == null || address.isEmpty())
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_inetaddress));
|
||||
/**
|
||||
* 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) {
|
||||
if (address.isEmpty())
|
||||
throw new IllegalArgumentException("Empty address");
|
||||
try {
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() == null ? e : e.getCause());
|
||||
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.
|
||||
throw cause instanceof IllegalArgumentException ?
|
||||
(IllegalArgumentException) cause : new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,68 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import org.threeten.bp.Duration;
|
||||
import org.threeten.bp.Instant;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
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("[/?#]");
|
||||
|
||||
public class InetEndpoint {
|
||||
private final String host;
|
||||
private final boolean isResolved;
|
||||
private final Object lock = new Object();
|
||||
private final int port;
|
||||
@Nullable private InetAddress resolvedHost;
|
||||
private Instant lastResolution = Instant.EPOCH;
|
||||
@Nullable private InetEndpoint resolved;
|
||||
|
||||
public InetEndpoint(@Nullable final String endpoint) {
|
||||
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_forbidden_endpoint_chars));
|
||||
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) {
|
||||
if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
|
||||
throw new IllegalArgumentException("Forbidden characters in Endpoint");
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
host = uri.getHost();
|
||||
port = uri.getPort();
|
||||
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 IllegalArgumentException 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() {
|
||||
@@ -45,28 +77,47 @@ public class InetEndpoint {
|
||||
return port;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String getResolvedEndpoint() throws UnknownHostException {
|
||||
if (resolvedHost == null) {
|
||||
final InetAddress[] candidates = InetAddress.getAllByName(host);
|
||||
if (candidates.length == 0)
|
||||
throw new UnknownHostException(host);
|
||||
for (final InetAddress addr : candidates) {
|
||||
if (addr instanceof Inet4Address) {
|
||||
resolvedHost = addr;
|
||||
break;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
if (resolvedHost == null)
|
||||
resolvedHost = candidates[0];
|
||||
return Optional.ofNullable(resolved);
|
||||
}
|
||||
return String.format(resolvedHost instanceof Inet6Address ?
|
||||
"[%s]:%d" : "%s:%d", resolvedHost.getHostAddress(), port);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String getEndpoint() {
|
||||
return String.format(host.contains(":") && !host.contains("[") ?
|
||||
"[%s]:%d" : "%s:%d", host, port);
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,36 @@ package com.wireguard.config;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public class InetNetwork {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public InetNetwork(final String input) {
|
||||
final int slash = input.lastIndexOf('/');
|
||||
private InetNetwork(final InetAddress address, final int mask) {
|
||||
this.address = address;
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public static InetNetwork parse(final String network) {
|
||||
final int slash = network.lastIndexOf('/');
|
||||
final int rawMask;
|
||||
final String rawAddress;
|
||||
if (slash >= 0) {
|
||||
rawMask = Integer.parseInt(input.substring(slash + 1), 10);
|
||||
rawAddress = input.substring(0, slash);
|
||||
rawMask = Integer.parseInt(network.substring(slash + 1), 10);
|
||||
rawAddress = network.substring(0, slash);
|
||||
} else {
|
||||
rawMask = -1;
|
||||
rawAddress = input;
|
||||
rawAddress = network;
|
||||
}
|
||||
address = InetAddresses.parse(rawAddress);
|
||||
final InetAddress address = InetAddresses.parse(rawAddress);
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
return new InetNetwork(address, mask);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,7 +44,7 @@ public class InetNetwork {
|
||||
if (!(obj instanceof InetNetwork))
|
||||
return false;
|
||||
final InetNetwork other = (InetNetwork) obj;
|
||||
return Objects.equals(address, other.address) && mask == other.mask;
|
||||
return address.equals(other.address) && mask == other.mask;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
|
||||
@@ -5,395 +5,345 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.crypto.Keypair;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyPair;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
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.Stream;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block).
|
||||
* 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;
|
||||
|
||||
public class Interface {
|
||||
private final List<InetNetwork> addressList;
|
||||
private final Context context = Application.get();
|
||||
private final List<InetAddress> dnsList;
|
||||
private final List<String> excludedApplications;
|
||||
@Nullable private Keypair keypair;
|
||||
private int listenPort;
|
||||
private int mtu;
|
||||
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;
|
||||
|
||||
public Interface() {
|
||||
addressList = new ArrayList<>();
|
||||
dnsList = new ArrayList<>();
|
||||
excludedApplications = new ArrayList<>();
|
||||
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;
|
||||
}
|
||||
|
||||
private void addAddresses(@Nullable final String[] addresses) {
|
||||
if (addresses != null && addresses.length > 0) {
|
||||
for (final String addr : addresses) {
|
||||
if (addr.isEmpty())
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_empty_interface_address));
|
||||
addressList.add(new InetNetwork(addr));
|
||||
/**
|
||||
* 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 ParseException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute = Attribute.parse(line)
|
||||
.orElseThrow(() -> new ParseException("[Interface]", line, "Syntax error"));
|
||||
switch (attribute.getKey().toLowerCase()) {
|
||||
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 ParseException("[Interface]", attribute.getKey(), "Unknown attribute");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addDnses(@Nullable final String[] dnses) {
|
||||
if (dnses != null && dnses.length > 0) {
|
||||
for (final String dns : dnses) {
|
||||
dnsList.add(InetAddresses.parse(dns));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addExcludedApplications(@Nullable final String[] applications) {
|
||||
if (applications != null && applications.length > 0) {
|
||||
excludedApplications.addAll(Arrays.asList(applications));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAddressString() {
|
||||
if (addressList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(addressList);
|
||||
}
|
||||
|
||||
public InetNetwork[] getAddresses() {
|
||||
return addressList.toArray(new InetNetwork[addressList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDnsString() {
|
||||
if (dnsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(getDnsStrings());
|
||||
}
|
||||
|
||||
private List<String> getDnsStrings() {
|
||||
final List<String> strings = new ArrayList<>();
|
||||
for (final InetAddress addr : dnsList)
|
||||
strings.add(addr.getHostAddress());
|
||||
return strings;
|
||||
}
|
||||
|
||||
public InetAddress[] getDnses() {
|
||||
return dnsList.toArray(new InetAddress[dnsList.size()]);
|
||||
}
|
||||
|
||||
public String[] getExcludedApplications() {
|
||||
return excludedApplications.toArray(new String[excludedApplications.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getExcludedApplicationsString() {
|
||||
if (excludedApplications.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(excludedApplications);
|
||||
}
|
||||
|
||||
public int getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getListenPortString() {
|
||||
if (listenPort == 0)
|
||||
return null;
|
||||
return Integer.valueOf(listenPort).toString();
|
||||
}
|
||||
|
||||
public int getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMtuString() {
|
||||
if (mtu == 0)
|
||||
return null;
|
||||
return Integer.toString(mtu);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPrivateKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPrivateKey();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPublicKey();
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(String.format(context.getString(R.string.tunnel_error_interface_parse_failed), line));
|
||||
switch (key) {
|
||||
case ADDRESS:
|
||||
addAddresses(key.parseList(line));
|
||||
break;
|
||||
case DNS:
|
||||
addDnses(key.parseList(line));
|
||||
break;
|
||||
case EXCLUDED_APPLICATIONS:
|
||||
addExcludedApplications(key.parseList(line));
|
||||
break;
|
||||
case LISTEN_PORT:
|
||||
setListenPortString(key.parse(line));
|
||||
break;
|
||||
case MTU:
|
||||
setMtuString(key.parse(line));
|
||||
break;
|
||||
case PRIVATE_KEY:
|
||||
setPrivateKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAddressString(@Nullable final String addressString) {
|
||||
addressList.clear();
|
||||
addAddresses(Attribute.stringToList(addressString));
|
||||
}
|
||||
|
||||
private void setDnsString(@Nullable final String dnsString) {
|
||||
dnsList.clear();
|
||||
addDnses(Attribute.stringToList(dnsString));
|
||||
}
|
||||
|
||||
private void setExcludedApplicationsString(@Nullable final String applicationsString) {
|
||||
excludedApplications.clear();
|
||||
addExcludedApplications(Attribute.stringToList(applicationsString));
|
||||
}
|
||||
|
||||
private void setListenPort(final int listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
}
|
||||
|
||||
private void setListenPortString(@Nullable final String port) {
|
||||
if (port != null && !port.isEmpty())
|
||||
setListenPort(Integer.parseInt(port, 10));
|
||||
else
|
||||
setListenPort(0);
|
||||
}
|
||||
|
||||
private void setMtu(final int mtu) {
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
private void setMtuString(@Nullable final String mtu) {
|
||||
if (mtu != null && !mtu.isEmpty())
|
||||
setMtu(Integer.parseInt(mtu, 10));
|
||||
else
|
||||
setMtu(0);
|
||||
}
|
||||
|
||||
private void setPrivateKey(@Nullable String privateKey) {
|
||||
if (privateKey != null && privateKey.isEmpty())
|
||||
privateKey = null;
|
||||
keypair = privateKey == null ? null : new Keypair(privateKey);
|
||||
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().append("[Interface]\n");
|
||||
if (!addressList.isEmpty())
|
||||
sb.append(Attribute.ADDRESS.composeWith(addressList));
|
||||
if (!dnsList.isEmpty())
|
||||
sb.append(Attribute.DNS.composeWith(getDnsStrings()));
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications));
|
||||
if (listenPort != 0)
|
||||
sb.append(Attribute.LISTEN_PORT.composeWith(listenPort));
|
||||
if (mtu != 0)
|
||||
sb.append(Attribute.MTU.composeWith(mtu));
|
||||
if (keypair != null)
|
||||
sb.append(Attribute.PRIVATE_KEY.composeWith(keypair.getPrivateKey()));
|
||||
final StringBuilder sb = new StringBuilder("(Interface ");
|
||||
sb.append(keyPair.getPublicKey().toBase64());
|
||||
listenPort.ifPresent(lp -> sb.append(" @").append(lp));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String addresses;
|
||||
@Nullable private String dnses;
|
||||
@Nullable private String excludedApplications;
|
||||
@Nullable private String listenPort;
|
||||
@Nullable private String mtu;
|
||||
@Nullable private String privateKey;
|
||||
@Nullable private String publicKey;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
public Observable(@Nullable final Interface parent) {
|
||||
if (parent != null)
|
||||
loadData(parent);
|
||||
@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;
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
addresses = in.readString();
|
||||
dnses = in.readString();
|
||||
publicKey = in.readString();
|
||||
privateKey = in.readString();
|
||||
listenPort = in.readString();
|
||||
mtu = in.readString();
|
||||
excludedApplications = in.readString();
|
||||
public Builder addAddresses(final Collection<InetNetwork> addresses) {
|
||||
this.addresses.addAll(addresses);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void commitData(final Interface parent) {
|
||||
parent.setAddressString(addresses);
|
||||
parent.setDnsString(dnses);
|
||||
parent.setExcludedApplicationsString(excludedApplications);
|
||||
parent.setPrivateKey(privateKey);
|
||||
parent.setListenPortString(listenPort);
|
||||
parent.setMtuString(mtu);
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
public Builder addDnsServer(final InetAddress dnsServer) {
|
||||
dnsServers.add(dnsServer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
|
||||
this.dnsServers.addAll(dnsServers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void generateKeypair() {
|
||||
final Keypair keypair = new Keypair();
|
||||
privateKey = keypair.getPrivateKey();
|
||||
publicKey = keypair.getPublicKey();
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
public Interface build() {
|
||||
return new Interface(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getAddresses() {
|
||||
return addresses;
|
||||
public Builder excludeApplication(final String application) {
|
||||
excludedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getDnses() {
|
||||
return dnses;
|
||||
public Builder excludeApplications(final Collection<String> applications) {
|
||||
excludedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getExcludedApplications() {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public int getExcludedApplicationsCount() {
|
||||
return Attribute.stringToList(excludedApplications).length;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Interface parent) {
|
||||
addresses = parent.getAddressString();
|
||||
dnses = parent.getDnsString();
|
||||
excludedApplications = parent.getExcludedApplicationsString();
|
||||
publicKey = parent.getPublicKey();
|
||||
privateKey = parent.getPrivateKey();
|
||||
listenPort = parent.getListenPortString();
|
||||
mtu = parent.getMtuString();
|
||||
}
|
||||
|
||||
public void setAddresses(final String addresses) {
|
||||
this.addresses = addresses;
|
||||
notifyPropertyChanged(BR.addresses);
|
||||
}
|
||||
|
||||
public void setDnses(final String dnses) {
|
||||
this.dnses = dnses;
|
||||
notifyPropertyChanged(BR.dnses);
|
||||
}
|
||||
|
||||
public void setExcludedApplications(final String excludedApplications) {
|
||||
this.excludedApplications = excludedApplications;
|
||||
notifyPropertyChanged(BR.excludedApplications);
|
||||
notifyPropertyChanged(BR.excludedApplicationsCount);
|
||||
}
|
||||
|
||||
public void setListenPort(final String listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
notifyPropertyChanged(BR.listenPort);
|
||||
}
|
||||
|
||||
public void setMtu(final String mtu) {
|
||||
this.mtu = mtu;
|
||||
notifyPropertyChanged(BR.mtu);
|
||||
}
|
||||
|
||||
public void setPrivateKey(final String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
|
||||
public Builder parseAddresses(final CharSequence addresses) throws ParseException {
|
||||
try {
|
||||
publicKey = new Keypair(privateKey).getPublicKey();
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
publicKey = "";
|
||||
final List<InetNetwork> parsed = Stream.of(Attribute.split(addresses))
|
||||
.map(InetNetwork::parse)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
return addAddresses(parsed);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("Address", addresses, e);
|
||||
}
|
||||
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(addresses);
|
||||
dest.writeString(dnses);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeString(privateKey);
|
||||
dest.writeString(listenPort);
|
||||
dest.writeString(mtu);
|
||||
dest.writeString(excludedApplications);
|
||||
public Builder parseDnsServers(final CharSequence dnsServers) throws ParseException {
|
||||
try {
|
||||
final List<InetAddress> parsed = Stream.of(Attribute.split(dnsServers))
|
||||
.map(InetAddresses::parse)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
return addDnsServers(parsed);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("DNS", dnsServers, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseExcludedApplications(final CharSequence apps) throws ParseException {
|
||||
try {
|
||||
return excludeApplications(Lists.of(Attribute.split(apps)));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("ExcludedApplications", apps, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseListenPort(final String listenPort) throws ParseException {
|
||||
try {
|
||||
return setListenPort(Integer.parseInt(listenPort));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("ListenPort", listenPort, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseMtu(final String mtu) throws ParseException {
|
||||
try {
|
||||
return setMtu(Integer.parseInt(mtu));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("MTU", mtu, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePrivateKey(final String privateKey) throws ParseException {
|
||||
try {
|
||||
return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
|
||||
} catch (final Key.KeyFormatException e) {
|
||||
throw new ParseException("PrivateKey", "(omitted)", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setKeyPair(final KeyPair keyPair) {
|
||||
this.keyPair = keyPair;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setListenPort(final int listenPort) {
|
||||
if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
|
||||
throw new IllegalArgumentException("ListenPort must be a valid UDP port number");
|
||||
this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMtu(final int mtu) {
|
||||
if (mtu < 0)
|
||||
throw new IllegalArgumentException("MTU must not be negative");
|
||||
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
/**
|
||||
* An exception representing a failure to parse an element of a WireGuard configuration. The context
|
||||
* for this failure can be retrieved with {@link #getContext}, and the text that failed to parse can
|
||||
* be retrieved with {@link #getText}.
|
||||
*/
|
||||
public class ParseException extends Exception {
|
||||
private final String context;
|
||||
private final CharSequence text;
|
||||
|
||||
public ParseException(final String context, final CharSequence text, final String message) {
|
||||
super(message);
|
||||
this.context = context;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ParseException(final String context, final CharSequence text, final Throwable cause) {
|
||||
super(cause.getMessage(), cause);
|
||||
this.context = context;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ParseException(final String context, final CharSequence text) {
|
||||
this.context = context;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -5,363 +5,291 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.lang.Iterables;
|
||||
import java9.util.Optional;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block).
|
||||
* 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;
|
||||
|
||||
public class Peer {
|
||||
private final List<InetNetwork> allowedIPsList;
|
||||
private final Context context = Application.get();
|
||||
@Nullable private InetEndpoint endpoint;
|
||||
private int persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
|
||||
public Peer() {
|
||||
allowedIPsList = new ArrayList<>();
|
||||
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");
|
||||
}
|
||||
|
||||
private void addAllowedIPs(@Nullable final String[] allowedIPs) {
|
||||
if (allowedIPs != null && allowedIPs.length > 0) {
|
||||
for (final String allowedIP : allowedIPs) {
|
||||
allowedIPsList.add(new InetNetwork(allowedIP));
|
||||
/**
|
||||
* 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 ParseException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute = Attribute.parse(line)
|
||||
.orElseThrow(() -> new ParseException("[Peer]", line, "Syntax error"));
|
||||
switch (attribute.getKey().toLowerCase()) {
|
||||
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 ParseException("[Peer]", line, "Unknown attribute");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InetNetwork[] getAllowedIPs() {
|
||||
return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAllowedIPsString() {
|
||||
if (allowedIPsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(allowedIPsList);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InetEndpoint getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getEndpointString() {
|
||||
if (endpoint == null)
|
||||
return null;
|
||||
return endpoint.getEndpoint();
|
||||
}
|
||||
|
||||
public int getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getPersistentKeepaliveString() {
|
||||
if (persistentKeepalive == 0)
|
||||
return null;
|
||||
return Integer.valueOf(persistentKeepalive).toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getResolvedEndpointString() throws UnknownHostException {
|
||||
if (endpoint == null)
|
||||
throw new UnknownHostException("{empty}");
|
||||
return endpoint.getResolvedEndpoint();
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_interface_parse_failed, line));
|
||||
switch (key) {
|
||||
case ALLOWED_IPS:
|
||||
addAllowedIPs(key.parseList(line));
|
||||
break;
|
||||
case ENDPOINT:
|
||||
setEndpointString(key.parse(line));
|
||||
break;
|
||||
case PERSISTENT_KEEPALIVE:
|
||||
setPersistentKeepaliveString(key.parse(line));
|
||||
break;
|
||||
case PRESHARED_KEY:
|
||||
setPreSharedKey(key.parse(line));
|
||||
break;
|
||||
case PUBLIC_KEY:
|
||||
setPublicKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAllowedIPsString(@Nullable final String allowedIPsString) {
|
||||
allowedIPsList.clear();
|
||||
addAllowedIPs(Attribute.stringToList(allowedIPsString));
|
||||
}
|
||||
|
||||
private void setEndpoint(@Nullable final InetEndpoint endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
private void setEndpointString(@Nullable final String endpoint) {
|
||||
if (endpoint != null && !endpoint.isEmpty())
|
||||
setEndpoint(new InetEndpoint(endpoint));
|
||||
else
|
||||
setEndpoint(null);
|
||||
}
|
||||
|
||||
private void setPersistentKeepalive(final int persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
}
|
||||
|
||||
private void setPersistentKeepaliveString(@Nullable final String persistentKeepalive) {
|
||||
if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
|
||||
setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
|
||||
else
|
||||
setPersistentKeepalive(0);
|
||||
}
|
||||
|
||||
private void setPreSharedKey(@Nullable String preSharedKey) {
|
||||
if (preSharedKey != null && preSharedKey.isEmpty())
|
||||
preSharedKey = null;
|
||||
if (preSharedKey != null)
|
||||
KeyEncoding.keyFromBase64(preSharedKey);
|
||||
this.preSharedKey = preSharedKey;
|
||||
}
|
||||
|
||||
private void setPublicKey(@Nullable String publicKey) {
|
||||
if (publicKey != null && publicKey.isEmpty())
|
||||
publicKey = null;
|
||||
if (publicKey != null)
|
||||
KeyEncoding.keyFromBase64(publicKey);
|
||||
this.publicKey = publicKey;
|
||||
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().append("[Peer]\n");
|
||||
if (!allowedIPsList.isEmpty())
|
||||
sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList));
|
||||
if (endpoint != null)
|
||||
sb.append(Attribute.ENDPOINT.composeWith(getEndpointString()));
|
||||
if (persistentKeepalive != 0)
|
||||
sb.append(Attribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
|
||||
if (preSharedKey != null)
|
||||
sb.append(Attribute.PRESHARED_KEY.composeWith(preSharedKey));
|
||||
if (publicKey != null)
|
||||
sb.append(Attribute.PUBLIC_KEY.composeWith(publicKey));
|
||||
final StringBuilder sb = new StringBuilder("(Peer ");
|
||||
sb.append(publicKey.toBase64());
|
||||
endpoint.ifPresent(ep -> sb.append(" @").append(ep));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
/**
|
||||
* 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() {
|
||||
return new Peer(this);
|
||||
}
|
||||
|
||||
public Builder parseAllowedIPs(final CharSequence allowedIps) throws ParseException {
|
||||
try {
|
||||
final List<InetNetwork> parsed = Stream.of(Attribute.split(allowedIps))
|
||||
.map(InetNetwork::parse)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
return addAllowedIps(parsed);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("AllowedIPs", allowedIps, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
public Builder parseEndpoint(final String endpoint) throws ParseException {
|
||||
try {
|
||||
return setEndpoint(InetEndpoint.parse(endpoint));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("Endpoint", endpoint, e);
|
||||
}
|
||||
};
|
||||
private static final List<String> DEFAULT_ROUTE_MOD_RFC1918_V4 = Arrays.asList("0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
|
||||
private static final String DEFAULT_ROUTE_V4 = "0.0.0.0/0";
|
||||
private final List<String> interfaceDNSRoutes = new ArrayList<>();
|
||||
@Nullable private String allowedIPs;
|
||||
@Nullable private String endpoint;
|
||||
private int numSiblings;
|
||||
@Nullable private String persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
|
||||
public Observable(final Peer parent) {
|
||||
loadData(parent);
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
allowedIPs = in.readString();
|
||||
endpoint = in.readString();
|
||||
persistentKeepalive = in.readString();
|
||||
preSharedKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
numSiblings = in.readInt();
|
||||
in.readStringList(interfaceDNSRoutes);
|
||||
}
|
||||
|
||||
public static Observable newInstance() {
|
||||
return new Observable(new Peer());
|
||||
}
|
||||
|
||||
public void commitData(final Peer parent) {
|
||||
parent.setAllowedIPsString(allowedIPs);
|
||||
parent.setEndpointString(endpoint);
|
||||
parent.setPersistentKeepaliveString(persistentKeepalive);
|
||||
parent.setPreSharedKey(preSharedKey);
|
||||
parent.setPublicKey(publicKey);
|
||||
if (parent.getPublicKey() == null)
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_peer_public_key));
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getAllowedIPs() {
|
||||
return allowedIPs;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getCanToggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = Arrays.asList(Attribute.stringToList(allowedIPs));
|
||||
return numSiblings == 0 && (ips.contains(DEFAULT_ROUTE_V4) || ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4));
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getIsExcludePrivateIPsOn() {
|
||||
return numSiblings == 0 && Arrays.asList(Attribute.stringToList(allowedIPs)).containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Peer parent) {
|
||||
allowedIPs = parent.getAllowedIPsString();
|
||||
endpoint = parent.getEndpointString();
|
||||
persistentKeepalive = parent.getPersistentKeepaliveString();
|
||||
preSharedKey = parent.getPreSharedKey();
|
||||
publicKey = parent.getPublicKey();
|
||||
}
|
||||
|
||||
public void setAllowedIPs(final String allowedIPs) {
|
||||
this.allowedIPs = allowedIPs;
|
||||
notifyPropertyChanged(BR.allowedIPs);
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
}
|
||||
|
||||
public void setEndpoint(final String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
notifyPropertyChanged(BR.endpoint);
|
||||
}
|
||||
|
||||
public void setInterfaceDNSRoutes(@Nullable final String dnsServers) {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean modifyAllowedIPs = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
|
||||
ips.removeAll(interfaceDNSRoutes);
|
||||
interfaceDNSRoutes.clear();
|
||||
for (final String dnsServer : Attribute.stringToList(dnsServers)) {
|
||||
if (!dnsServer.contains(":"))
|
||||
interfaceDNSRoutes.add(dnsServer + "/32");
|
||||
public Builder parsePersistentKeepalive(final String persistentKeepalive) throws ParseException {
|
||||
try {
|
||||
return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException("PersistentKeepalive", persistentKeepalive, e);
|
||||
}
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
if (modifyAllowedIPs)
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
public void setNumSiblings(final int num) {
|
||||
numSiblings = num;
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
public Builder parsePreSharedKey(final String preSharedKey) throws ParseException {
|
||||
try {
|
||||
return setPreSharedKey(Key.fromBase64(preSharedKey));
|
||||
} catch (final Key.KeyFormatException e) {
|
||||
throw new ParseException("PresharedKey", preSharedKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPersistentKeepalive(final String persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
notifyPropertyChanged(BR.persistentKeepalive);
|
||||
public Builder parsePublicKey(final String publicKey) throws ParseException {
|
||||
try {
|
||||
return setPublicKey(Key.fromBase64(publicKey));
|
||||
} catch (final Key.KeyFormatException e) {
|
||||
throw new ParseException("PublicKey", publicKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPreSharedKey(final String preSharedKey) {
|
||||
this.preSharedKey = preSharedKey;
|
||||
notifyPropertyChanged(BR.preSharedKey);
|
||||
public Builder setEndpoint(final InetEndpoint endpoint) {
|
||||
this.endpoint = Optional.of(endpoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setPublicKey(final String publicKey) {
|
||||
public Builder setPersistentKeepalive(final int persistentKeepalive) {
|
||||
if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
|
||||
throw new IllegalArgumentException("Invalid value for 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;
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
public void toggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean hasDefaultRoute = ips.contains(DEFAULT_ROUTE_V4);
|
||||
final boolean hasDefaultRouteModRFC1918 = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
if ((!hasDefaultRoute && !hasDefaultRouteModRFC1918) || numSiblings > 0)
|
||||
return;
|
||||
Iterables.removeIf(ips, ip -> !ip.contains(":"));
|
||||
if (hasDefaultRoute) {
|
||||
ips.addAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
} else if (hasDefaultRouteModRFC1918)
|
||||
ips.add(DEFAULT_ROUTE_V4);
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(allowedIPs);
|
||||
dest.writeString(endpoint);
|
||||
dest.writeString(persistentKeepalive);
|
||||
dest.writeString(preSharedKey);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeInt(numSiblings);
|
||||
dest.writeStringList(interfaceDNSRoutes);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user