/* * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.config; import androidx.annotation.Nullable; import org.threeten.bp.Duration; import org.threeten.bp.Instant; import java.net.Inet4Address; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.regex.Pattern; import java9.util.Optional; /** * An external endpoint (host and port) used to connect to a WireGuard {@link Peer}. *

* Instances of this class are externally immutable. */ public final class InetEndpoint { private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*"); private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]"); private final String host; private final boolean isResolved; private final Object lock = new Object(); private final int port; private Instant lastResolution = Instant.EPOCH; @Nullable private InetEndpoint resolved; private InetEndpoint(final String host, final boolean isResolved, final int port) { this.host = host; this.isResolved = isResolved; this.port = port; } public static InetEndpoint parse(final String endpoint) throws ParseException { if (FORBIDDEN_CHARACTERS.matcher(endpoint).find()) throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters"); final URI uri; try { uri = new URI("wg://" + endpoint); } catch (final URISyntaxException e) { throw new IllegalArgumentException(e); } if (uri.getPort() < 0 || uri.getPort() > 65535) throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number"); try { InetAddresses.parse(uri.getHost()); // Parsing ths host as a numeric address worked, so we don't need to do DNS lookups. return new InetEndpoint(uri.getHost(), true, uri.getPort()); } catch (final ParseException ignored) { // Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN. return new InetEndpoint(uri.getHost(), false, uri.getPort()); } } @Override public boolean equals(final Object obj) { if (!(obj instanceof InetEndpoint)) return false; final InetEndpoint other = (InetEndpoint) obj; return host.equals(other.host) && port == other.port; } public String getHost() { return host; } public int getPort() { return port; } /** * Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS * to a numeric address. If the host is already numeric, the existing instance may be returned. * Because this function may perform network I/O, it must not be called from the main thread. * * @return the resolved endpoint, or {@link Optional#empty()} */ public Optional getResolved() { if (isResolved) return Optional.of(this); synchronized (lock) { //TODO(zx2c4): Implement a real timeout mechanism using DNS TTL if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) { try { // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues. final InetAddress[] candidates = InetAddress.getAllByName(host); InetAddress address = candidates[0]; for (final InetAddress candidate : candidates) { if (candidate instanceof Inet4Address) { address = candidate; break; } } resolved = new InetEndpoint(address.getHostAddress(), true, port); lastResolution = Instant.now(); } catch (final UnknownHostException e) { resolved = null; } } return Optional.ofNullable(resolved); } } @Override public int hashCode() { return host.hashCode() ^ port; } @Override public String toString() { final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches(); return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port; } }