diff options
Diffstat (limited to 'tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java')
-rw-r--r-- | tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java | 130 |
1 files changed, 114 insertions, 16 deletions
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java index 9fafc918..6b66f2c5 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -16,6 +16,7 @@ import com.wireguard.android.backend.BackendException.Reason; import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; +import com.wireguard.config.InetEndpoint; import com.wireguard.config.InetNetwork; import com.wireguard.config.Peer; import com.wireguard.crypto.Key; @@ -23,6 +24,7 @@ import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; import java.net.InetAddress; +import java.time.Instant; import java.util.Collections; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -34,8 +36,13 @@ import java.util.concurrent.TimeoutException; import androidx.annotation.Nullable; import androidx.collection.ArraySet; +/** + * Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide + * WireGuard tunnels. + */ @NonNullForAll public final class GoBackend implements Backend { + private static final int DNS_RESOLUTION_RETRIES = 10; private static final String TAG = "WireGuard/GoBackend"; @Nullable private static AlwaysOnCallback alwaysOnCallback; private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>(); @@ -44,16 +51,27 @@ public final class GoBackend implements Backend { @Nullable private Tunnel currentTunnel; private int currentTunnelHandle = -1; + /** + * Public constructor for GoBackend. + * + * @param context An Android {@link Context} + */ public GoBackend(final Context context) { SharedLibraryLoader.loadSharedLibrary(context, "wg-go"); this.context = context; } + /** + * Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the + * system's Always-On VPN mode. + * + * @param cb Callback to be invoked + */ public static void setAlwaysOnCallback(final AlwaysOnCallback cb) { alwaysOnCallback = cb; } - private static native String wgGetConfig(int handle); + @Nullable private static native String wgGetConfig(int handle); private static native int wgGetSocketV4(int handle); @@ -65,6 +83,11 @@ public final class GoBackend implements Backend { private static native String wgVersion(); + /** + * Method to get the names of running tunnels. + * + * @return A set of string values denoting names of running tunnels. + */ @Override public Set<String> getRunningTunnelNames() { if (currentTunnel != null) { @@ -75,27 +98,42 @@ public final class GoBackend implements Backend { return Collections.emptySet(); } + /** + * Get the associated {@link State} for a given {@link Tunnel}. + * + * @param tunnel The tunnel to examine the state of. + * @return {@link State} associated with the given tunnel. + */ @Override public State getState(final Tunnel tunnel) { return currentTunnel == tunnel ? State.UP : State.DOWN; } + /** + * Get the associated {@link Statistics} for a given {@link Tunnel}. + * + * @param tunnel The tunnel to retrieve statistics for. + * @return {@link Statistics} associated with the given tunnel. + */ @Override public Statistics getStatistics(final Tunnel tunnel) { final Statistics stats = new Statistics(); - if (tunnel != currentTunnel) { + if (tunnel != currentTunnel || currentTunnelHandle == -1) return stats; - } final String config = wgGetConfig(currentTunnelHandle); + if (config == null) + return stats; Key key = null; long rx = 0; long tx = 0; + long latestHandshakeMSec = 0; for (final String line : config.split("\\n")) { if (line.startsWith("public_key=")) { if (key != null) - stats.add(key, rx, tx); + stats.add(key, rx, tx, latestHandshakeMSec); rx = 0; tx = 0; + latestHandshakeMSec = 0; try { key = Key.fromHex(line.substring(11)); } catch (final KeyFormatException ignored) { @@ -117,18 +155,49 @@ public final class GoBackend implements Backend { } catch (final NumberFormatException ignored) { tx = 0; } + } else if (line.startsWith("last_handshake_time_sec=")) { + if (key == null) + continue; + try { + latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000; + } catch (final NumberFormatException ignored) { + latestHandshakeMSec = 0; + } + } else if (line.startsWith("last_handshake_time_nsec=")) { + if (key == null) + continue; + try { + latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000; + } catch (final NumberFormatException ignored) { + latestHandshakeMSec = 0; + } } } if (key != null) - stats.add(key, rx, tx); + stats.add(key, rx, tx, latestHandshakeMSec); return stats; } + /** + * Get the version of the underlying wireguard-go library. + * + * @return {@link String} value of the version of the wireguard-go library. + */ @Override public String getVersion() { return wgVersion(); } + /** + * Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}. + * + * @param tunnel The tunnel to control the state of. + * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or + * {@code TOGGLE}. + * @param config The configuration for this tunnel, may be null if state is {@code DOWN}. + * @return {@link State} of the tunnel after state changes are applied. + * @throws Exception Exception raised while changing tunnel state. + */ @Override public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception { final State originalState = getState(tunnel); @@ -167,8 +236,10 @@ public final class GoBackend implements Backend { throw new BackendException(Reason.VPN_NOT_AUTHORIZED); final VpnService service; - if (!vpnService.isDone()) - startVpnService(); + if (!vpnService.isDone()) { + Log.d(TAG, "Requesting to start VpnService"); + context.startService(new Intent(context, VpnService.class)); + } try { service = vpnService.get(2, TimeUnit.SECONDS); @@ -184,6 +255,25 @@ public final class GoBackend implements Backend { return; } + + dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) { + // Pre-resolve IPs so they're cached when building the userspace string + for (final Peer peer : config.getPeers()) { + final InetEndpoint ep = peer.getEndpoint().orElse(null); + if (ep == null) + continue; + if (ep.getResolved().orElse(null) == null) { + if (i < DNS_RESOLUTION_RETRIES - 1) { + Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again"); + Thread.sleep(1000); + continue dnsRetry; + } else + throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost()); + } + } + break; + } + // Build config final String goConfig = config.toWgUserspaceString(); @@ -203,6 +293,9 @@ public final class GoBackend implements Backend { for (final InetAddress addr : config.getInterface().getDnsServers()) builder.addDnsServer(addr.getHostAddress()); + for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains()) + builder.addSearchDomain(dnsSearchDomain); + boolean sawDefaultRoute = false; for (final Peer peer : config.getPeers()) { for (final InetNetwork addr : peer.getAllowedIps()) { @@ -229,7 +322,7 @@ public final class GoBackend implements Backend { try (final ParcelFileDescriptor tun = builder.establish()) { if (tun == null) throw new BackendException(Reason.TUN_CREATION_ERROR); - Log.d(TAG, "Go backend v" + wgVersion()); + Log.d(TAG, "Go backend " + wgVersion()); currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig); } if (currentTunnelHandle < 0) @@ -245,21 +338,23 @@ public final class GoBackend implements Backend { Log.w(TAG, "Tunnel already down"); return; } - - wgTurnOff(currentTunnelHandle); + int handleToClose = currentTunnelHandle; currentTunnel = null; currentTunnelHandle = -1; currentConfig = null; + wgTurnOff(handleToClose); + try { + vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf(); + } catch (final TimeoutException ignored) { } } tunnel.onStateChange(state); } - private void startVpnService() { - Log.d(TAG, "Requesting to start VpnService"); - context.startService(new Intent(context, VpnService.class)); - } - + /** + * Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the + * system's Always-On VPN mode. + */ public interface AlwaysOnCallback { void alwaysOnTriggered(); } @@ -293,6 +388,9 @@ public final class GoBackend implements Backend { } } + /** + * {@link android.net.VpnService} implementation for {@link GoBackend} + */ public static class VpnService extends android.net.VpnService { @Nullable private GoBackend owner; |