diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/config/Interface.java')
-rw-r--r-- | app/src/main/java/com/wireguard/config/Interface.java | 588 |
1 files changed, 269 insertions, 319 deletions
diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java index aa1d986b..dc1a291d 100644 --- a/app/src/main/java/com/wireguard/config/Interface.java +++ b/app/src/main/java/com/wireguard/config/Interface.java @@ -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 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; - - public Interface() { - addressList = new ArrayList<>(); - dnsList = new ArrayList<>(); - excludedApplications = new ArrayList<>(); +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; } - 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"); } } + return builder.build(); } - 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()); + @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); } - private List<String> getDnsStrings() { - final List<String> strings = new ArrayList<>(); - for (final InetAddress addr : dnsList) - strings.add(addr.getHostAddress()); - return strings; + /** + * 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; } - public InetAddress[] getDnses() { - return dnsList.toArray(new InetAddress[dnsList.size()]); + /** + * 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; } - public String[] getExcludedApplications() { - return excludedApplications.toArray(new String[excludedApplications.size()]); + /** + * 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; } - @Nullable - private String getExcludedApplicationsString() { - if (excludedApplications.isEmpty()) - return null; - return Attribute.iterableToString(excludedApplications); + /** + * Returns the public/private key pair used by the interface. + * + * @return a key pair + */ + public KeyPair getKeyPair() { + return keyPair; } - public int getListenPort() { + /** + * 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; } - @Nullable - private String getListenPortString() { - if (listenPort == 0) - return null; - return Integer.valueOf(listenPort).toString(); - } - - public int getMtu() { + /** + * 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; } - @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); + @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); - } - - @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; - - public Observable(@Nullable final Interface parent) { - if (parent != null) - loadData(parent); - } - - 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 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(); - } - - @Override - public int describeContents() { - return 0; + /** + * 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(); + } - public void generateKeypair() { - final Keypair keypair = new Keypair(); - privateKey = keypair.getPrivateKey(); - publicKey = keypair.getPublicKey(); - notifyPropertyChanged(BR.privateKey); - notifyPropertyChanged(BR.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(); + } - @Nullable - @Bindable - public String getAddresses() { - return addresses; + @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; } - @Nullable - @Bindable - public String getDnses() { - return dnses; + public Builder addAddresses(final Collection<InetNetwork> addresses) { + this.addresses.addAll(addresses); + return this; } - @Nullable - @Bindable - public String getExcludedApplications() { - return excludedApplications; + public Builder addDnsServer(final InetAddress dnsServer) { + dnsServers.add(dnsServer); + return this; } - @Bindable - public int getExcludedApplicationsCount() { - return Attribute.stringToList(excludedApplications).length; + public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) { + this.dnsServers.addAll(dnsServers); + return this; } - @Nullable - @Bindable - public String getListenPort() { - return listenPort; + public Interface build() { + return new Interface(this); } - @Nullable - @Bindable - public String getMtu() { - return mtu; + public Builder excludeApplication(final String application) { + excludedApplications.add(application); + return this; } - @Nullable - @Bindable - public String getPrivateKey() { - return privateKey; + public Builder excludeApplications(final Collection<String> applications) { + excludedApplications.addAll(applications); + return this; } - @Nullable - @Bindable - public String getPublicKey() { - return publicKey; + public Builder parseAddresses(final CharSequence addresses) throws ParseException { + try { + 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); + } } - 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 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 void setAddresses(final String addresses) { - this.addresses = addresses; - notifyPropertyChanged(BR.addresses); + 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 void setDnses(final String dnses) { - this.dnses = dnses; - notifyPropertyChanged(BR.dnses); + 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 void setExcludedApplications(final String excludedApplications) { - this.excludedApplications = excludedApplications; - notifyPropertyChanged(BR.excludedApplications); - notifyPropertyChanged(BR.excludedApplicationsCount); + 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 void setListenPort(final String listenPort) { - this.listenPort = listenPort; - notifyPropertyChanged(BR.listenPort); + 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 void setMtu(final String mtu) { - this.mtu = mtu; - notifyPropertyChanged(BR.mtu); + public Builder setKeyPair(final KeyPair keyPair) { + this.keyPair = keyPair; + return this; } - public void setPrivateKey(final String privateKey) { - this.privateKey = privateKey; - - try { - publicKey = new Keypair(privateKey).getPublicKey(); - } catch (final IllegalArgumentException ignored) { - publicKey = ""; - } - - notifyPropertyChanged(BR.privateKey); - notifyPropertyChanged(BR.publicKey); + 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; } - @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 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; } } } |