diff options
Diffstat (limited to 'tunnel/src')
14 files changed, 1106 insertions, 44 deletions
diff --git a/tunnel/src/main/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml index dc0d6c7f..0d97f321 100644 --- a/tunnel/src/main/AndroidManifest.xml +++ b/tunnel/src/main/AndroidManifest.xml @@ -3,14 +3,13 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.wireguard.android.tunnel"> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <service android:name="com.wireguard.android.backend.GoBackend$VpnService" android:permission="android.permission.BIND_VPN_SERVICE" - android:exported="true"> + android:exported="false"> <intent-filter> <action android:name="android.net.VpnService" /> </intent-filter> 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 3d0886cf..e8148c04 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -7,30 +7,81 @@ package com.wireguard.android.backend; 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.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.Process; 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.GetConnectionOwnerUidResponse; +import com.wireguard.android.backend.gen.IpcSetRequest; +import com.wireguard.android.backend.gen.IpcSetResponse; +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.okhttp.OkHttpChannelBuilder; +import io.grpc.stub.StreamObserver; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; 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.nio.ByteOrder; import java.util.Collections; +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; @@ -49,6 +100,10 @@ public final class GoBackend implements Backend { @Nullable private Config currentConfig; @Nullable private Tunnel currentTunnel; private int currentTunnelHandle = -1; + private ManagedChannel channel; + private ConnectivityManager connectivityManager; + private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback(); + @Nullable private Network activeNetwork; /** * Public constructor for GoBackend. @@ -58,6 +113,15 @@ public final class GoBackend implements Backend { 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)); + ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress("localhost", 10000).usePlaintext(); + LocalSocketAddress address = new LocalSocketAddress(socketName, LocalSocketAddress.Namespace.FILESYSTEM); + SocketFactory socketFactory = new UnixDomainSocketFactory(address); + ((OkHttpChannelBuilder) channelBuilder).socketFactory(socketFactory); + channel = channelBuilder.build(); } /** @@ -82,6 +146,8 @@ public final class GoBackend implements Backend { private static native String wgVersion(); + private static native int wgStartGrpc(String sockName); + /** * Method to get the names of running tunnels. * @@ -125,10 +191,17 @@ public final class GoBackend implements Backend { Key key = null; long rx = 0; long tx = 0; + long handshakeSec = 0; + int handshakeNSec = 0; for (final String line : config.split("\\n")) { if (line.startsWith("public_key=")) { - if (key != null) - stats.add(key, rx, tx); + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); + } + stats.add(key, rx, tx, handshake); + } rx = 0; tx = 0; try { @@ -152,10 +225,31 @@ 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 { + handshakeSec = Long.parseLong(line.substring(24)); + } catch (final NumberFormatException ignored) { + handshakeSec = 0; + } + } else if (line.startsWith("last_handshake_time_nsec=")) { + if (key == null) + continue; + try { + handshakeNSec = Integer.parseInt(line.substring(25)); + } catch (final NumberFormatException ignored) { + handshakeNSec = 0; + } } } - if (key != null) - stats.add(key, rx, tx); + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); + } + stats.add(key, rx, tx, handshake); + } return stats; } @@ -166,7 +260,10 @@ public final class GoBackend implements Backend { */ @Override public String getVersion() { - return wgVersion(); + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + VersionRequest request = VersionRequest.newBuilder().build(); + VersionResponse resp = stub.version(request); + return resp.getVersion(); } /** @@ -205,6 +302,130 @@ public final class GoBackend implements Backend { return getState(tunnel); } + private int startHttpProxy(Uri pacFileUrl) { + LibwgGrpc.LibwgStub asyncStub = LibwgGrpc.newStub(channel); + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + StartHttpProxyRequest.Builder reqBuilder = StartHttpProxyRequest.newBuilder(); + if (pacFileUrl != null && pacFileUrl != Uri.EMPTY) { + reqBuilder.setPacFileUrl(pacFileUrl.toString()); + } + + 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()); + 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 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<StreamObserver<ReverseRequest>> atomicRequestObserver = new AtomicReference<StreamObserver<ReverseRequest>>(); + // Throwable failed = null; + + StreamObserver<ReverseResponse> responseObserver = new StreamObserver<ReverseResponse>() { + @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<ReverseRequest> 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 void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state); @@ -237,13 +458,19 @@ public final class GoBackend implements Backend { } + activeNetwork = connectivityManager.getActiveNetwork(); + 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; - if (ep.getResolved().orElse(null) == null) { + 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); @@ -256,7 +483,7 @@ public final class GoBackend implements Backend { } // Build config - final String goConfig = config.toWgUserspaceString(); + final String goConfig = config.toWgUserspaceString(resolver); // Create the vpn tunnel with android API final VpnService.Builder builder = service.getBuilder(); @@ -299,6 +526,22 @@ public final class GoBackend implements Backend { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) service.setUnderlyingNetworks(null); + Optional<HttpProxy> 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) { + int listenPort = startHttpProxy(pacFileUrl); + ProxyInfo localPi = ProxyInfo.buildDirectProxy("localhost", listenPort); + builder.setHttpProxy(localPi); + } else { + builder.setHttpProxy(pi); + } + } + } + builder.setBlocking(true); try (final ParcelFileDescriptor tun = builder.establish()) { if (tun == null) @@ -314,6 +557,9 @@ public final class GoBackend implements Backend { service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle)); + + NetworkRequest req = new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build(); + connectivityManager.requestNetwork(req, myNetworkCallback); } else { if (currentTunnelHandle == -1) { Log.w(TAG, "Tunnel already down"); @@ -323,6 +569,9 @@ public final class GoBackend implements Backend { currentTunnel = null; currentTunnelHandle = -1; currentConfig = null; + stopHttpProxy(); + connectivityManager.unregisterNetworkCallback(myNetworkCallback); + activeNetwork = null; wgTurnOff(handleToClose); } @@ -385,10 +634,14 @@ public final class GoBackend implements Backend { @Override public void onDestroy() { if (owner != null) { + owner.stopHttpProxy(); final Tunnel tunnel = owner.currentTunnel; if (tunnel != null) { - if (owner.currentTunnelHandle != -1) + if (owner.currentTunnelHandle != -1) { + owner.connectivityManager.unregisterNetworkCallback(owner.myNetworkCallback); + owner.activeNetwork = null; wgTurnOff(owner.currentTunnelHandle); + } owner.currentTunnel = null; owner.currentTunnelHandle = -1; owner.currentConfig = null; @@ -414,4 +667,27 @@ public final class GoBackend implements Backend { this.owner = owner; } } + + private class MyNetworkCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + activeNetwork = network; + Log.w(TAG, "onAvailable: " + activeNetwork); + } + + @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.toWgUserspaceStringWithChangedEndpoints(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); + } + } + } } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/LocalSocketAdapter.java b/tunnel/src/main/java/com/wireguard/android/backend/LocalSocketAdapter.java new file mode 100644 index 00000000..bf027ae1 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/LocalSocketAdapter.java @@ -0,0 +1,325 @@ +/* + */ +package com.wireguard.android.backend; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketImplFactory; +import java.net.SocketOptions; +import java.nio.channels.SocketChannel; +import java.util.Vector; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.util.Log; + +/** + * Adaptor allows using a LocalSocket as a Socket. + */ +final class LocalSocketAdapter extends Socket { + private final LocalSocketAddress address; + private final LocalSocket unix; + private final SocketAddress localAddress; + private InetSocketAddress inetSocketAddress; + private InputStream is; + private OutputStream os; + + LocalSocketAdapter(LocalSocketAddress address) { + this.address = address; + this.localAddress = new InetSocketAddress(0); + unix = new LocalSocket(); + } + + LocalSocketAdapter(LocalSocketAddress address, InetSocketAddress inetAddress) { + this(address); + this.inetSocketAddress = inetAddress; + } + + private void throwUnsupportedOperationException() { + Log.i("helloworld", "Unsupported: " + Log.getStackTraceString(new Exception())); + throw new UnsupportedOperationException(); + } + + @Override + public void bind (SocketAddress bindpoint) { + throwUnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + unix.close(); + } + + @Override public void connect(SocketAddress endpoint) throws IOException { + this.inetSocketAddress = (InetSocketAddress) endpoint; + try { + unix.connect(address); + } catch (IOException e) { + Log.i("helloworld", "Error: " + e.toString()); + throw e; + } + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + this.inetSocketAddress = (InetSocketAddress) endpoint; + unix.connect(address, timeout); + } + + @Override + public SocketChannel getChannel() { + throwUnsupportedOperationException(); + return null; + } + + @Override public InetAddress getInetAddress() { + return inetSocketAddress.getAddress(); + } + + @Override + public InputStream getInputStream() throws IOException { + is = unix.getInputStream(); + return is; + } + + @Override + public boolean getKeepAlive() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public InetAddress getLocalAddress() { + throwUnsupportedOperationException(); + return null; + } + + @Override + public int getLocalPort() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public SocketAddress getLocalSocketAddress() { + //throwUnsupportedOperationException(); + return localAddress; + } + + @Override + public boolean getOOBInline() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public OutputStream getOutputStream() throws IOException { + if (os != null) + return os; + + OutputStream unixOs = unix.getOutputStream(); + os = new OutputStream() { + @Override + public void close() throws IOException { + // LocalSocket's default implementation closes the socket, + // which leaves readers of thes InputStream hanging. + // Instead shutdown input (and output) to release readers. + LocalSocketAdapter.this.shutdownInput(); + LocalSocketAdapter.this.shutdownOutput(); + } + + @Override + public void write (byte[] b) throws IOException { + unixOs.write(b); + } + + @Override + public void write (byte[] b, int off, int len) throws IOException { + unixOs.write(b, off, len); + } + + @Override + public void write (int b) throws IOException { + unixOs.write(b); + } + }; + return os; + } + + @Override + public int getPort() { + return inetSocketAddress.getPort(); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + try { + return unix.getReceiveBufferSize(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return inetSocketAddress; + } + + @Override + public boolean getReuseAddress() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public int getSendBufferSize() throws SocketException { + try { + return unix.getSendBufferSize(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public int getSoLinger() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public int getSoTimeout() throws SocketException { + try { + return unix.getSoTimeout(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public boolean getTcpNoDelay() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public int getTrafficClass() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public boolean isBound() { + return unix.isBound(); + } + + @Override + public boolean isClosed() { + return unix.isClosed(); + } + + @Override + public boolean isConnected() { + return unix.isConnected(); + } + + @Override + public boolean isInputShutdown() { + return unix.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return unix.isOutputShutdown(); + } + + @Override + public void sendUrgentData (int data) { + throwUnsupportedOperationException(); + } + + @Override + public void setKeepAlive (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setOOBInline (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setPerformancePreferences (int connectionTime, + int latency, + int bandwidth) { + throwUnsupportedOperationException(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + try { + unix.setReceiveBufferSize(size); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setReuseAddress (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + try { + unix.setSendBufferSize(size); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setSoLinger (boolean on, + int linger) { + throwUnsupportedOperationException(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + try { + unix.setSoTimeout(timeout); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setTcpNoDelay (boolean on) { + // Not relevant for local sockets. + } + + @Override + public void setTrafficClass (int tc) { + throwUnsupportedOperationException(); + } + + @Override + public void shutdownInput() throws IOException { + unix.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + unix.shutdownOutput(); + } + + @Override + public String toString() { + return unix.toString(); + } +} diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java index 5d658019..322e766f 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java @@ -11,6 +11,7 @@ import android.util.Pair; import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -19,7 +20,19 @@ import java.util.Map; */ @NonNullForAll public class Statistics { - private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); + private static class Stat { + long rx; + long tx; + LocalDateTime lastHandshake; + + Stat(long rx, long tx, LocalDateTime lastHandshake) { + this.rx = rx; + this.tx = tx; + this.lastHandshake = lastHandshake; + } + } + + private final Map<Key, Stat> peerBytes = new HashMap<>(); private long lastTouched = SystemClock.elapsedRealtime(); Statistics() { @@ -34,8 +47,8 @@ public class Statistics { * @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by * the provided {@link Key}. This value is in bytes. */ - void add(final Key key, final long rx, final long tx) { - peerBytes.put(key, Pair.create(rx, tx)); + void add(final Key key, final long rx, final long tx, final LocalDateTime lastHandshake) { + peerBytes.put(key, new Stat(rx, tx, lastHandshake)); lastTouched = SystemClock.elapsedRealtime(); } @@ -56,10 +69,10 @@ public class Statistics { * @return a long representing the number of bytes received by this peer. */ public long peerRx(final Key peer) { - final Pair<Long, Long> rxTx = peerBytes.get(peer); - if (rxTx == null) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.first; + return stat.rx; } /** @@ -70,10 +83,17 @@ public class Statistics { * @return a long representing the number of bytes transmitted by this peer. */ public long peerTx(final Key peer) { - final Pair<Long, Long> rxTx = peerBytes.get(peer); - if (rxTx == null) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.second; + return stat.tx; + } + + public LocalDateTime peerLastHandshake(final Key peer) { + final Stat stat = peerBytes.get(peer); + if (stat == null) + return null; + return stat.lastHandshake; } /** @@ -93,8 +113,8 @@ public class Statistics { */ public long totalRx() { long rx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - rx += val.first; + for (final Stat val : peerBytes.values()) { + rx += val.rx; } return rx; } @@ -106,8 +126,8 @@ public class Statistics { */ public long totalTx() { long tx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - tx += val.second; + for (final Stat val : peerBytes.values()) { + tx += val.tx; } return tx; } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/UnixDomainSocketFactory.java b/tunnel/src/main/java/com/wireguard/android/backend/UnixDomainSocketFactory.java new file mode 100644 index 00000000..427a19f1 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/UnixDomainSocketFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wireguard.android.backend; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import javax.net.SocketFactory; +import android.net.LocalSocketAddress; + +/** Impersonate TCP-style SocketFactory over UNIX domain sockets. */ +public final class UnixDomainSocketFactory extends SocketFactory { + private final LocalSocketAddress address; + + public UnixDomainSocketFactory(LocalSocketAddress address) { + this.address = address; + } + + @Override public Socket createSocket() throws IOException { + return new LocalSocketAdapter(address); + } + + @Override public Socket createSocket(String host, int port) throws IOException { + Socket result = createSocket(); + result.connect(new InetSocketAddress(host, port)); + return result; + } + + @Override public Socket createSocket( + String host, int port, InetAddress localHost, int localPort) throws IOException { + return createSocket(host, port); + } + + @Override public Socket createSocket(InetAddress host, int port) throws IOException { + Socket result = createSocket(); + result.connect(new InetSocketAddress(host, port)); + return result; + } + + @Override public Socket createSocket( + InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { + return createSocket(host, port); + } +} diff --git a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index 3121c996..d0dc6a46 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -93,7 +93,7 @@ public final class WgQuickBackend implements Backend { if (parts.length != 3) continue; try { - stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2])); + stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]), null); } catch (final Exception ignored) { } } diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java index 8766ce51..e5a94e89 100644 --- a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java +++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java @@ -8,6 +8,8 @@ package com.wireguard.config; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; +import java.net.MalformedURLException; + import androidx.annotation.Nullable; @NonNullForAll @@ -44,6 +46,12 @@ public class BadConfigException extends Exception { } public BadConfigException(final Section section, final Location location, + @Nullable final CharSequence text, + final MalformedURLException cause) { + this(section, location, Reason.INVALID_VALUE, text, cause); + } + + public BadConfigException(final Section section, final Location location, final ParseException cause) { this(section, location, Reason.INVALID_VALUE, cause.getText(), cause); } @@ -73,6 +81,7 @@ public class BadConfigException extends Exception { ENDPOINT("Endpoint"), EXCLUDED_APPLICATIONS("ExcludedApplications"), INCLUDED_APPLICATIONS("IncludedApplications"), + HTTP_PROXY("HttpProxy"), LISTEN_PORT("ListenPort"), MTU("MTU"), PERSISTENT_KEEPALIVE("PersistentKeepalive"), diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java index 807ebec8..ea05e9c8 100644 --- a/tunnel/src/main/java/com/wireguard/config/Config.java +++ b/tunnel/src/main/java/com/wireguard/config/Config.java @@ -9,6 +9,7 @@ import com.wireguard.config.BadConfigException.Location; import com.wireguard.config.BadConfigException.Reason; import com.wireguard.config.BadConfigException.Section; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.io.BufferedReader; import java.io.IOException; @@ -173,12 +174,19 @@ public final class Config { * * @return the {@code Config} represented as a series of "key=value" lines */ - public String toWgUserspaceString() { + public String toWgUserspaceString(Resolver resolver) { final StringBuilder sb = new StringBuilder(); sb.append(interfaze.toWgUserspaceString()); sb.append("replace_peers=true\n"); for (final Peer peer : peers) - sb.append(peer.toWgUserspaceString()); + sb.append(peer.toWgUserspaceString(resolver)); + return sb.toString(); + } + + public String toWgUserspaceStringWithChangedEndpoints(Resolver resolver) { + final StringBuilder sb = new StringBuilder(); + for (final Peer peer : peers) + sb.append(peer.toWgUserspaceStringWithChangedEndpoint(resolver)); return sb.toString(); } diff --git a/tunnel/src/main/java/com/wireguard/config/HttpProxy.java b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java new file mode 100644 index 00000000..d45914f8 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2022 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.config; + +import com.wireguard.config.BadConfigException.Location; +import com.wireguard.config.BadConfigException.Section; +import com.wireguard.util.NonNullForAll; + +import java.net.MalformedURLException; +import java.net.URL; + +import android.net.ProxyInfo; +import android.net.Uri; + +@NonNullForAll +public final class HttpProxy { + public static final int DEFAULT_PROXY_PORT = 8080; + + private ProxyInfo pi; + + protected HttpProxy(ProxyInfo pi) { + this.pi = pi; + } + + public ProxyInfo getProxyInfo() { + return pi; + } + + public String getHost() { + return pi.getHost(); + } + + public Uri getPacFileUrl() { + return pi.getPacFileUrl(); + } + + public int getPort() { + return pi.getPort(); + } + + public static HttpProxy parse(final String httpProxy) throws BadConfigException { + try { + if (httpProxy.startsWith("pac:")) { + return new HttpProxy(ProxyInfo.buildPacProxy(Uri.parse(httpProxy.substring(4)))); + } else { + final String urlStr; + if (!httpProxy.contains("://")) { + urlStr = "http://" + httpProxy; + } else { + urlStr = httpProxy; + } + URL url = new URL(urlStr); + return new HttpProxy(ProxyInfo.buildDirectProxy(url.getHost(), url.getPort() <= 0 ? DEFAULT_PROXY_PORT : url.getPort())); + } + } catch (final MalformedURLException e) { + throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, httpProxy, e); + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (pi.getPacFileUrl() != null && pi.getPacFileUrl() != Uri.EMPTY) + sb.append("pac:").append(pi.getPacFileUrl()); + else { + sb.append("http://").append(pi.getHost()).append(':'); + if (pi.getPort() <= 0) + sb.append(DEFAULT_PROXY_PORT); + else + sb.append(pi.getPort()); + } + + return sb.toString(); + } +} diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java index 66855f11..abd888c8 100644 --- a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java +++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java @@ -6,6 +6,7 @@ package com.wireguard.config; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.net.Inet4Address; import java.net.InetAddress; @@ -87,24 +88,34 @@ public final class InetEndpoint { * * @return the resolved endpoint, or {@link Optional#empty()} */ - public Optional<InetEndpoint> getResolved() { - if (isResolved) + public Optional<InetEndpoint> getResolved(Resolver resolver) { + return getResolved(resolver, false, false); + } + + public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) { + return getResolved(resolver, force, false); + } + + public Optional<InetEndpoint> getResolvedIfChanged(Resolver resolver) { + return getResolved(resolver, true, true); + } + + public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force, Boolean ifChanged) { + if (!force && 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) { + if (force || 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); + InetAddress address = resolver.resolve(host); + InetEndpoint resolvedNow = new InetEndpoint(address.getHostAddress(), true, port); lastResolution = Instant.now(); + + if (ifChanged && resolvedNow.equals(resolved)) { + return Optional.empty(); + } + + resolved = resolvedNow; } catch (final UnknownHostException e) { resolved = null; } diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java index 694f313a..a4fa2a19 100644 --- a/tunnel/src/main/java/com/wireguard/config/Interface.java +++ b/tunnel/src/main/java/com/wireguard/config/Interface.java @@ -46,6 +46,7 @@ public final class Interface { private final KeyPair keyPair; private final Optional<Integer> listenPort; private final Optional<Integer> mtu; + private final Optional<HttpProxy> httpProxy; private Interface(final Builder builder) { // Defensively copy to ensure immutability even if the Builder is reused. @@ -57,6 +58,7 @@ public final class Interface { keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key"); listenPort = builder.listenPort; mtu = builder.mtu; + httpProxy = builder.httpProxy; } /** @@ -92,6 +94,9 @@ public final class Interface { case "mtu": builder.parseMtu(attribute.getValue()); break; + case "httpproxy": + builder.parseHttpProxy(attribute.getValue()); + break; case "privatekey": builder.parsePrivateKey(attribute.getValue()); break; @@ -115,7 +120,8 @@ public final class Interface { && includedApplications.equals(other.includedApplications) && keyPair.equals(other.keyPair) && listenPort.equals(other.listenPort) - && mtu.equals(other.mtu); + && mtu.equals(other.mtu) + && httpProxy.equals(other.httpProxy); } /** @@ -195,6 +201,15 @@ public final class Interface { return mtu; } + /** + * Returns the HTTP proxy used for the WireGuard interface. + * + * @return the HTTP proxy, or {@code Optional.empty()} if none is configured + */ + public Optional<HttpProxy> getHttpProxy() { + return httpProxy; + } + @Override public int hashCode() { int hash = 1; @@ -205,6 +220,7 @@ public final class Interface { hash = 31 * hash + keyPair.hashCode(); hash = 31 * hash + listenPort.hashCode(); hash = 31 * hash + mtu.hashCode(); + hash = 31 * hash + httpProxy.hashCode(); return hash; } @@ -244,6 +260,7 @@ public final class Interface { sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n'); listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n')); mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n')); + httpProxy.ifPresent(p -> sb.append("HttpProxy = ").append(p).append('\n')); sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n'); return sb.toString(); } @@ -279,6 +296,8 @@ public final class Interface { private Optional<Integer> listenPort = Optional.empty(); // Defaults to not present. private Optional<Integer> mtu = Optional.empty(); + // Defaults to not present. + private Optional<HttpProxy> httpProxy = Optional.empty(); public Builder addAddress(final InetNetwork address) { addresses.add(address); @@ -391,6 +410,10 @@ public final class Interface { } } + public Builder parseHttpProxy(final String httpProxy) throws BadConfigException { + return setHttpProxy(HttpProxy.parse(httpProxy)); + } + public Builder parsePrivateKey(final String privateKey) throws BadConfigException { try { return setKeyPair(new KeyPair(Key.fromBase64(privateKey))); @@ -419,5 +442,13 @@ public final class Interface { this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu); return this; } + + public Builder setHttpProxy(final HttpProxy httpProxy) throws BadConfigException { + if (httpProxy == null) + throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, + Reason.INVALID_VALUE, String.valueOf(httpProxy)); + this.httpProxy = httpProxy == null ? Optional.empty() : Optional.of(httpProxy); + return this; + } } } diff --git a/tunnel/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java index 9b87b397..3cf5dc15 100644 --- a/tunnel/src/main/java/com/wireguard/config/Peer.java +++ b/tunnel/src/main/java/com/wireguard/config/Peer.java @@ -11,6 +11,7 @@ import com.wireguard.config.BadConfigException.Section; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.util.Collection; import java.util.Collections; @@ -190,18 +191,26 @@ public final class Peer { * * @return the {@code Peer} represented as a series of "key=value" lines */ - public String toWgUserspaceString() { + public String toWgUserspaceString(Resolver resolver) { 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')); + endpoint.flatMap(ep -> ep.getResolved(resolver)).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(); } + public String toWgUserspaceStringWithChangedEndpoint(Resolver resolver) { + 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'); + endpoint.flatMap(ep -> ep.getResolved(resolver, true)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n')); + return sb.toString(); + } + @SuppressWarnings("UnusedReturnValue") public static final class Builder { // See wg(8) diff --git a/tunnel/src/main/java/com/wireguard/util/Resolver.java b/tunnel/src/main/java/com/wireguard/util/Resolver.java new file mode 100644 index 00000000..f401b584 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2023 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.util; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +import android.net.IpPrefix; +import android.net.LinkProperties; +import android.net.Network; +import android.util.Log; + +import androidx.annotation.Nullable; + +@NonNullForAll +public class Resolver { + private static final String TAG = "WireGuard/Resolver"; + @Nullable private final Network network; + @Nullable private final LinkProperties linkProps; + @Nullable private IpPrefix nat64Prefix; + + public Resolver(Network network, LinkProperties linkProps) { + this.network = network; + this.linkProps = linkProps; + if (linkProps != null) { + this.nat64Prefix = linkProps.getNat64Prefix(); + } + } + + static boolean isULA(Inet6Address addr) { + byte[] raw = addr.getAddress(); + return ((raw[0] & 0xfe) == 0xfc); + } + + boolean isWithinNAT64Prefix(Inet6Address address) { + if (nat64Prefix == null) + return false; + + int prefixLength = nat64Prefix.getPrefixLength(); + byte[] rawAddr = address.getAddress(); + byte[] rawPrefix = nat64Prefix.getRawAddress(); + + for (int i=0; i < prefixLength/8; i++) { + if (rawAddr[i] != rawPrefix[i]) + return false; + } + + return true; + } + + boolean isPreferredIPv6(Inet6Address local, Inet6Address remote) { + if (linkProps == null) { + // Prefer IPv4 if there are not link properties that can + // be tested. + return false; + } + + // * Prefer IPv4 if local or remote address is ULA + // * Prefer IPv4 if remote IPv6 is within NAT64 prefix. + // * Otherwise prefer IPv6 + boolean isLocalULA = isULA(local); + boolean isRemoteULA = isULA(remote); + + if (isLocalULA || isRemoteULA) { + return false; + } + + if (isWithinNAT64Prefix(remote)) { + return false; + } + + return true; + } + + public InetAddress resolve(String host) throws UnknownHostException { + final InetAddress[] candidates = network != null ? network.getAllByName(host) : InetAddress.getAllByName(host); + InetAddress address = candidates[0]; + for (final InetAddress candidate : candidates) { + DatagramSocket sock; + + try { + sock = new DatagramSocket(); + if (network != null) { + network.bindSocket(sock); + } + } catch (SocketException e) { + // Return first candidate as fallback + Log.w(TAG, "DatagramSocket failed, fallback to: \"" + address); + return address; + } catch (IOException e) { + // Return first candidate as fallback + Log.w(TAG, "BindSocket failed, fallback to: \"" + address); + return address; + } + + sock.connect(candidate, 51820); + + if (sock.getLocalAddress().isAnyLocalAddress()) { + // Connect didn't find a local address. + Log.w(TAG, "No local address"); + continue; + } + + Log.w(TAG, "Local address: " + sock.getLocalAddress()); + + if (candidate instanceof Inet4Address) { + // Accept IPv4 as preferred address. + address = candidate; + break; + } + + Inet6Address local = (Inet6Address)sock.getLocalAddress(); + InetSocketAddress remoteSockAddr = (InetSocketAddress)sock.getRemoteSocketAddress(); + Inet6Address remote = (Inet6Address)remoteSockAddr.getAddress(); + sock.close(); + + if (isPreferredIPv6(local, remote)) { + address = candidate; + break; + } + } + Log.w(TAG, "Resolved \"" + host + "\" to: " + address); + return address; + } +} diff --git a/tunnel/src/main/proto/libwg.proto b/tunnel/src/main/proto/libwg.proto new file mode 100644 index 00000000..e633ea46 --- /dev/null +++ b/tunnel/src/main/proto/libwg.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = 'com.wireguard.android.backend.gen'; +option java_outer_classname = "LibwgProto"; +option java_generic_services = true; +option go_package = 'golang.zx2c4.com/wireguard/android/gen'; + +package api; + +service Libwg { + rpc StopGrpc(StopGrpcRequest) returns (StopGrpcResponse); + rpc Version(VersionRequest) returns (VersionResponse); + rpc StartHttpProxy(StartHttpProxyRequest) returns (StartHttpProxyResponse); + rpc StopHttpProxy(StopHttpProxyRequest) returns (StopHttpProxyResponse); + rpc Reverse(stream ReverseRequest) returns (stream ReverseResponse); + rpc IpcSet(IpcSetRequest) returns (IpcSetResponse); +} + +message TunnelHandle { int32 handle = 1; } + +message Error { + enum Code { + NO_ERROR = 0; + UNSPECIFIED = 1; + INVALID_PROTOCOL_BUFFER = 2; + INVALID_RESPONSE = 3; + } + Code code = 1; + string message = 2; +} + +message InetAddress { + bytes address = 1; +} + +message InetSocketAddress { + InetAddress address = 1; + uint32 port = 2; +} + +message StopGrpcRequest { +} + +message StopGrpcResponse { +} + +message VersionRequest { +} + +message VersionResponse { + string version = 1; +} + +message StartHttpProxyRequest { + string pacFileUrl = 1; +} + +message StartHttpProxyResponse { + uint32 listen_port = 1; + Error error = 2; +} + +message StopHttpProxyRequest { +} + +message StopHttpProxyResponse { + Error error = 1; +} + +message ReverseRequest { + oneof response { + GetConnectionOwnerUidResponse uid = 1; + } +} + +message ReverseResponse { + oneof request { + GetConnectionOwnerUidRequest uid = 1; + } +} + +message GetConnectionOwnerUidRequest { + // ConnectivityManager.getConnectionOwnerUid(int protocol, + // InetSocketAddress local, InetSocketAddress remote) + int32 protocol = 1; + InetSocketAddress local = 2; + InetSocketAddress remote = 3; +} + +message GetConnectionOwnerUidResponse { + int32 uid = 1; + string package = 2; // context.getPackageManager().getNameForUid() +} + +message IpcSetRequest { + TunnelHandle tunnel = 1; + string config = 2; +} + +message IpcSetResponse { + Error error = 1; +} |