/* * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.backend; import android.app.AlarmManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.LocalSocketAddress; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.ProxyInfo; import android.net.TrafficStats; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.system.OsConstants; import android.util.Log; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Empty; import com.wireguard.android.backend.BackendException.Reason; import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.backend.gen.CapabilitiesChangedRequest; import com.wireguard.android.backend.gen.CapabilitiesChangedResponse; import com.wireguard.android.backend.gen.DhcpRequest; import com.wireguard.android.backend.gen.DhcpResponse; import com.wireguard.android.backend.gen.GetConnectionOwnerUidResponse; import com.wireguard.android.backend.gen.IpcSetRequest; import com.wireguard.android.backend.gen.IpcSetResponse; import com.wireguard.android.backend.gen.Lease; import com.wireguard.android.backend.gen.LibwgGrpc; import com.wireguard.android.backend.gen.ReverseRequest; import com.wireguard.android.backend.gen.ReverseResponse; import com.wireguard.android.backend.gen.StartHttpProxyRequest; import com.wireguard.android.backend.gen.StartHttpProxyResponse; import com.wireguard.android.backend.gen.StopHttpProxyRequest; import com.wireguard.android.backend.gen.StopHttpProxyResponse; import com.wireguard.android.backend.gen.TunnelHandle; import com.wireguard.android.backend.gen.VersionRequest; import com.wireguard.android.backend.gen.VersionResponse; import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; import com.wireguard.config.HttpProxy; import com.wireguard.config.InetEndpoint; import com.wireguard.config.InetNetwork; import com.wireguard.config.Peer; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; import com.wireguard.util.Resolver; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.android.UdsChannelBuilder; import io.grpc.okhttp.OkHttpChannelBuilder; import io.grpc.stub.StreamObserver; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.net.URL; import java.time.Duration; import java.time.Instant; import java.nio.ByteOrder; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import javax.net.SocketFactory; 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"; private static final int STATS_TAG = 2; private static final int MSG_DHCP_EXPIRE = 1; private static final int MSG_CAPABILITIES_CHANGED = 2; @Nullable private static AlwaysOnCallback alwaysOnCallback; private static GhettoCompletableFuture vpnService = new GhettoCompletableFuture<>(); private final Context context; @Nullable private Config currentConfig; @Nullable private Tunnel currentTunnel; private int currentTunnelHandle = -1; private ConnectivityManager connectivityManager; private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback(); private ConnectivityManager.NetworkCallback vpnNetworkCallback; @Nullable private Network activeNetwork; private ManagedChannel channel; private boolean obtainDhcpLease = false; @Nullable private Bgp bgp; private HandlerThread thread; private Handler handler; private NetworkCapabilities activeNetworkCapabilities; /** * Public constructor for GoBackend. * * @param context An Android {@link Context} */ public GoBackend(final Context context) { SharedLibraryLoader.loadSharedLibrary(context, "wg-go"); this.context = context; connectivityManager = context.getSystemService(ConnectivityManager.class); File socketFile = new File(context.getCacheDir(), "libwg.sock"); String socketName = socketFile.getAbsolutePath(); Log.i(TAG, "wgStartGrpc: " + wgStartGrpc(socketName)); channel = UdsChannelBuilder.forPath(socketName, LocalSocketAddress.Namespace.FILESYSTEM).build(); } /** * 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; } @Nullable private static native String wgGetConfig(int handle); private static native int wgGetSocketV4(int handle); private static native int wgGetSocketV6(int handle); private static native int wgSetConfig(int handle, String settings); private static native void wgSetFd(int handle, int tunFd); private static native void wgTurnOff(int handle); private static native int wgTurnOn(String ifName, int tunFd, String settings); private static native String wgVersion(); private static native int wgStartGrpc(String sockName); /** * Method to get the names of running tunnels. * * @return A set of string values denoting names of running tunnels. */ @Override public Set getRunningTunnelNames() { if (currentTunnel != null) { final Set runningTunnels = new ArraySet<>(); runningTunnels.add(currentTunnel.getName()); return runningTunnels; } 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 || 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, latestHandshakeMSec); rx = 0; tx = 0; latestHandshakeMSec = 0; try { key = Key.fromHex(line.substring(11)); } catch (final KeyFormatException ignored) { key = null; } } else if (line.startsWith("rx_bytes=")) { if (key == null) continue; try { rx = Long.parseLong(line.substring(9)); } catch (final NumberFormatException ignored) { rx = 0; } } else if (line.startsWith("tx_bytes=")) { if (key == null) continue; try { tx = Long.parseLong(line.substring(9)); } 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, 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() { LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); VersionRequest request = VersionRequest.newBuilder().build(); VersionResponse resp = stub.version(request); return resp.getVersion(); } /** * 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); if (state == State.TOGGLE) state = originalState == State.UP ? State.DOWN : State.UP; if (state == originalState && tunnel == currentTunnel && config == currentConfig) return originalState; if (state == State.UP) { final Config originalConfig = currentConfig; final Tunnel originalTunnel = currentTunnel; if (currentTunnel != null) setStateInternal(currentTunnel, null, State.DOWN); try { setStateInternal(tunnel, config, state); } catch (final Exception e) { if (originalTunnel != null) setStateInternal(originalTunnel, originalConfig, State.UP); throw e; } } else if (state == State.DOWN && tunnel == currentTunnel) { setStateInternal(tunnel, null, State.DOWN); } return getState(tunnel); } @Override public void addAllowedIps(Tunnel tunnel, Key publicKey, List addNetworks) { if (tunnel != currentTunnel) { // TODO logerror and/or return error/throw. return; } StringBuffer sb = new StringBuffer(); sb.append("public_key=").append(publicKey.toHex()).append('\n'); for (final InetNetwork allowedIp: addNetworks) { sb.append("allowed_ip=").append(allowedIp).append('\n'); } String goConfig = sb.toString(); // TODO removed removeNetworks Log.w(TAG, "Wg user string: " + goConfig); LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); TunnelHandle handle = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build(); IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(handle).setConfig(goConfig).build(); IpcSetResponse resp = stub.ipcSet(request); } private static String downloadPacFile(Network network, Uri pacFileUrl) { HttpURLConnection urlConnection = null; StringBuffer buf = new StringBuffer(); try { URL url = new URL(pacFileUrl.toString()); TrafficStats.setThreadStatsTag(STATS_TAG); urlConnection = (HttpURLConnection) network.openConnection(url); InputStream in = urlConnection.getInputStream(); InputStreamReader isw = new InputStreamReader(in); int data = isw.read(); while (data != -1) { char current = (char) data; data = isw.read(); buf.append(current); } } catch (Exception e) { } finally { if (urlConnection != null) { urlConnection.disconnect(); } } return buf.toString(); } private void capabilitiesChanged(NetworkCapabilities capabilities) { LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); CapabilitiesChangedRequest.Builder reqBuilder = CapabilitiesChangedRequest.newBuilder(); if (capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { reqBuilder.addCapabilities(CapabilitiesChangedRequest.Capability.NOT_METERED); } CapabilitiesChangedResponse resp = stub.capabilitiesChanged(reqBuilder.build()); Log.i(TAG, "Capabilities change: " + resp.getError().getMessage()); } private int startHttpProxy(String pacFile) { LibwgGrpc.LibwgStub asyncStub = LibwgGrpc.newStub(channel); LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); StartHttpProxyRequest.Builder reqBuilder = StartHttpProxyRequest.newBuilder(); if (pacFile != null && pacFile != "") { reqBuilder.setPacFileContent(pacFile); } Thread streamer = new Thread(new Runnable() { public void run() { try { Log.i(TAG, "Before streamReverse"); streamReverse(asyncStub); Log.i(TAG, "After streamReverse"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); StartHttpProxyRequest req = reqBuilder.build(); StartHttpProxyResponse resp = stub.startHttpProxy(req); Log.i(TAG, "Start http proxy listen_port:" + resp.getListenPort() + ", error:" + resp.getError().getMessage()); capabilitiesChanged(activeNetworkCapabilities); streamer.start(); return resp.getListenPort(); } private void stopHttpProxy() { LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); StopHttpProxyRequest req = StopHttpProxyRequest.newBuilder().build(); StopHttpProxyResponse resp = stub.stopHttpProxy(req); Log.i(TAG, "Stop http proxy: " + resp.getError().getMessage()); } private static InetSocketAddress toInetSocketAddress(com.wireguard.android.backend.gen.InetSocketAddress sockAddr) { try { return new InetSocketAddress(InetAddress.getByAddress(sockAddr.getAddress().getAddress().toByteArray()), sockAddr.getPort()); } catch (UnknownHostException e) { throw new RuntimeException(e); } } private void Dhcp(VpnService service) throws Exception{ if (currentConfig == null || currentTunnel == null || currentTunnelHandle < 0) { return; } obtainDhcpLease = false; // Heuristics: Use first ULA address as client address com.wireguard.android.backend.gen.InetAddress source = null; for (final InetNetwork net : currentConfig.getInterface().getAddresses()) { InetAddress addr = net.getAddress(); if (addr instanceof Inet6Address) { if (Resolver.isULA((Inet6Address)addr)) { source = com.wireguard.android.backend.gen.InetAddress.newBuilder().setAddress(ByteString.copyFrom(addr.getAddress())).build(); } } } LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); DhcpRequest.Builder requestBuilder = DhcpRequest.newBuilder(); if (source != null) { requestBuilder.setSource(source); } DhcpRequest request = requestBuilder.build(); DhcpResponse resp = stub.dhcp(request); Log.i(TAG, "Dhcp: " + resp.getError().getMessage()); Dhcp dhcp = new Dhcp(); long delayMillis = 1000 * 3600 * 12; // Max renew 12h if (resp.getLeasesList() != null) { for (final Lease lease: resp.getLeasesList()) { try { InetAddress addr = InetAddress.getByAddress(lease.getAddress().getAddress().toByteArray()); Log.i(TAG, "Lease: " + addr + " " + lease.getValidLifetime().getSeconds() + " " + lease.getPreferredLifetime().getSeconds()); dhcp.addLease(new InetNetwork(addr, 128), Duration.ofSeconds(lease.getValidLifetime().getSeconds(), lease.getValidLifetime().getNanos()), Duration.ofSeconds(lease.getPreferredLifetime().getSeconds(), lease.getPreferredLifetime().getNanos())); long leaseDelayMillis = lease.getValidLifetime().getSeconds() * 1000 + lease.getValidLifetime().getNanos() / 1000; delayMillis = Math.min(delayMillis, leaseDelayMillis); } catch (UnknownHostException ex) { // Ignore } } } // Replace the vpn tunnel final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getLeases()); Log.i(TAG, "Builder: " + builder); try (final ParcelFileDescriptor tun = builder.establish()) { if (tun == null) throw new BackendException(Reason.TUN_CREATION_ERROR); Log.d(TAG, "Go backend " + wgVersion()); // SetFd wgSetFd(currentTunnelHandle, tun.detachFd()); } if (currentTunnelHandle < 0) throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle); service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle)); Log.i(TAG, "Dhcp done"); AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); am.cancel(alarmListener); am.setWindow(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis * 3 / 4, delayMillis / 4, null, alarmListener, handler); if (bgp != null) { bgp = new Bgp(this, channel, currentTunnel, currentTunnelHandle); bgp.startServer(); } currentTunnel.onDhcpChange(dhcp); } private int getConnectionOwnerUid(int protocol, InetSocketAddress local, InetSocketAddress remote) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return connectivityManager.getConnectionOwnerUid(protocol, local, remote); else return Process.INVALID_UID; } private void streamReverse(LibwgGrpc.LibwgStub asyncStub) throws InterruptedException { Log.i(TAG, "In streamReverse"); final CountDownLatch finishLatch = new CountDownLatch(1); final AtomicReference> atomicRequestObserver = new AtomicReference>(); // Throwable failed = null; StreamObserver responseObserver = new StreamObserver() { @Override public void onNext(ReverseResponse resp) { try { String pkg = ""; int uid = getConnectionOwnerUid(resp.getUid().getProtocol(), toInetSocketAddress(resp.getUid().getLocal()), toInetSocketAddress(resp.getUid().getRemote())); if (uid != Process.INVALID_UID) { PackageManager pm = context.getPackageManager(); pkg = pm.getNameForUid(uid); String[] pkgs = pm.getPackagesForUid(uid); Log.i(TAG, "reverse onNext uid:" + uid + " package:" + pkg); if (pkgs != null) { for (int i=0; i < pkgs.length; i++) { Log.i(TAG, "getPackagesForUid() = " + pkgs[i]); } } } else { Log.i(TAG, "Connection not found"); } ReverseRequest req = ReverseRequest.newBuilder() .setUid(GetConnectionOwnerUidResponse.newBuilder() .setUid(uid) .setPackage(pkg != null ? pkg: "") .build()) .build(); io.grpc.Context.current().fork().run(new Runnable() { public void run() { atomicRequestObserver.get().onNext(req); } }); } catch (RuntimeException ex) { Log.i(TAG, "onNext " + ex); throw ex; } } @Override public void onError(Throwable t) { // failed = t; Log.i(TAG, "streamReverse error: " + t); finishLatch.countDown(); } @Override public void onCompleted() { Log.i(TAG, "streamReverse completed"); finishLatch.countDown(); } }; StreamObserver requestObserver = asyncStub.reverse(responseObserver); atomicRequestObserver.set(requestObserver); // Mark the end of requests //requestObserver.onCompleted(); //requestObserver.onNext(ReverseRequest.getDefaultInstance()); Log.i(TAG, "Waiting streamReverse"); // Receiving happens asynchronously finishLatch.await(); // if (failed != null) { // throw new RuntimeException(failed); // } Log.i(TAG, "Exit streamReverse"); } private VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set leases) throws PackageManager.NameNotFoundException { Log.i(TAG, "Builder 1"); final VpnService.Builder builder = service.getBuilder(); Log.i(TAG, "Builder 2"); builder.setSession(name); Log.i(TAG, "Builder 3"); for (final String excludedApplication : config.getInterface().getExcludedApplications()) builder.addDisallowedApplication(excludedApplication); Log.i(TAG, "Builder 4"); for (final String includedApplication : config.getInterface().getIncludedApplications()) builder.addAllowedApplication(includedApplication); Log.i(TAG, "Builder 5"); if (leases != null) { for (final Dhcp.Lease lease: leases) { InetNetwork addr = lease.getAddress(); builder.addAddress(addr.getAddress(), addr.getMask()); } } Log.i(TAG, "Builder 6"); for (final InetNetwork addr : config.getInterface().getAddresses()) builder.addAddress(addr.getAddress(), addr.getMask()); Log.i(TAG, "Builder 7"); for (final InetAddress addr : config.getInterface().getDnsServers()) builder.addDnsServer(addr.getHostAddress()); Log.i(TAG, "Builder 8"); for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains()) builder.addSearchDomain(dnsSearchDomain); Log.i(TAG, "Builder 9"); boolean sawDefaultRoute = false; for (final Peer peer : config.getPeers()) { for (final InetNetwork addr : peer.getAllowedIps()) { if (addr.getMask() == 0) sawDefaultRoute = true; builder.addRoute(addr.getAddress(), addr.getMask()); } } Log.i(TAG, "Builder 10"); // "Kill-switch" semantics if (!(sawDefaultRoute && config.getPeers().size() == 1)) { builder.allowFamily(OsConstants.AF_INET); builder.allowFamily(OsConstants.AF_INET6); } Log.i(TAG, "Builder 11"); builder.setMtu(config.getInterface().getMtu().orElse(1280)); Log.i(TAG, "Builder 12"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false); Log.i(TAG, "Builder 13"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) service.setUnderlyingNetworks(null); Log.i(TAG, "Builder 14"); Optional proxy = config.getInterface().getHttpProxy(); if (proxy.isPresent()) { ProxyInfo pi = proxy.get().getProxyInfo(); Uri pacFileUrl = pi.getPacFileUrl(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (pacFileUrl != null && pacFileUrl != Uri.EMPTY) { String pacFile = downloadPacFile(activeNetwork, pacFileUrl); int listenPort = startHttpProxy(pacFile); ProxyInfo localPi = ProxyInfo.buildDirectProxy("localhost", listenPort); builder.setHttpProxy(localPi); } else { builder.setHttpProxy(pi); } } } Log.i(TAG, "Builder 15"); builder.setBlocking(true); return builder; } private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state); if (state == State.UP) { if (config == null) throw new BackendException(Reason.TUNNEL_MISSING_CONFIG); if (VpnService.prepare(context) != null) throw new BackendException(Reason.VPN_NOT_AUTHORIZED); final VpnService service; if (!vpnService.isDone()) { Log.d(TAG, "Requesting to start VpnService"); context.startService(new Intent(context, VpnService.class)); } try { service = vpnService.get(2, TimeUnit.SECONDS); } catch (final TimeoutException e) { final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN); be.initCause(e); throw be; } service.setOwner(this); if (currentTunnelHandle != -1) { Log.w(TAG, "Tunnel already up"); return; } activeNetwork = connectivityManager.getActiveNetwork(); if (activeNetwork == null) { Log.w(TAG, "Null activeNetwork"); activeNetwork = null; } else if (!connectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { Log.w(TAG, "VPN network is active, null activeNetwork"); activeNetwork = null; } final Resolver resolver = new Resolver(activeNetwork, connectivityManager.getLinkProperties(activeNetwork)); 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; // FIXME tunnel.onEndpointChange(peer.getPublicKey(), ep); Log.i(TAG, "onEndpointChange " + peer.getPublicKey() + ", " + ep); if (ep.getResolved(resolver, true).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(resolver); // Create the vpn tunnel with android API final VpnService.Builder builder = getBuilder(tunnel.getName(), config, service, null); try (final ParcelFileDescriptor tun = builder.establish()) { if (tun == null) throw new BackendException(Reason.TUN_CREATION_ERROR); Log.d(TAG, "Go backend " + wgVersion()); currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig); } if (currentTunnelHandle < 0) throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle); currentTunnel = tunnel; currentConfig = config; service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle)); obtainDhcpLease = true; thread = new HandlerThread("GoBackend HandlerThread"); thread.start(); handler = new Handler(thread.getLooper()) { public void handleMessage(Message msg) { switch (msg.what) { case MSG_DHCP_EXPIRE: Log.w(TAG, "DHCP expire: " + ((activeNetwork != null) ? activeNetwork : "disabled")); try { if (activeNetwork != null) { Log.w(TAG, "DHCP before"); Dhcp(service); // Renew addresses Log.w(TAG, "DHCP after"); } else { Log.w(TAG, "DHCP delay obtain lease"); obtainDhcpLease = true; } } catch (Exception ex) { Log.e(TAG, "DHCP failed: " + ex); } break; case MSG_CAPABILITIES_CHANGED: Log.w(TAG, "Msg: capabilities changed: " + activeNetworkCapabilities); capabilitiesChanged(activeNetworkCapabilities); break; default: Log.w(TAG, "Unknown message: " + msg.what); break; } } }; NetworkRequest req = new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build(); connectivityManager.requestNetwork(req, myNetworkCallback); NetworkRequest vpnReq = new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_VPN).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build(); vpnNetworkCallback = new VpnNetworkCallback(service); connectivityManager.requestNetwork(vpnReq, vpnNetworkCallback); } else { if (currentTunnelHandle == -1) { Log.w(TAG, "Tunnel already down"); return; } int handleToClose = currentTunnelHandle; currentTunnel = null; currentTunnelHandle = -1; currentConfig = null; AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); am.cancel(alarmListener); if (thread != null) { handler = null; thread.quit(); thread = null; } if (bgp != null) { bgp.stopServer(); bgp = null; } stopHttpProxy(); if (vpnNetworkCallback != null) connectivityManager.unregisterNetworkCallback(vpnNetworkCallback); vpnNetworkCallback = null; connectivityManager.unregisterNetworkCallback(myNetworkCallback); activeNetwork = null; wgTurnOff(handleToClose); try { vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf(); } catch (final TimeoutException ignored) { } } tunnel.onStateChange(state); } /** * 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(); } // TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture. private static final class GhettoCompletableFuture { private final LinkedBlockingQueue completion = new LinkedBlockingQueue<>(1); private final FutureTask result = new FutureTask<>(completion::peek); public boolean complete(final V value) { final boolean offered = completion.offer(value); if (offered) result.run(); return offered; } public V get() throws ExecutionException, InterruptedException { return result.get(); } public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException { return result.get(timeout, unit); } public boolean isDone() { return !completion.isEmpty(); } public GhettoCompletableFuture newIncompleteFuture() { return new GhettoCompletableFuture<>(); } } /** * {@link android.net.VpnService} implementation for {@link GoBackend} */ public static class VpnService extends android.net.VpnService { @Nullable private GoBackend owner; public Builder getBuilder() { return new Builder(); } @Override public void onCreate() { vpnService.complete(this); super.onCreate(); } @Override public void onDestroy() { if (owner != null) { if (owner.bgp != null) { owner.bgp.stopServer(); owner.bgp = null; } AlarmManager am = (AlarmManager)owner.context.getSystemService(Context.ALARM_SERVICE); am.cancel(owner.alarmListener); if (owner.thread != null) { owner.handler = null; owner.thread.quit(); owner.thread = null; } owner.stopHttpProxy(); final Tunnel tunnel = owner.currentTunnel; if (tunnel != null) { if (owner.currentTunnelHandle != -1) { if (owner.vpnNetworkCallback != null) owner.connectivityManager.unregisterNetworkCallback(owner.vpnNetworkCallback); owner.vpnNetworkCallback = null; owner.connectivityManager.unregisterNetworkCallback(owner.myNetworkCallback); owner.activeNetwork = null; wgTurnOff(owner.currentTunnelHandle); } owner.currentTunnel = null; owner.currentTunnelHandle = -1; owner.currentConfig = null; tunnel.onStateChange(State.DOWN); } } vpnService = vpnService.newIncompleteFuture(); super.onDestroy(); } @Override public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) { vpnService.complete(this); if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) { Log.d(TAG, "Service started by Always-on VPN feature"); if (alwaysOnCallback != null) alwaysOnCallback.alwaysOnTriggered(); } return super.onStartCommand(intent, flags, startId); } public void setOwner(final GoBackend owner) { this.owner = owner; } } private class VpnNetworkCallback extends ConnectivityManager.NetworkCallback { private VpnService service; public VpnNetworkCallback(VpnService service) { this.service = service; } @Override public void onAvailable(Network network) { Log.w(TAG, "VPN onAvailable: " + network); if (obtainDhcpLease && activeNetwork != null) { Log.w(TAG, "Obtaindhcplease"); try { Log.w(TAG, "Before Dhcp"); Dhcp(service); Log.w(TAG, "After Dhcp"); } catch (Exception ex) { Log.e(TAG, "DHCP failed: " + ex); } } } } private class MyNetworkCallback extends ConnectivityManager.NetworkCallback { @Override public void onAvailable(Network network) { activeNetwork = network; Log.w(TAG, "onAvailable: " + activeNetwork); } @Override public void onLosing(Network network, int maxMsToLive) { Log.w(TAG, "onLosing: " + network + " maxMsToLive: " + maxMsToLive); // TODO release DHCP? } @Override public void onLost(Network network) { Log.w(TAG, "onLost: " + network + " (" + activeNetwork + ")"); if (network.equals(activeNetwork)) { activeNetwork = null; } } @Override public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { Log.w(TAG, "onLinkPropertiesChanged: " + network + " is default:" + (network.equals(activeNetwork))); if (network.equals(activeNetwork) && currentConfig != null && currentTunnelHandle > -1) { final Resolver resolver = new Resolver(network, linkProperties); final String goConfig = currentConfig.toWgEndpointsUserspaceString(resolver); Log.w(TAG, "is default network, config:" + goConfig); LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); TunnelHandle tunnel = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build(); IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(tunnel).setConfig(goConfig).build(); IpcSetResponse resp = stub.ipcSet(request); for (final Peer peer : currentConfig.getPeers()) { final InetEndpoint ep = peer.getEndpoint().orElse(null); if (ep == null) continue; currentTunnel.onEndpointChange(peer.getPublicKey(), ep); } } } @Override public void onCapabilitiesChanged (Network network, NetworkCapabilities networkCapabilities) { boolean hasCapNotMetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); Log.w(TAG, "onCapabilitiesChanged: " + network + " is not metered:" + (hasCapNotMetered)); if (network.equals(activeNetwork) && handler != null) { activeNetworkCapabilities = networkCapabilities; handler.sendEmptyMessage(MSG_CAPABILITIES_CHANGED); } } } private AlarmManager.OnAlarmListener alarmListener = new AlarmManager.OnAlarmListener() { @Override public void onAlarm() { if (handler != null) { handler.sendEmptyMessage(MSG_DHCP_EXPIRE); } } }; }