diff options
47 files changed, 3763 insertions, 127 deletions
diff --git a/gradle.properties b/gradle.properties index efc1fe18..3e22e0f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ wireguardVersionCode=510 wireguardVersionName=1.0.20231018 wireguardPackageName=com.wireguard.android +wireguardApplicationID=eu.m7n.wireguard.android # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38e874ca..605e53a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,11 @@ [versions] agp = "8.2.0-beta06" +grpc = "1.59.1" grgit = "5.2.1" kotlin = "1.9.0" +protobuf = "0.9.4" +protoc = "3.25.1" +protocgengrpc = '1.59.1' [libraries] androidx-activity-ktx = "androidx.activity:activity-ktx:1.8.0" @@ -11,6 +15,7 @@ androidx-biometric = "androidx.biometric:biometric:1.1.0" androidx-collection = "androidx.collection:collection:1.3.0" androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0" +androidx-core = "androidx.core:core:1.12.0" androidx-core-ktx = "androidx.core:core-ktx:1.12.0" androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0" androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.6.1" @@ -18,6 +23,11 @@ androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.1" desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3" google-material = "com.google.android.material:material:1.10.0" +grpc-android = "io.grpc:grpc-android:1.59.1" +grpc-okhttp = "io.grpc:grpc-okhttp:1.59.1" +grpc-protobuf-lite = "io.grpc:grpc-protobuf-lite:1.59.1" +grpc-stub = "io.grpc:grpc-stub:1.59.1" +javax-annotation-api = "javax.annotation:javax.annotation-api:1.3.2" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0" @@ -27,5 +37,6 @@ zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0" ajoberstar-grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } +google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } diff --git a/keystore/debug.keystore b/keystore/debug.keystore Binary files differnew file mode 100644 index 00000000..c2655107 --- /dev/null +++ b/keystore/debug.keystore diff --git a/settings.gradle.kts b/settings.gradle.kts index 91bc0b90..e87d2a0d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,5 @@ dependencyResolutionManagement { rootProject.name = "wireguard-android" include(":tunnel") +include(":bgp-java") include(":ui") diff --git a/tunnel/build.gradle.kts b/tunnel/build.gradle.kts index 589d72da..625959e6 100644 --- a/tunnel/build.gradle.kts +++ b/tunnel/build.gradle.kts @@ -1,5 +1,6 @@ @file:Suppress("UnstableApiUsage") +import com.google.protobuf.gradle.* import org.gradle.api.tasks.testing.logging.TestLogEvent val pkg: String = providers.gradleProperty("wireguardPackageName").get() @@ -7,6 +8,7 @@ val pkg: String = providers.gradleProperty("wireguardPackageName").get() plugins { alias(libs.plugins.android.library) `maven-publish` + alias(libs.plugins.google.protobuf) signing } @@ -65,12 +67,70 @@ android { } dependencies { + implementation(project(":bgp-java")) implementation(libs.androidx.annotation) implementation(libs.androidx.collection) + implementation(libs.androidx.core) + implementation(libs.grpc.android) + implementation(libs.grpc.okhttp) + implementation(libs.grpc.protobuf.lite) + implementation(libs.grpc.stub) + compileOnly(libs.javax.annotation.api) compileOnly(libs.jsr305) testImplementation(libs.junit) } +protobuf { + protoc { + // The artifact spec for the Protobuf Compiler + artifact = "com.google.protobuf:protoc:${libs.versions.protoc.get()}" + } + plugins { + // Optional: an artifact spec for a protoc plugin, with "grpc" as + // the identifier, which can be referred to in the "plugins" + // container of the "generateProtoTasks" closure. + id("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.protocgengrpc.get()}" + } + id("java") { + } + } + generateProtoTasks { + all().forEach { task -> + task.plugins{ + id("grpc") { + option("lite") + } + id("java") { + option("lite") + } + } + } + } +} + +afterEvaluate({ -> + // All custom configurations created by the protobuf plugin, + // are only available at this point. + var protoc = configurations.named("protobufToolsLocator_protoc") + var protocCache = "${gradle.gradleUserHomeDir}/caches/protoc-bin-${libs.versions.protoc.get()}" + + tasks.register("copyProtoc", Copy::class) { + // Used by tunnel/tools/libwg-go/Makefile run in tools/CMakeLists.txt + from(protoc) + into(protocCache) + rename("protoc-.*", "protoc") + setFileMode(7 * 64 + 7 * 8 + 5) // 0775 + } + + tasks.named("preBuild").get().dependsOn("copyProtoc") + + // Extract duration.proto used by external library in libwg.proto + tasks.named("preDebugBuild").get().dependsOn(tasks.named("extractIncludeDebugProto").get()) + tasks.named("preReleaseBuild").get().dependsOn(tasks.named("extractIncludeReleaseProto").get()) + println("done afterEvaluate") +}) + publishing { publications { register<MavenPublication>("release") { diff --git a/tunnel/src/main/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml index 99509a20..bbefdb1c 100644 --- a/tunnel/src/main/AndroidManifest.xml +++ b/tunnel/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ <service android:name="com.wireguard.android.backend.GoBackend$VpnService" android:permission="android.permission.BIND_VPN_SERVICE" - android:exported="false"> + android:exported="false" + android:foregroundServiceType="systemExempted"> <intent-filter> <action android:name="android.net.VpnService" /> </intent-filter> diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Backend.java b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java index edf98b9e..7e40da3f 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Backend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java @@ -6,8 +6,12 @@ package com.wireguard.android.backend; import com.wireguard.config.Config; +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.InetNetwork; +import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; +import java.util.List; import java.util.Set; import androidx.annotation.Nullable; @@ -64,4 +68,10 @@ public interface Backend { * @throws Exception Exception raised while changing state. */ Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception; + + void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks); + + void setPeer(Tunnel tunnel, Key publicKey, @Nullable InetEndpoint endpoint, + Integer persistentKeepalive, + List<InetNetwork> addAllowedIps, List<InetNetwork> removeAllowedIps); } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java new file mode 100644 index 00000000..092b91b7 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java @@ -0,0 +1,287 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.backend; + +import android.net.TrafficStats; +import android.util.Log; + +import com.lumaserv.bgp.BGPListener; +import com.lumaserv.bgp.BGPServer; +import com.lumaserv.bgp.BGPSession; +import com.lumaserv.bgp.BGPSessionConfiguration; +import com.lumaserv.bgp.protocol.AFI; +import com.lumaserv.bgp.protocol.BGPPacket; +import com.lumaserv.bgp.protocol.IPPrefix; +import com.lumaserv.bgp.protocol.SAFI; +import com.lumaserv.bgp.protocol.attribute.ASPathAttribute; +import com.lumaserv.bgp.protocol.attribute.MPReachableNLRIAttribute; +import com.lumaserv.bgp.protocol.attribute.NextHopAttribute; +import com.lumaserv.bgp.protocol.attribute.OriginAttribute; +import com.lumaserv.bgp.protocol.attribute.PathAttribute; +import com.lumaserv.bgp.protocol.attribute.TunnelEncapsAttribute; +import com.lumaserv.bgp.protocol.message.BGPUpdate; + +import com.wireguard.android.backend.Backend; +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.InetNetwork; +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; + +import io.grpc.ManagedChannel; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +import javax.net.SocketFactory; + +public class Bgp implements BGPListener { + private static final String TAG = "WireGuard/Bgp"; + private static final String SESSION = "demosession"; + private static final int MY_ASN = (int)4200000201L; + private static final int REMOTE_ASN = (int)4200000010L; + private static final String REMOTE_ADDR = "10.49.32.1"; + private static final String REMOTE_ID = "10.49.160.1"; + private static final String LOCAL_ID = "10.49.33.218"; + private static final int PORT = 0; + private static final int STATS_TAG = 1; // FIXME + + private final Backend backend; + private final ManagedChannel channel; + private final Tunnel tunnel; + private final int tunnelHandle; + private BGPServer server; + + public Bgp(Backend backend, ManagedChannel channel, Tunnel tunnel, int tunnelHandle) { + this.backend = backend; + this.channel = channel; + this.tunnel = tunnel; + this.tunnelHandle = tunnelHandle; + } + + @Override + public void onOpen(BGPSession session) { + // DO WHAT YOU WANT + Log.i(TAG, "onOpen"); + + // BGPUpdate update; + // { + // List<IPPrefix> prefixes = new ArrayList<>(1); + // prefixes.add(new IPPrefix(new byte[]{10, 49, 124, 105}, 32)); + // List<PathAttribute> attrs = new ArrayList<>(); + // attrs.add(new OriginAttribute(OriginAttribute.Origin.IGP)); + // attrs.add(new NextHopAttribute().setAddress(new byte[]{10, 49, 125, 105})); + // ASPathAttribute.Segment seg = new ASPathAttribute.Segment(); + // seg.setType(ASPathAttribute.Segment.Type.SEQUENCE); + // seg.getAsns().add(MY_ASN); + // ASPathAttribute asPath = new ASPathAttribute(session); + // asPath.getSegments().add(seg); + // attrs.add(asPath); + // update = new BGPUpdate().setAttributes(attrs).setPrefixes(prefixes); + // } + + // BGPUpdate update2; + // try { + // List<PathAttribute> attrs = new ArrayList<>(); + // attrs.add(new OriginAttribute(OriginAttribute.Origin.IGP)); + // ASPathAttribute.Segment seg = new ASPathAttribute.Segment(); + // seg.setType(ASPathAttribute.Segment.Type.SEQUENCE); + // seg.getAsns().add(MY_ASN); + // ASPathAttribute asPath = new ASPathAttribute(session); + // List<IPPrefix> prefixes = new ArrayList<>(1); + // prefixes.add(new IPPrefix(new byte[]{0x20, 0x1, 0x04, 0x70, (byte)0xdf, (byte)0xae, 0x63, 0, 0, 0, 0, 0, 0, 0, 0x01, 0x05}, 128)); + // MPReachableNLRIAttribute mpr = new MPReachableNLRIAttribute(); + // mpr.setAfi(AFI.IPV6).setSafi(SAFI.UNICAST).setNextHop(InetAddress.getByName("2001:470:dfae:6300::1:105")).setNlriPrefixes(prefixes); + // attrs.add(mpr); + // asPath.getSegments().add(seg); + // attrs.add(asPath); + // update2 = new BGPUpdate().setAttributes(attrs); + // } catch (UnknownHostException ex) { + // throw new RuntimeException(ex); + // } + + // try { + // session.sendUpdate(update); + // session.sendUpdate(update2); + // } catch (IOException ex) { + // throw new RuntimeException(ex); + // } + } + + @Override + public void onUpdate(BGPSession session, BGPUpdate update) { + // DO WHAT YOU WANT + Log.i(TAG, "onUpdate: " + update.getPrefixes() + ",-" + update.getWithdrawnPrefixes() + "," + update.getAttributes()); + MPReachableNLRIAttribute mpr = null; + TunnelEncapsAttribute te = null; + + for (PathAttribute attr: update.getAttributes()) { + if (attr instanceof TunnelEncapsAttribute) { + te = (TunnelEncapsAttribute)attr; + } else if (attr instanceof MPReachableNLRIAttribute) { + mpr = (MPReachableNLRIAttribute)attr; + } + } + + if (te == null) { + return; + } + + TunnelEncapsAttribute.WireGuard wg = null; + TunnelEncapsAttribute.Color col = null; + TunnelEncapsAttribute.EgressEndpoint ep = null; + TunnelEncapsAttribute.UDPDestinationPort port = null; + + for (TunnelEncapsAttribute.Tunnel t: te.getTunnels()) { + if (t.getType() != 51820) { + continue; + } + + for (TunnelEncapsAttribute.SubTlv st: t.getSubTlvs()) { + if (st instanceof TunnelEncapsAttribute.WireGuard) { + wg = (TunnelEncapsAttribute.WireGuard)st; + } else if (st instanceof TunnelEncapsAttribute.Color) { + col = (TunnelEncapsAttribute.Color)st; + } else if (st instanceof TunnelEncapsAttribute.EgressEndpoint) { + ep = (TunnelEncapsAttribute.EgressEndpoint)st; + } else if (st instanceof TunnelEncapsAttribute.UDPDestinationPort) { + port = (TunnelEncapsAttribute.UDPDestinationPort)st; + } + } + } + + if (wg == null) { + return; + } + + try { + Key publicKey = Key.fromBytes(wg.getPublicKey()); + InetEndpoint endpoint = null; + + if (ep != null && port != null) { + endpoint = InetEndpoint.fromAddress(ep.getAddress(), port.getPort()); + } + + tunnel.onEndpointChange(publicKey, endpoint); + + List<InetNetwork> addNetworks = new ArrayList<>(); + List<InetNetwork> removeNetworks = new ArrayList<>(); + + for (IPPrefix prefix: update.getPrefixes()) { + try { + addNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength())); + } catch (UnknownHostException ignore) { + } + } + + for (IPPrefix prefix: update.getWithdrawnPrefixes()) { + try { + removeNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength())); + } catch (UnknownHostException ignore) { + } + } + + if (mpr != null && (mpr.getAfi() == AFI.IPV6 || mpr.getAfi() == AFI.IPV4) && mpr.getSafi() == SAFI.UNICAST) { + for (IPPrefix prefix: mpr.getNlriPrefixes()) { + try { + addNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength())); + } catch (UnknownHostException ignore) { + } + } + } + + tunnel.onPersistentKeepaliveChange(publicKey, 24550); + tunnel.onAllowedIpsChange(publicKey, addNetworks, removeNetworks); + // backend.addAllowedIps(tunnel, publicKey, addNetworks); + // backend.removeAllowedIps(tunnel, publicKey, addNetworks); // TODO + backend.setPeer(tunnel, publicKey, endpoint, 24550, addNetworks, removeNetworks); + } catch (KeyFormatException ex) { + Log.w(TAG, "Key.fromBytes " + ex); + } + } + + @Override + public void onClose(BGPSession session) { + // NOT YET IMPLEMENTED + Log.i(TAG, "onClose"); + } + + public boolean startServer() { + stopServer(); + try { + SocketFactory factory = new SocketFactory() { + private Socket taggedSocket(Socket sock) throws SocketException { + TrafficStats.tagSocket(sock); + return sock; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + TrafficStats.setThreadStatsTag(STATS_TAG); + return taggedSocket(new Socket(host, port)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + TrafficStats.setThreadStatsTag(STATS_TAG); + return taggedSocket(new Socket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + TrafficStats.setThreadStatsTag(STATS_TAG); + return taggedSocket(new Socket(address, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + TrafficStats.setThreadStatsTag(STATS_TAG); + return taggedSocket(new Socket(host, port, localHost, localPort)); + } + }; + + BGPSessionConfiguration config = + new BGPSessionConfiguration(SESSION, + MY_ASN, + ip(LOCAL_ID), + REMOTE_ASN, + ip(REMOTE_ID), + null, // Remote address + factory, + this); + TrafficStats.setThreadStatsTag(STATS_TAG); + ServerSocket socket = new ServerSocket(PORT); + //TrafficStats.tagSocket(socket); + // Set + server = new BGPServer(socket); + server.getSessionConfigurations().add(config); + server.connect(config, REMOTE_ADDR); + return true; + } catch (IOException ex) { + return false; + } + } + + public void stopServer() { + if (server != null) { + server.shutdown(); + server = null; + } + } + + private static byte[] ip(String s) throws UnknownHostException { + InetAddress addr = InetAddress.getByName(s); + byte[] data = addr.getAddress(); + if (data.length != 4) + throw new UnknownHostException(s + ": Not an IPv4 address"); + return data; + } +} diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java new file mode 100644 index 00000000..41060ddf --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.backend; + +import com.wireguard.config.InetNetwork; +import com.wireguard.util.NonNullForAll; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Class representing DHCP info for a {@link Tunnel} instance. + */ +@NonNullForAll +public class Dhcp { + private Set<Lease> leases = new LinkedHashSet<>(); + + public class Lease { + private InetNetwork address; + private Duration validLt; + private Duration preferredLt; + private Instant validTs; + private Instant preferredTs; + + public Lease(InetNetwork address, Duration validLt, Duration preferredLt) { + this.address = address; + this.validLt = validLt; + this.preferredLt = preferredLt; + + Instant now = Instant.now(); + this.validTs = now.plus(validLt); + this.preferredTs = now.plus(preferredLt); + } + + public final InetNetwork getAddress() { + return this.address; + } + + public String toString() { + ZoneId zone = ZoneId.systemDefault(); + LocalTime validLocal = validTs.atZone(zone).toLocalTime().withNano(0); + LocalTime preferredLocal = preferredTs.atZone(zone).toLocalTime().withNano(0); + // TODO add date when needed + return address.toString() + " (valid " + validLocal + ", preferred " + preferredLocal + ")"; + } + } + + public void addLease(InetNetwork address, Duration valid, Duration preferred) { + this.leases.add(new Lease(address, valid, preferred)); + } + + public final Set<Lease> getLeases() { + return leases; + } + + public String toString() { + return "DHCP"; + } +} 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 429cb1f1..dec3021c 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -5,36 +5,109 @@ package com.wireguard.android.backend; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +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; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationChannelCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.ServiceCompat; /** * Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide @@ -44,12 +117,26 @@ import androidx.collection.ArraySet; 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; + private static final String NOTIFICATION_CHANNEL_ID = "WireGuardVPN"; @Nullable private static AlwaysOnCallback alwaysOnCallback; private static GhettoCompletableFuture<VpnService> 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. @@ -59,6 +146,23 @@ 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)); + channel = UdsChannelBuilder.forPath(socketName, LocalSocketAddress.Namespace.FILESYSTEM).build(); + + NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(context); + + NotificationChannelCompat channel = + new NotificationChannelCompat + .Builder(NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_DEFAULT) + .setName(NOTIFICATION_CHANNEL_ID) + .build(); + + notificationManager.createNotificationChannel(channel); } /** @@ -77,12 +181,18 @@ public final class GoBackend implements Backend { 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. * @@ -185,7 +295,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(); } /** @@ -224,78 +337,341 @@ public final class GoBackend implements Backend { return getState(tunnel); } - private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) - throws Exception { - Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state); + @Override + public void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks) { + if (tunnel != currentTunnel) { + // TODO logerror and/or return error/throw. + return; + } - if (state == State.UP) { - if (config == null) - throw new BackendException(Reason.TUNNEL_MISSING_CONFIG); + 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'); + } - if (VpnService.prepare(context) != null) - throw new BackendException(Reason.VPN_NOT_AUTHORIZED); + String goConfig = sb.toString(); + // TODO removed removeNetworks + Log.w(TAG, "Wg user string: " + goConfig); - final VpnService service; - if (!vpnService.isDone()) { - Log.d(TAG, "Requesting to start VpnService"); - context.startService(new Intent(context, VpnService.class)); + 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); + } + + @Override + public void setPeer(Tunnel tunnel, Key publicKey, @Nullable InetEndpoint endpoint, + Integer persistentKeepalive, + List<InetNetwork> addAllowedIps, List<InetNetwork> removeAllowedIps) { + if (tunnel != currentTunnel) { + // TODO logerror and/or return error/throw. + Log.w(TAG, "setPeer: wrong tunnel"); + return; + } + + final Resolver resolver = new Resolver(activeNetwork, connectivityManager.getLinkProperties(activeNetwork)); + + StringBuffer sb = new StringBuffer(); + + sb.append("public_key=").append(publicKey.toHex()).append('\n'); + for (final InetNetwork allowedIp: addAllowedIps) { + sb.append("allowed_ip=").append(allowedIp).append('\n'); + } + + if (endpoint != null) { + endpoint.getResolved(resolver, true).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n')); + } + + sb.append("persistent_keepalive_interval=").append(persistentKeepalive).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(); } + } - 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; + 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); + } } - service.setOwner(this); + }); - if (currentTunnelHandle != -1) { - Log.w(TAG, "Tunnel already up"); - return; + 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()); - 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()); - } + 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 } - break; } + } - // Build config - final String goConfig = config.toWgUserspaceString(); + // Replace the vpn tunnel + final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getLeases()); - // Create the vpn tunnel with android API + 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<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 VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set<Dhcp.Lease> leases) throws PackageManager.NameNotFoundException { + Log.i(TAG, "Builder 1"); final VpnService.Builder builder = service.getBuilder(); - builder.setSession(tunnel.getName()); + 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()) { @@ -305,20 +681,115 @@ public final class GoBackend implements Backend { } } + 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<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) { + 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); @@ -333,6 +804,46 @@ public final class GoBackend implements Backend { 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"); @@ -342,6 +853,24 @@ public final class GoBackend implements Backend { 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(); @@ -402,15 +931,48 @@ public final class GoBackend implements Backend { public void onCreate() { vpnService.complete(this); super.onCreate(); + + Notification notification = + new NotificationCompat.Builder(this, + GoBackend.NOTIFICATION_CHANNEL_ID) + .build(); + + int foregroundServiceType = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + ? ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED : 0; + + ServiceCompat.startForeground(this, + 100, + notification, + foregroundServiceType); } @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.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; @@ -436,4 +998,88 @@ public final class GoBackend implements Backend { 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); + } + } + }; } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java index 766df443..8c6eacda 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java @@ -5,8 +5,14 @@ package com.wireguard.android.backend; +import androidx.annotation.Nullable; + +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.InetNetwork; +import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; +import java.util.List; import java.util.regex.Pattern; /** @@ -54,4 +60,17 @@ public interface Tunnel { return running ? UP : DOWN; } } + + /** + * React to a change of DHCP of the tunnel. Should only be directly called by Backend. + * + * @param newDhcp The new DHCP info of the tunnel. + */ + void onDhcpChange(Dhcp newDhcp); + + void onEndpointChange(Key publicKey, @Nullable InetEndpoint newEndpoint); + + void onPersistentKeepaliveChange(Key publicKey, @Nullable Integer newPersistentKeepalive); + + void onAllowedIpsChange(Key publicKey, @Nullable List<InetNetwork> addNetworks, @Nullable List<InetNetwork> removeNetworks); } 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 023743a8..645b2fe0 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -14,6 +14,8 @@ import com.wireguard.android.backend.Tunnel.State; import com.wireguard.android.util.RootShell; import com.wireguard.android.util.ToolsInstaller; import com.wireguard.config.Config; +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.InetNetwork; import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; @@ -167,6 +169,18 @@ public final class WgQuickBackend implements Backend { return state; } + @Override + public void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setPeer(Tunnel tunnel, Key publicKey, @Nullable InetEndpoint endpoint, + Integer persistentKeepalive, + List<InetNetwork> addAllowedIps, List<InetNetwork> removeAllowedIps) { + throw new RuntimeException("Not implemented"); + } + private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception { Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state); diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java index db022e14..0d41cc05 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 ee9cebce..12ddb242 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 toWgEndpointsUserspaceString(Resolver resolver) { + final StringBuilder sb = new StringBuilder(); + for (final Peer peer : peers) + sb.append(peer.toWgEndpointsUserspaceString(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..0a9d19f6 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2023 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 d1db432b..f9db5779 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; @@ -64,6 +65,10 @@ public final class InetEndpoint { } } + public static InetEndpoint fromAddress(final InetAddress address, final int port) { + return new InetEndpoint(address.getHostAddress(), true, port); + } + @Override public boolean equals(final Object obj) { if (!(obj instanceof InetEndpoint)) @@ -80,6 +85,14 @@ public final class InetEndpoint { return port; } + public Optional<InetEndpoint> getResolved() { + if (isResolved) { + return Optional.of(this); + } else { + return Optional.ofNullable(resolved); + } + } + /** * 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. @@ -87,24 +100,22 @@ 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); + } + + public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) { + 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(); + + resolved = resolvedNow; } catch (final UnknownHostException e) { resolved = null; } diff --git a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java index 4a918044..02ccd946 100644 --- a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java +++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java @@ -20,7 +20,7 @@ public final class InetNetwork { private final InetAddress address; private final int mask; - private InetNetwork(final InetAddress address, final int mask) { + public InetNetwork(final InetAddress address, final int mask) { this.address = address; this.mask = mask; } diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java index bebca2e5..e47e0be3 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 8a0fd763..1d20a961 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 toWgEndpointsUserspaceString(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..24f5ab88 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java @@ -0,0 +1,140 @@ +/* + * 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.net.TrafficStats; +import android.util.Log; + +import androidx.annotation.Nullable; + +@NonNullForAll +public class Resolver { + private static final String TAG = "WireGuard/Resolver"; + private static final int STATS_TAG = 3; // FIXME + @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(); + } + } + + public 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 { + TrafficStats.setThreadStatsTag(STATS_TAG); + 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(); + TrafficStats.tagDatagramSocket(sock); + 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"); + sock.close(); + continue; + } + + Log.w(TAG, "Local address: " + sock.getLocalAddress()); + + if (candidate instanceof Inet4Address) { + // Accept IPv4 as preferred address. + address = candidate; + sock.close(); + 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..bf471599 --- /dev/null +++ b/tunnel/src/main/proto/libwg.proto @@ -0,0 +1,139 @@ +syntax = "proto3"; + +import "google/protobuf/duration.proto"; + +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); + rpc Dhcp(DhcpRequest) returns (DhcpResponse); + rpc CapabilitiesChanged(CapabilitiesChangedRequest) returns (CapabilitiesChangedResponse); +} + +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 { + oneof pacFile { + string pacFileUrl = 1; + string pacFileContent = 2; + } +} + +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; +} + +message Lease { + InetAddress address = 1; + google.protobuf.Duration preferred_lifetime = 2; + google.protobuf.Duration valid_lifetime = 3; +} + +message DhcpRequest { + InetAddress relay = 1; + InetAddress source = 2; +} + +message DhcpResponse { + Error error = 1; + repeated Lease leases = 2; +} + +message CapabilitiesChangedRequest { + enum Capability { + NONE = 0; + NOT_METERED = 11; + }; + + repeated Capability capabilities = 1; +} + +message CapabilitiesChangedResponse { + Error error = 1; +} diff --git a/tunnel/tools/libwg-go/.gitignore b/tunnel/tools/libwg-go/.gitignore index d1638636..d69ee823 100644 --- a/tunnel/tools/libwg-go/.gitignore +++ b/tunnel/tools/libwg-go/.gitignore @@ -1 +1,2 @@ -build/
\ No newline at end of file +build/ +/gen/ diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile index 427bfdc3..ed718258 100644 --- a/tunnel/tools/libwg-go/Makefile +++ b/tunnel/tools/libwg-go/Makefile @@ -19,6 +19,7 @@ export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(com export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME)) export GOOS := android export CGO_ENABLED := 1 +export GOPATH ?= $(HOME)/go GO_VERSION := 1.21.3 GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)) @@ -27,6 +28,13 @@ GO_HASH_darwin-amd64 := 27014fc69e301d7588a169ca239b3cc609f0aa1abf38528bf0d20d3b GO_HASH_darwin-arm64 := 65302a7a9f7a4834932b3a7a14cb8be51beddda757b567a2f9e0cbd0d7b5a6ab GO_HASH_linux-amd64 := 1241381b2843fae5a9707eec1f8fb2ef94d827990582c7c7c32f5bdfbfd420c8 +PROTOC_VERSION := 3.24.4 +PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go +PROTOC := $(GRADLE_USER_HOME)/caches/protoc-bin-$(PROTOC_VERSION)/protoc +PROTODIR = $(CURDIR)/../../src/main/proto +PROTO_INCLUDEDIR = $(CURDIR)/../../build/extracted-include-protos/debug +PBDIR = $(GOPATH)/pkg/mod + default: $(DESTDIR)/libwg-go.so $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL): @@ -45,8 +53,25 @@ $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_T patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \ touch "$@"' +$(PROTOC_GEN_GO): export GOARCH := +$(PROTOC_GEN_GO): export GOOS := +$(PROTOC_GEN_GO): export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH) +$(PROTOC_GEN_GO): $(BUILDDIR)/go-$(GO_VERSION)/.prepared Makefile + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + +gen/%.pb.go: export PATH := $(GOPATH)/bin:$(PATH) +gen/%.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO) + test -d gen || mkdir gen + $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go_out=paths=source_relative:./gen $< + +gen/%_grpc.pb.go: export PATH := $(GOPATH)/bin:$(PATH) +gen/%_grpc.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO) + test -d gen || mkdir gen + $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go-grpc_out=./gen --go-grpc_opt=paths=source_relative $< + $(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH) -$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod +$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod api-android.go dhcp.go http-proxy.go service.go gen/libwg.pb.go gen/libwg_grpc.pb.go jni.c go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared .DELETE_ON_ERROR: diff --git a/tunnel/tools/libwg-go/api-android.go b/tunnel/tools/libwg-go/api-android.go index d47c5d76..5d9e8b96 100644 --- a/tunnel/tools/libwg-go/api-android.go +++ b/tunnel/tools/libwg-go/api-android.go @@ -48,10 +48,17 @@ func (l AndroidLogger) Printf(format string, args ...interface{}) { type TunnelHandle struct { device *device.Device uapi net.Listener + logger *device.Logger + tun *tun.NativeTun } var tunnelHandles map[int32]TunnelHandle +func GetTunnel(handle int32) (tunnelHandle TunnelHandle, ok bool) { + tunnelHandle, ok = tunnelHandles[handle] + return +} + func init() { tunnelHandles = make(map[int32]TunnelHandle) signals := make(chan os.Signal) @@ -142,10 +149,29 @@ func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 { device.Close() return -1 } - tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi} + tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi, tun: tun} return i } +//export wgSetFd +func wgSetFd(tunnelHandle int32, tunFd int32) { + tag := cstring(fmt.Sprintf("WireGuard/GoBackend/%x", tunnelHandle)) + logger := &device.Logger{ + Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf, + Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf, + } + + handle, ok := tunnelHandles[tunnelHandle] + if !ok { + unix.Close(int(tunFd)) + logger.Errorf("Tunnel not found") + return + } + + handle.tun.SetFd(int(tunFd)) + logger.Verbosef("wgSetFd: %v", tunFd) +} + //export wgTurnOff func wgTurnOff(tunnelHandle int32) { handle, ok := tunnelHandles[tunnelHandle] @@ -208,20 +234,53 @@ func wgGetConfig(tunnelHandle int32) *C.char { //export wgVersion func wgVersion() *C.char { + return C.CString(Version()) +} + +func Version() string { info, ok := debug.ReadBuildInfo() if !ok { - return C.CString("unknown") + return "unknown" } for _, dep := range info.Deps { if dep.Path == "golang.zx2c4.com/wireguard" { parts := strings.Split(dep.Version, "-") if len(parts) == 3 && len(parts[2]) == 12 { - return C.CString(parts[2][:7]) + return parts[2][:7] } - return C.CString(dep.Version) + return dep.Version } } - return C.CString("unknown") + return "unknown" +} + +//export wgStartGrpc +func wgStartGrpc(sock_path string) C.int{ + tag := cstring("WireGuard/GoBackend/gRPC") + logger := &device.Logger{ + Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf, + Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf, + } + + res, errmsg := StartGrpc(sock_path, logger) + if res < 0 { + logger.Verbosef("wgStartGrpc: %v (%v)", errmsg, res) + } + + return C.int(res) +} + +//export wgSetConfig +func wgSetConfig(tunnelHandle int32, settings string) int32 { + handle, ok := tunnelHandles[tunnelHandle] + if !ok { + return -1 + } + err := handle.device.IpcSet(settings) + if err != nil { + return -1 + } + return 0 } func main() {} diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go new file mode 100644 index 00000000..4af5b0cb --- /dev/null +++ b/tunnel/tools/libwg-go/dhcp.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/dhcpv6/nclient6" + "github.com/insomniacslk/dhcp/iana" + + gen "golang.zx2c4.com/wireguard/android/gen" + "google.golang.org/protobuf/types/known/durationpb" +) + +const ( + ENABLE_PD = true +) + +type dhcp struct { + fqdn string + hwAddr net.HardwareAddr + conn *net.UDPConn + serverAddr net.UDPAddr + client *nclient6.Client + linkAddr net.IP + peerAddr net.IP +} + +func newClientIDOpt(duid dhcpv6.DUID) dhcpv4.Option { + iaid := []byte{0, 0, 0, 3} + ident := []byte{255} // Type IAID+DUID + ident = append(ident, iaid...) // IAID + ident = append(ident, duid.ToBytes()...) // DUID + return dhcpv4.OptClientIdentifier(ident) +} + +func getDuid(hwAddr net.HardwareAddr) dhcpv6.DUID { + duid := &dhcpv6.DUIDLLT{ + HWType: iana.HWTypeEthernet, + Time: uint32(time.Now().Unix()), + LinkLayerAddr: hwAddr, + } + + return duid +} + +func (d *dhcp) encapSendAndRead(ctx context.Context, msg *dhcpv6.Message, match nclient6.Matcher) (*dhcpv6.Message, error) { + packet, err := dhcpv6.EncapsulateRelay(msg, dhcpv6.MessageTypeRelayForward, d.linkAddr, d.peerAddr) + if err != nil { + return nil, err + } + + relay, err := d.client.SendAndReadRelay(ctx, &d.serverAddr, packet, match) + if err != nil { + return nil, err + } + + inner, err := relay.GetInnerMessage() + if err != nil { + return nil, err + } + + return inner, nil +} + +// isRelayMessageType returns a matcher that checks for the message type. +func isRelayMessageType(t dhcpv6.MessageType, tt ...dhcpv6.MessageType) nclient6.Matcher { + return func(p dhcpv6.DHCPv6) bool { + inner, err := p.GetInnerMessage() + if err != nil { + return false + } + if inner.Type() == t { + return true + } + for _, mt := range tt { + if inner.Type() == mt { + return true + } + } + return false + } +} + +// func New() *dhcp { +// } + +func RunDhcp(ctx context.Context, laddr, raddr netip.Addr) ([]*gen.Lease, error) { + d := &dhcp{} + + d.linkAddr = net.ParseIP("fe80::101") + d.peerAddr = net.ParseIP("::1") + + // TODO generate hostname and hwAddr from public key + hostName := "foobar" + d.fqdn = hostName + ".m7n.se" + d.hwAddr = []byte{41, 42, 43, 44, 45, 46} + + + src := net.UDPAddr{IP: laddr.AsSlice(), + Port: 0, // Use non-restrict UDP source port + } + + + d.serverAddr = net.UDPAddr{IP: raddr.AsSlice(), + Port: 547, + } + + err := d.Start(&src) + if err != nil { + return nil, err + } + + defer d.Close() + + reply, err := d.ObtainLease(ctx) // Use reply + if err != nil { + return nil, err + } + + return getAddressesFromReply(reply), nil +} + +func getAddressesFromReply(reply *dhcpv6.Message) []*gen.Lease{ + var leases []*gen.Lease = make([]*gen.Lease, 0, 4) + + if opt := reply.GetOneOption(dhcpv6.OptionIANA); opt != nil { + iana := opt.(*dhcpv6.OptIANA) + ianaOpts := iana.Options.Get(dhcpv6.OptionIAAddr) + + for _, opt := range ianaOpts { + addrOpt := opt.(*dhcpv6.OptIAAddress) + lease := &gen.Lease{ + Address: &gen.InetAddress{ + Address: addrOpt.IPv6Addr, + }, + PreferredLifetime: durationpb.New(addrOpt.PreferredLifetime), + ValidLifetime: durationpb.New(addrOpt.ValidLifetime), + } + leases = append(leases, lease) + } + } + + return leases +} + +func (d *dhcp) Start(localAddr *net.UDPAddr) error { + conn, err := net.ListenUDP("udp6", localAddr) + if err != nil { + return err + } + + d.client, err = nclient6.NewWithConn(conn, d.hwAddr, nclient6.WithDebugLogger()) + return err +} + +func (d *dhcp) Close() error { + err := d.client.Close() + + d.client = nil + return err +} + +func (d *dhcp) ObtainLease(ctx context.Context) (*dhcpv6.Message, error){ + duidOpt := dhcpv6.WithClientID(getDuid(d.hwAddr)) + fqdnOpt := dhcpv6.WithFQDN(0x1, d.fqdn) + + solicit, err := dhcpv6.NewSolicit(d.hwAddr, duidOpt, fqdnOpt, dhcpv6.WithRapidCommit) + if err != nil { + return nil, err + } + + msg, err := d.encapSendAndRead(ctx, solicit, isRelayMessageType(dhcpv6.MessageTypeReply, dhcpv6.MessageTypeAdvertise)) + if err != nil { + return nil, err + } + + if msg.Type() == dhcpv6.MessageTypeReply { + // We got RapidCommitted. + return msg, nil + } + + // We didn't get RapidCommitted. Request regular lease. + req, err := dhcpv6.NewRequestFromAdvertise(msg, fqdnOpt) + if err != nil { + return nil, err + } + + return d.encapSendAndRead(ctx, req, nil) +} diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod index 9318ebcf..86a09293 100644 --- a/tunnel/tools/libwg-go/go.mod +++ b/tunnel/tools/libwg-go/go.mod @@ -3,12 +3,27 @@ module golang.zx2c4.com/wireguard/android go 1.20 require ( - golang.org/x/sys v0.6.0 - golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 + github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d + github.com/insomniacslk/dhcp v0.0.0-00010101000000-000000000000 + golang.org/x/net v0.17.0 + golang.org/x/sys v0.13.0 + golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb + google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.27.1 + gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440 ) require ( - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect ) + +replace github.com/insomniacslk/dhcp => /src/insomniacslk-dhcp + +replace golang.zx2c4.com/wireguard => /src/wireguard-go diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum index 3b49b497..70c365ff 100644 --- a/tunnel/tools/libwg-go/go.sum +++ b/tunnel/tools/libwg-go/go.sum @@ -1,10 +1,167 @@ -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d h1:ikfPAvbWWkCagwNfwfWsIaRyKHDF1I6T4lh7PewKV9A= +github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU= -golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440 h1:SxFAMd+8zfpL/Rk4pgdb8leeZDiL3M/gCWCbBvmLkoE= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tunnel/tools/libwg-go/http-proxy.go b/tunnel/tools/libwg-go/http-proxy.go new file mode 100644 index 00000000..11ab0716 --- /dev/null +++ b/tunnel/tools/libwg-go/http-proxy.go @@ -0,0 +1,736 @@ +package main + +import ( + "fmt" + "io" + "net" + "net/http" + "net/netip" + "net/url" + "strings" + "sync" + + "github.com/elazarl/goproxy" + + "golang.zx2c4.com/wireguard/device" + + net_proxy "golang.org/x/net/proxy" + + "gopkg.in/olebedev/go-duktape.v3" +) + +const ( + // Imported from Firefox' ProxyAutoConfig.cpp + // https://searchfox.org/mozilla-central/source/netwerk/base/ProxyAutoConfig.cpp + // This Source Code Form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at http://mozilla.org/MPL/2.0/. + ASCII_PAC_UTILS = "function dnsDomainIs(host, domain) {\n" + + " return (host.length >= domain.length &&\n" + + " host.substring(host.length - domain.length) == domain);\n" + + "}\n" + + "" + + "function dnsDomainLevels(host) {\n" + + " return host.split('.').length - 1;\n" + + "}\n" + + "" + + "function isValidIpAddress(ipchars) {\n" + + " var matches = " + + "/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(ipchars);\n" + + " if (matches == null) {\n" + + " return false;\n" + + " } else if (matches[1] > 255 || matches[2] > 255 || \n" + + " matches[3] > 255 || matches[4] > 255) {\n" + + " return false;\n" + + " }\n" + + " return true;\n" + + "}\n" + + "" + + "function convert_addr(ipchars) {\n" + + " var bytes = ipchars.split('.');\n" + + " var result = ((bytes[0] & 0xff) << 24) |\n" + + " ((bytes[1] & 0xff) << 16) |\n" + + " ((bytes[2] & 0xff) << 8) |\n" + + " (bytes[3] & 0xff);\n" + + " return result;\n" + + "}\n" + + "" + + "function isInNet(ipaddr, pattern, maskstr) {\n" + + " if (!isValidIpAddress(pattern) || !isValidIpAddress(maskstr)) {\n" + + " return false;\n" + + " }\n" + + " if (!isValidIpAddress(ipaddr)) {\n" + + " ipaddr = dnsResolve(ipaddr);\n" + + " if (ipaddr == null) {\n" + + " return false;\n" + + " }\n" + + " }\n" + + " var host = convert_addr(ipaddr);\n" + + " var pat = convert_addr(pattern);\n" + + " var mask = convert_addr(maskstr);\n" + + " return ((host & mask) == (pat & mask));\n" + + " \n" + + "}\n" + + "" + + "function isPlainHostName(host) {\n" + + " return (host.search('(\\\\.)|:') == -1);\n" + + "}\n" + + "" + + "function isResolvable(host) {\n" + + " var ip = dnsResolve(host);\n" + + " return (ip != null);\n" + + "}\n" + + "" + + "function localHostOrDomainIs(host, hostdom) {\n" + + " return (host == hostdom) ||\n" + + " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" + + "}\n" + + "" + + "function shExpMatch(url, pattern) {\n" + + " pattern = pattern.replace(/\\./g, '\\\\.');\n" + + " pattern = pattern.replace(/\\*/g, '.*');\n" + + " pattern = pattern.replace(/\\?/g, '.');\n" + + " var newRe = new RegExp('^'+pattern+'$');\n" + + " return newRe.test(url);\n" + + "}\n" + + "" + + "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" + + "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, " + + "AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" + + "" + + "function weekdayRange() {\n" + + " function getDay(weekday) {\n" + + " if (weekday in wdays) {\n" + + " return wdays[weekday];\n" + + " }\n" + + " return -1;\n" + + " }\n" + + " var date = new Date();\n" + + " var argc = arguments.length;\n" + + " var wday;\n" + + " if (argc < 1)\n" + + " return false;\n" + + " if (arguments[argc - 1] == 'GMT') {\n" + + " argc--;\n" + + " wday = date.getUTCDay();\n" + + " } else {\n" + + " wday = date.getDay();\n" + + " }\n" + + " var wd1 = getDay(arguments[0]);\n" + + " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" + + " return (wd1 == -1 || wd2 == -1) ? false\n" + + " : (wd1 <= wd2) ? (wd1 <= wday && wday " + + "<= wd2)\n" + + " : (wd2 >= wday || wday " + + ">= wd1);\n" + + "}\n" + + "" + + "function dateRange() {\n" + + " function getMonth(name) {\n" + + " if (name in months) {\n" + + " return months[name];\n" + + " }\n" + + " return -1;\n" + + " }\n" + + " var date = new Date();\n" + + " var argc = arguments.length;\n" + + " if (argc < 1) {\n" + + " return false;\n" + + " }\n" + + " var isGMT = (arguments[argc - 1] == 'GMT');\n" + + "\n" + + " if (isGMT) {\n" + + " argc--;\n" + + " }\n" + + " // function will work even without explict handling of this case\n" + + " if (argc == 1) {\n" + + " var tmp = parseInt(arguments[0]);\n" + + " if (isNaN(tmp)) {\n" + + " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" + + " getMonth(arguments[0]));\n" + + " } else if (tmp < 32) {\n" + + " return ((isGMT ? date.getUTCDate() : date.getDate()) == " + + "tmp);\n" + + " } else { \n" + + " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) " + + "==\n" + + " tmp);\n" + + " }\n" + + " }\n" + + " var year = date.getFullYear();\n" + + " var date1, date2;\n" + + " date1 = new Date(year, 0, 1, 0, 0, 0);\n" + + " date2 = new Date(year, 11, 31, 23, 59, 59);\n" + + " var adjustMonth = false;\n" + + " for (var i = 0; i < (argc >> 1); i++) {\n" + + " var tmp = parseInt(arguments[i]);\n" + + " if (isNaN(tmp)) {\n" + + " var mon = getMonth(arguments[i]);\n" + + " date1.setMonth(mon);\n" + + " } else if (tmp < 32) {\n" + + " adjustMonth = (argc <= 2);\n" + + " date1.setDate(tmp);\n" + + " } else {\n" + + " date1.setFullYear(tmp);\n" + + " }\n" + + " }\n" + + " for (var i = (argc >> 1); i < argc; i++) {\n" + + " var tmp = parseInt(arguments[i]);\n" + + " if (isNaN(tmp)) {\n" + + " var mon = getMonth(arguments[i]);\n" + + " date2.setMonth(mon);\n" + + " } else if (tmp < 32) {\n" + + " date2.setDate(tmp);\n" + + " } else {\n" + + " date2.setFullYear(tmp);\n" + + " }\n" + + " }\n" + + " if (adjustMonth) {\n" + + " date1.setMonth(date.getMonth());\n" + + " date2.setMonth(date.getMonth());\n" + + " }\n" + + " if (isGMT) {\n" + + " var tmp = date;\n" + + " tmp.setFullYear(date.getUTCFullYear());\n" + + " tmp.setMonth(date.getUTCMonth());\n" + + " tmp.setDate(date.getUTCDate());\n" + + " tmp.setHours(date.getUTCHours());\n" + + " tmp.setMinutes(date.getUTCMinutes());\n" + + " tmp.setSeconds(date.getUTCSeconds());\n" + + " date = tmp;\n" + + " }\n" + + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + + " : (date2 >= date) || (date >= date1);\n" + + "}\n" + + "" + + "function timeRange() {\n" + + " var argc = arguments.length;\n" + + " var date = new Date();\n" + + " var isGMT= false;\n" + + "" + + " if (argc < 1) {\n" + + " return false;\n" + + " }\n" + + " if (arguments[argc - 1] == 'GMT') {\n" + + " isGMT = true;\n" + + " argc--;\n" + + " }\n" + + "\n" + + " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" + + " var date1, date2;\n" + + " date1 = new Date();\n" + + " date2 = new Date();\n" + + "\n" + + " if (argc == 1) {\n" + + " return (hour == arguments[0]);\n" + + " } else if (argc == 2) {\n" + + " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" + + " } else {\n" + + " switch (argc) {\n" + + " case 6:\n" + + " date1.setSeconds(arguments[2]);\n" + + " date2.setSeconds(arguments[5]);\n" + + " case 4:\n" + + " var middle = argc >> 1;\n" + + " date1.setHours(arguments[0]);\n" + + " date1.setMinutes(arguments[1]);\n" + + " date2.setHours(arguments[middle]);\n" + + " date2.setMinutes(arguments[middle + 1]);\n" + + " if (middle == 2) {\n" + + " date2.setSeconds(59);\n" + + " }\n" + + " break;\n" + + " default:\n" + + " throw 'timeRange: bad number of arguments'\n" + + " }\n" + + " }\n" + + "\n" + + " if (isGMT) {\n" + + " date.setFullYear(date.getUTCFullYear());\n" + + " date.setMonth(date.getUTCMonth());\n" + + " date.setDate(date.getUTCDate());\n" + + " date.setHours(date.getUTCHours());\n" + + " date.setMinutes(date.getUTCMinutes());\n" + + " date.setSeconds(date.getUTCSeconds());\n" + + " }\n" + + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + + " : (date2 >= date) || (date >= date1);\n" + + "\n" + + "}\n" +) + +type HttpProxy struct { + listener net.Listener + tlsListener net.Listener + logger *device.Logger + addrPort netip.AddrPort + tlsAddrPort netip.AddrPort + ctx *duktape.Context + defaultProxy *goproxy.ProxyHttpServer + uidRequest chan UidRequest + handlers []*HttpHandler +} + +func NewHttpProxy(uidRequest chan UidRequest, logger *device.Logger) *HttpProxy { + logger.Verbosef("NewHttpProxy") + return &HttpProxy{ + listener: nil, + logger: logger, + uidRequest: uidRequest, + handlers: make([]*HttpHandler, 0, 2), + } +} + +var ( + ASCII_PAC_UTILS_NAMES = []string{"dnsDomainIs", "dnsDomainLevels", "isValidIpAddress", "convert_addr", "isInNet", "isPlainHostName", "isResolvable", "localHostOrDomainIs", "shExpMatch", "weekdayRange", "dateRange", "timeRange"} +) + +func (p *HttpProxy) GetAddrPort() netip.AddrPort { + return p.addrPort +} + +type Logger struct { + logger *device.Logger +} + +func (l *Logger) Printf(format string, v ...interface{}) { + l.logger.Verbosef(format, v...) +} + +func (p *HttpProxy) newGoProxy(cat, proxyUrl string) (*goproxy.ProxyHttpServer, error) { + proxy := goproxy.NewProxyHttpServer() + proxy.Logger = &Logger{logger: p.logger} + proxy.Verbose = true + if cat == "SOCKS5" { + socksDialer, err := net_proxy.SOCKS5("tcp", proxyUrl, nil, nil) + if err != nil { + return nil, err + } + proxy.Tr.Dial = socksDialer.Dial + } + proxy.NonproxyHandler = http.HandlerFunc(func (w http.ResponseWriter, req *http.Request) { + if req.Host == "" { + fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") + return + } + req.URL.Scheme = "http" + req.URL.Host = req.Host + proxy.ServeHTTP(w, req) + }) + + if cat == "PROXY" { + proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) { + return url.Parse(proxyUrl) + } + proxy.ConnectDial = proxy.NewConnectDialToProxy(proxyUrl) + } + + return proxy, nil +} + +func FindProxyForURL(ctx *duktape.Context, url, host string, logger *device.Logger) (res string, err error) { + if !ctx.GetGlobalString("FindProxyForURL") { + ctx.Pop() + return "", fmt.Errorf("FindProxyForURL not found") + } + + ctx.PushString(url) + ctx.PushString(host) + + res = "DIRECT" + logger.Verbosef("Before pcall") + r := ctx.Pcall(2) + logger.Verbosef("After pcall") + if r == 0 { + res = ctx.GetString(-1) + } else if ctx.IsError(-1) { + ctx.GetPropString(-1, "stack") + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } else { + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } + ctx.Pop() + return +} + + +func FindProxyForPkg(ctx *duktape.Context, pkg string, logger *device.Logger) (res string, err error) { + if !ctx.GetGlobalString("FindProxyForPkg") { + ctx.Pop() + return "", fmt.Errorf("FindProxyForPkg not found") + } + + ctx.PushString(pkg) + + res = "DIRECT" + logger.Verbosef("Before pcall") + r := ctx.Pcall(1) + logger.Verbosef("After pcall") + if r == 0 { + res = ctx.GetString(-1) + } else if ctx.IsError(-1) { + ctx.GetPropString(-1, "stack") + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } else { + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } + ctx.Pop() + return +} + +func newPacBodyCtx(body string, logger *device.Logger) *duktape.Context { + ctx := duktape.New() + ctx.PushGlobalGoFunction("dnsResolve", + func(ctx *duktape.Context) int { + // Check stack + host := ctx.GetString(-1) + ctx.Pop() + ips, err := net.LookupIP(host) + if err != nil { + return 0 + } + + for _, ip := range(ips) { + ipStr := string(ip) + if strings.Contains(ipStr, ".") { + // Found IPv4 + ctx.PushString(ipStr) + return 1 + } + } + // No IPv4 + return 0 + }) + + ctx.PevalString(ASCII_PAC_UTILS) + logger.Verbosef("ASCII_PAC_UTILS result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) + ctx.Pop() + + ctx.PevalString(string(body)) + logger.Verbosef("result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) + ctx.Pop() + FindProxyForURL(ctx, "http://www.jabra.se/", "www.jabra.se", logger) + return ctx +} + +func (p *HttpProxy) newPacFileCtx(pacFileUrl *url.URL) (*duktape.Context, error) { + var pacFileBody = "" + if pacFileUrl == nil { + return nil, nil + } + + resp, err := http.Get(pacFileUrl.String()) + p.logger.Verbosef("pacFile: %v, %v", resp, err) + + if err == nil { + defer resp.Body.Close() + ct, ok := resp.Header["Content-Type"] + if ok && len(ct) == 1 && ct[0] == "application/x-ns-proxy-autoconfig" { + body, err := io.ReadAll(resp.Body) + if err == nil { + pacFileBody = string(body) + return newPacBodyCtx(pacFileBody, p.logger), nil + } + } + } + + return nil, err +} + +func (p *HttpProxy) SetPacFileUrl(pacFileUrl *url.URL) error { + ctx, err := p.newPacFileCtx(pacFileUrl) + if err != nil { + return err + } + + for _, handler := range(p.handlers) { + handler.setPacCtx(ctx) + } + + if p.ctx != nil { + p.ctx.Destroy() + } + p.ctx = ctx + p.logger.Verbosef("SetNotMetered(false) from SetPacFileUrl") + p.SetNotMetered(false) + return nil +} + +func (p *HttpProxy) SetPacFileContent(pacFile string) error { + ctx := newPacBodyCtx(pacFile, p.logger) + + for _, handler := range(p.handlers) { + handler.setPacCtx(ctx) + } + + if p.ctx != nil { + p.ctx.Destroy() + } + p.ctx = ctx + p.logger.Verbosef("SetNotMetered(false) from SetPacFileContent") + p.SetNotMetered(false) + return nil +} + +func (p *HttpProxy) SetNotMetered(notMetered bool) error { + p.logger.Verbosef("SetNotMetered: %v", notMetered) + + if p.ctx == nil { + return fmt.Errorf("No ductape context") + } + + p.ctx.PushGlobalObject() + p.ctx.PushBoolean(notMetered) + p.ctx.PutPropString(-2, "notMetered") + p.ctx.Pop() + + return nil +} + +func (p *HttpProxy) Start() (listen_port uint16, err error) { + p.logger.Verbosef("HttpProxy.Start()") + listen_port = 0 + + + proxyMap := make(map[string]*goproxy.ProxyHttpServer) + + p.defaultProxy, err = p.newGoProxy("", "") + if err != nil { + return + } + proxyMap[""] = p.defaultProxy + + listen_port, handler, err := p.startRegularProxy(proxyMap) + if err != nil { + return + } + p.handlers = append(p.handlers, handler) + + return +} + +func (p *HttpProxy) startRegularProxy(proxyMap map[string]*goproxy.ProxyHttpServer) (listen_port uint16, handler *HttpHandler, err error) { + p.listener, err = net.Listen("tcp", "[::]:") + if err != nil { + return + } + + p.addrPort, err = netip.ParseAddrPort(p.listener.Addr().String()) + if err != nil { + return + } + + listen_port = p.addrPort.Port() + + handler = NewHttpHandler(p, p.defaultProxy, proxyMap, p.logger) + + go http.Serve(NewListener(p.listener, handler, p.logger), handler) + return +} + +func (p *HttpProxy) Stop() { + if p.listener != nil { + p.logger.Verbosef("Close: %v", p.listener) + p.listener.Close() +// p.listener = nil + } + if p.tlsListener != nil { + p.logger.Verbosef("Close: %v", p.tlsListener) + p.tlsListener.Close() +// p.tlsListener = nil + } + if p.ctx != nil { + p.ctx.DestroyHeap() + p.ctx = nil + } +} + +type HttpHandler struct { + p *HttpProxy + defaultProxy *goproxy.ProxyHttpServer + logger *device.Logger + remoteAddrPkgMapMutex sync.RWMutex + remoteAddrPkgMap map[string]*goproxy.ProxyHttpServer + uidRequest chan UidRequest + ctx *duktape.Context + proxyMap map[string]*goproxy.ProxyHttpServer // FIXME include type in map key +} + +func NewHttpHandler(p *HttpProxy, defaultProxy *goproxy.ProxyHttpServer, proxyMap map[string]*goproxy.ProxyHttpServer, logger *device.Logger) *HttpHandler{ + h := &HttpHandler{ + p: p, + defaultProxy: defaultProxy, + logger: logger, + remoteAddrPkgMapMutex: sync.RWMutex{}, + remoteAddrPkgMap: make(map[string]*goproxy.ProxyHttpServer), + uidRequest: p.uidRequest, + proxyMap: proxyMap, + } + + return h +} + +// TODO fix multi-threading +func (h *HttpHandler) setPacCtx(ctx *duktape.Context) { + h.ctx = ctx +} + +func (h *HttpHandler) addConnToProxyMap(c net.Conn) error { + h.logger.Verbosef("Accept: %v -> %v", c.RemoteAddr().String(), c.LocalAddr().String()) + local, err := netip.ParseAddrPort(c.RemoteAddr().String()) + if err != nil { + return fmt.Errorf("Bad local address (%v): %v", c.RemoteAddr(), err) + } + // Remove IPv6 zone, NOOP on IPv4 + local = netip.AddrPortFrom(local.Addr().WithZone(""), local.Port()) + + remote, err := netip.ParseAddrPort(c.LocalAddr().String()) + if err != nil { + return fmt.Errorf("Bad remote address (%v): %v", c.LocalAddr(), err) + } + + h.logger.Verbosef("uidRequest: %v -> %v", local, remote) + addrPortPair := AddrPortPair{local: local, remote: remote} + retCh := make(chan string) + h.uidRequest <- UidRequest{Data: addrPortPair, RetCh: retCh} + + select { + case pkg := <-retCh: + h.logger.Verbosef("uidResponse: '%v'", pkg) + + // TODO add NotMetered as + proxy, ok := h.proxyMap[pkg] + if !ok { + proxy, err = h.findProxyForPkg(pkg) + if err != nil { + return err + } + } + + if proxy != nil { + h.remoteAddrPkgMapMutex.Lock() + h.remoteAddrPkgMap[c.RemoteAddr().String()] = proxy + h.remoteAddrPkgMapMutex.Unlock() + } + } + + return nil +} + +func (h *HttpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + h.remoteAddrPkgMapMutex.Lock() + proxy, ok := h.remoteAddrPkgMap[req.RemoteAddr] + if ok && proxy != nil { + delete(h.remoteAddrPkgMap, req.RemoteAddr) + h.remoteAddrPkgMapMutex.Unlock() + + proxyStr := "nil" + proxyUrl, err := proxy.Tr.Proxy(req) + if err == nil && proxyUrl != nil { + proxyStr = proxyUrl.String() + } + h.logger.Verbosef("ServeHTTP remote:%s proxy:%v", req.RemoteAddr, proxyStr) + + proxy.ServeHTTP(rw, req) + } else { + h.remoteAddrPkgMapMutex.Unlock() + h.defaultProxy.ServeHTTP(rw, req) + } +} + +func (h *HttpHandler) findProxyForPkg(pkg string) (*goproxy.ProxyHttpServer, error) { + if h.ctx == nil { + return nil, fmt.Errorf("Null context") + } + + find := func() (cat string, res string, err error) { + h.logger.Verbosef("Call FindProxyForPkg %v %v", pkg) + res, err = FindProxyForPkg(h.ctx, pkg, h.logger) + h.logger.Verbosef("FindProxyForPkg res %v %v", res, err) + if err != nil { + h.logger.Verbosef("FindProxyForPkg result is: %v stack:%v", res, h.ctx.GetTop()) + return "", "", err + } else if res == "" { + return "DIRECT", "", nil + } else { + values := strings.Split(strings.Trim(res, " "), ";") + for _, v := range values { + value := strings.Trim(v, " ") + parts := strings.SplitN(value, " ", 2) + if parts[0] == "PROXY" || parts[0] == "HTTP" { + return "PROXY", "http://" + parts[1], nil + } else if parts[0] == "HTTPS" { + return "PROXY", "https://" + parts[1], nil + } else if parts[0] == "DIRECT" { + return parts[0], "", nil + } else if parts[0] == "SOCKS" || parts[0] == "SOCKS5" { + return "SOCKS5", parts[1], nil + } + } + } + return "", "", fmt.Errorf("No result") + } + cat, res, err := find() + if err != nil { + return nil, err + } + + var proxy *goproxy.ProxyHttpServer + if cat == "DIRECT" { + proxy = h.defaultProxy + } else { + proxy, err = h.p.newGoProxy(cat, res) + if err != nil { + return nil, err + } + } + h.proxyMap[pkg] = proxy + + return proxy, nil +} + +type AddrPortPair struct { + local netip.AddrPort + remote netip.AddrPort +} + +// Listener +type Listener struct { + l net.Listener + handler *HttpHandler + logger *device.Logger +} + +func NewListener(listener net.Listener, handler *HttpHandler, logger *device.Logger) *Listener{ + l := &Listener{ + l: listener, + handler: handler, + logger: logger, + } + + return l +} + +func (l *Listener) Accept() (net.Conn, error) { + c, err := l.l.Accept() + if err != nil { + l.logger.Verbosef("Accept failed: %v", err) + return c, err + } + + if err := l.handler.addConnToProxyMap(c); err != nil { + err = fmt.Errorf("Reject connection (%v): %v", c, err) + c.Close() + return nil, err + } + + return c, nil +} + +func (l *Listener) Close() error { + return l.l.Close() +} + +func (l *Listener) Addr() net.Addr { + return l.l.Addr() +} diff --git a/tunnel/tools/libwg-go/jni.c b/tunnel/tools/libwg-go/jni.c index 7ad94d35..443848e8 100644 --- a/tunnel/tools/libwg-go/jni.c +++ b/tunnel/tools/libwg-go/jni.c @@ -14,6 +14,9 @@ extern int wgGetSocketV4(int handle); extern int wgGetSocketV6(int handle); extern char *wgGetConfig(int handle); extern char *wgVersion(); +extern int wgSetConfig(int handle, struct go_string settings); +extern int wgStartGrpc(); +extern int wgSetFd(int handle, int tun_fd); JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings) { @@ -69,3 +72,32 @@ JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion free(version); return ret; } + +JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetConfig(JNIEnv *env, jclass c, jint handle, jstring settings) +{ + const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0); + size_t settings_len = (*env)->GetStringUTFLength(env, settings); + int ret = wgSetConfig(handle, (struct go_string){ + .str = settings_str, + .n = settings_len + }); + (*env)->ReleaseStringUTFChars(env, settings, settings_str); + return ret; +} + +JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgStartGrpc(JNIEnv *env, jclass c, jstring sockname) +{ + const char *sockname_str = (*env)->GetStringUTFChars(env, sockname, 0); + size_t sockname_len = (*env)->GetStringUTFLength(env, sockname); + jint res = wgStartGrpc((struct go_string){ + .str = sockname_str, + .n = sockname_len + }); + (*env)->ReleaseStringUTFChars(env, sockname, sockname_str); + return res; +} + +JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetFd(JNIEnv *env, jclass c, jint handle, jint tun_fd) +{ + wgSetFd(handle, tun_fd); +} diff --git a/tunnel/tools/libwg-go/service.go b/tunnel/tools/libwg-go/service.go new file mode 100644 index 00000000..41834fe0 --- /dev/null +++ b/tunnel/tools/libwg-go/service.go @@ -0,0 +1,342 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + "net/netip" + "net/url" + "os" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + gen "golang.zx2c4.com/wireguard/android/gen" + "golang.zx2c4.com/wireguard/device" +) + +const ( + IPPROTO_TCP = 6 +) + +type UidRequest struct { + Data AddrPortPair + RetCh chan string +} + +type LibwgServiceImpl struct { + gen.UnimplementedLibwgServer + logger *device.Logger + httpProxy *HttpProxy + uidRequest chan UidRequest + stopReverse chan bool +} + +var service *LibwgServiceImpl +var server *grpc.Server + +func NewLibwgService(logger *device.Logger) gen.LibwgServer { + return &LibwgServiceImpl{ + logger: logger, + uidRequest: make(chan UidRequest), + stopReverse: make(chan bool), + } +} + +func StartGrpc(sock_path string, logger *device.Logger) (int, string) { + if server != nil { + return -1, "Already started" + } + + if _, err := os.Stat(sock_path); err == nil { + if err := os.RemoveAll(sock_path); err != nil { + return -1, fmt.Sprintf("Cleanup failed: %v %v", sock_path, err) + } + } + + listener, err := net.Listen("unix", sock_path) + if err != nil { + return -1, fmt.Sprintf("Listen failed: %v %v", sock_path, err) + } + + server = grpc.NewServer() + service = NewLibwgService(logger).(*LibwgServiceImpl) + + gen.RegisterLibwgServer(server, service) + + go func() { + server.Serve(listener) + }() + + logger.Verbosef("gRPC started") + + return 0, "" +} + +func (e *LibwgServiceImpl) Version(ctx context.Context, req *gen.VersionRequest) (*gen.VersionResponse, error) { + + r := &gen.VersionResponse{ + Version: Version(), + } + + return r, nil +} + +func (e *LibwgServiceImpl) StopGrpc(ctx context.Context, req *gen.StopGrpcRequest) (*gen.StopGrpcResponse, error) { + if server != nil { + server.Stop() + server = nil + service = nil + } + + r := &gen.StopGrpcResponse{ + } + + return r, nil +} + +func buildStartHttpProxyError(message string) (*gen.StartHttpProxyResponse, error) { + r := &gen.StartHttpProxyResponse{ + Error: &gen.Error{ + Message: message, + }, + } + return r, nil +} + +func (e *LibwgServiceImpl) StartHttpProxy(ctx context.Context, req *gen.StartHttpProxyRequest) (*gen.StartHttpProxyResponse, error) { + var listenPort uint16 + if e.httpProxy == nil { + e.httpProxy = NewHttpProxy(e.uidRequest, e.logger) + var err error + listenPort, err = e.httpProxy.Start() + + if err != nil { + e.httpProxy = nil + return buildStartHttpProxyError(fmt.Sprintf("Http proxy start failed: %v", err)) + } + } else { + listenPort = e.httpProxy.GetAddrPort().Port() + } + + content := req.GetPacFileContent() + if content != "" { + err := e.httpProxy.SetPacFileContent(content) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileContent: %v (%s)", content)) + } + } else { + pacFileUrl, err := url.Parse(req.GetPacFileUrl()) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, req.GetPacFileUrl())) + } + err = e.httpProxy.SetPacFileUrl(pacFileUrl) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, pacFileUrl)) + } + } + + r := &gen.StartHttpProxyResponse{ + ListenPort: uint32(listenPort), + } + return r, nil +} + +func (e *LibwgServiceImpl) StopHttpProxy(ctx context.Context, req *gen.StopHttpProxyRequest) (*gen.StopHttpProxyResponse, error) { + if e.httpProxy == nil { + r := &gen.StopHttpProxyResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Http proxy not running"), + }, + } + return r, nil + } + + e.httpProxy.Stop() + e.httpProxy = nil + e.stopReverse <- true + r := &gen.StopHttpProxyResponse{} + return r, nil +} + +func (e *LibwgServiceImpl) Reverse(stream gen.Libwg_ReverseServer) error { + e.logger.Verbosef("Reverse enter loop") + for e.httpProxy != nil { + var err error + + // err := contextError(stream.Context()) + err = stream.Context().Err() + if err != nil { + e.logger.Verbosef("Reverse: context: %v", err) + return err + } + + select { + case <-e.stopReverse: + e.logger.Verbosef("Reverse: stop") + break + case uidReq := <-e.uidRequest: + addrPortPair := uidReq.Data + local := addrPortPair.local + remote := addrPortPair.remote + r := &gen.ReverseResponse{ + Request: &gen.ReverseResponse_Uid{ + Uid: &gen.GetConnectionOwnerUidRequest{ + Protocol: IPPROTO_TCP, + Local: &gen.InetSocketAddress{ + Address: &gen.InetAddress{ + Address: local.Addr().AsSlice(), + }, + Port: uint32(local.Port()), + }, + Remote: &gen.InetSocketAddress{ + Address: &gen.InetAddress{ + Address: remote.Addr().AsSlice(), + }, + Port: uint32(remote.Port()), + }, + }, + }, + } + + stream.Send(r) + + req, err := stream.Recv() + if err == io.EOF { + e.logger.Verbosef("no more data") + uidReq.RetCh <- "" + break + } + if err != nil { + err = status.Errorf(codes.Unknown, "cannot receive stream request: %v", err) + e.logger.Verbosef("Reverse: %v", err) + uidReq.RetCh <- "" + return err + } + + e.logger.Verbosef("Reverse: received, wait: %v", req) + uidReq.RetCh <- req.GetUid().GetPackage() + } + } + + + e.logger.Verbosef("Reverse returns") + return nil +} + +func (e *LibwgServiceImpl) IpcSet(ctx context.Context, req *gen.IpcSetRequest) (*gen.IpcSetResponse, error) { + tunnel, ok := GetTunnel(req.GetTunnel().GetHandle()) + if !ok { + r := &gen.IpcSetResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Invalid tunnel"), + }, + } + return r, nil + } + + err := tunnel.device.IpcSet(req.GetConfig()) + if err != nil { + r := &gen.IpcSetResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("IpcSet failed: %v", err), + }, + } + return r, nil + } + + r := &gen.IpcSetResponse{ + } + + return r, nil +} + +func (e *LibwgServiceImpl) Dhcp(ctx context.Context, req *gen.DhcpRequest) (*gen.DhcpResponse, error) { + var relayAddr netip.Addr + var sourceAddr netip.Addr + + source := req.GetSource() + if source != nil { + sourceAddr, _ = netip.AddrFromSlice(source.GetAddress()) + } + + if !sourceAddr.IsValid() || !sourceAddr.Is6() { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("DHCPv6 source address missing"), + }, + } + return r, nil + } + + relay := req.GetRelay() + if relay != nil { + relayAddr, _ = netip.AddrFromSlice(relay.GetAddress()) + } else { + // Construct relay address from source prefix + relayRaw := source.GetAddress()[:8] + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 1) + relayAddr, _ = netip.AddrFromSlice(relayRaw) + } + + if !relayAddr.IsValid() || !relayAddr.Is6() { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("DHCPv6 relay address calculation failed"), + }, + } + return r, nil + } + + e.logger.Verbosef("RunDhcp %v %v", sourceAddr, relayAddr) + + leases, err := RunDhcp(ctx, sourceAddr, relayAddr) + if err != nil { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("RunDhcp failed: %v", err), + }, + } + return r, nil + } + + r := &gen.DhcpResponse{ + Leases: leases, + } + return r, nil +} + +func (e *LibwgServiceImpl) CapabilitiesChanged(ctx context.Context, req *gen.CapabilitiesChangedRequest) (*gen.CapabilitiesChangedResponse, error) { + if e.httpProxy == nil { + r := &gen.CapabilitiesChangedResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Http Proxy not running"), + }, + } + return r, nil + } + + notMetered := false + + for _, cap := range req.Capabilities { + if cap == gen.CapabilitiesChangedRequest_NOT_METERED { + notMetered = true + } + } + + e.logger.Verbosef("Set not metered %v", notMetered) + e.httpProxy.SetNotMetered(notMetered) + + r := &gen.CapabilitiesChangedResponse{ + } + return r, nil +} diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index be11cc30..fff3acd9 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val pkg: String = providers.gradleProperty("wireguardPackageName").get() +val appID: String = providers.gradleProperty("wireguardApplicationID").get() plugins { alias(libs.plugins.android.application) @@ -21,7 +22,7 @@ android { } namespace = pkg defaultConfig { - applicationId = pkg + applicationId = appID minSdk = 21 targetSdk = 34 versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt() @@ -36,6 +37,14 @@ android { targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } + signingConfigs { + named("debug") { + storeFile = File("/src/wireguard-android/keystore/debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } buildTypes { release { isMinifyEnabled = true @@ -52,6 +61,7 @@ android { debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" + signingConfig = signingConfigs.getByName("debug") } create("googleplay") { initWith(getByName("release")) diff --git a/ui/src/debug/res/values/strings.xml b/ui/src/debug/res/values/strings.xml index 947b7381..af58a39d 100644 --- a/ui/src/debug/res/values/strings.xml +++ b/ui/src/debug/res/values/strings.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="app_name" translatable="false">WireGuard β</string> + <string name="app_name" translatable="false">(m7n.eu) WireGuard β</string> </resources> diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 754992d5..cf70f494 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -3,7 +3,13 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="internalOnly"> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission + android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" + android:minSdkVersion="34" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> @@ -162,5 +168,10 @@ <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent> + + <intent> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> + </intent> </queries> </manifest> diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt index afba41cb..6b2040e1 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.wireguard.android.BR import com.wireguard.android.R +import com.wireguard.android.backend.Dhcp; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler import com.wireguard.android.widget.ToggleSwitch import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener @@ -170,6 +171,12 @@ object BindingAdapters { } @JvmStatic + @BindingAdapter("android:text") + fun setDhcpLeaseSetText(view: TextView, dhcp: Dhcp?) { + view.text = if (dhcp?.leases != null) Attribute.join(dhcp.leases.map { it }) else "" + } + + @JvmStatic fun tryParseInt(s: String?): Int { if (s == null) return 0 diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 81d8e8c6..fce14d20 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -5,6 +5,7 @@ package com.wireguard.android.fragment import android.os.Bundle +import android.util.Log; import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -21,6 +22,7 @@ import com.wireguard.android.databinding.TunnelDetailFragmentBinding import com.wireguard.android.databinding.TunnelDetailPeerBinding import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.QuantityFormatter +import com.wireguard.android.viewmodel.ConfigDetail import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -79,7 +81,9 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } else { lifecycleScope.launch { try { - binding.config = newTunnel.getConfigAsync() + var config = newTunnel.getConfigDetailAsync() + binding.config = config + Log.i(TAG, "onSelectedTunnelChanged " + config + ", " + config.config) } catch (_: Throwable) { binding.config = null } @@ -147,4 +151,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } } } + + companion object { + private const val TAG = "WireGuard/TunnelDetailFragment" + } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index edf4b226..6be27f94 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -16,13 +16,17 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView import android.widget.EditText +import android.widget.Filter import android.widget.Toast import androidx.core.os.BundleCompat import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputLayout import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.Tunnel @@ -32,6 +36,7 @@ import com.wireguard.android.util.AdminKnobs import com.wireguard.android.util.BiometricAuthenticator import com.wireguard.android.util.ErrorMessages import com.wireguard.android.viewmodel.ConfigProxy +import com.wireguard.android.viewmodel.Constants import com.wireguard.config.Config import kotlinx.coroutines.launch @@ -43,6 +48,21 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null + private class MaterialSpinnerAdapter<T>(context: Context, resource: Int, private val objects: List<T>) : ArrayAdapter<T>(context, resource, objects) { + private val _filter: Filter by lazy { + object : Filter() { + override fun performFiltering(constraint: CharSequence?): FilterResults { + return FilterResults() + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + } + } + } + + override fun getFilter(): Filter = _filter + } + private fun onConfigLoaded(config: Config) { binding?.config = ConfigProxy(config) } @@ -80,6 +100,13 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { executePendingBindings() privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() } } + + var httpProxyMenu = binding?.root?.findViewById<TextInputLayout>(R.id.http_proxy_menu) + var httpProxyItems = listOf(Constants.HTTP_PROXY_NONE, Constants.HTTP_PROXY_MANUAL, Constants.HTTP_PROXY_PAC) + var httpProxyAdapter = MaterialSpinnerAdapter(requireContext(), R.layout.http_proxy_menu_item, httpProxyItems) + var httpProxyMenuText = httpProxyMenu?.editText as? AutoCompleteTextView + httpProxyMenuText?.setAdapter(httpProxyAdapter) + return binding?.root } diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index aa237aee..03529454 100644 --- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt @@ -7,12 +7,20 @@ package com.wireguard.android.model import android.util.Log import androidx.databinding.BaseObservable import androidx.databinding.Bindable +import com.wireguard.android.Application import com.wireguard.android.BR +import com.wireguard.android.backend.Dhcp import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel import com.wireguard.android.databinding.Keyed import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail +import com.wireguard.android.viewmodel.PeerDetail import com.wireguard.config.Config +import com.wireguard.config.InetEndpoint +import com.wireguard.config.InetNetwork +import com.wireguard.crypto.Key +import java.util.Optional import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -55,7 +63,18 @@ class ObservableTunnel internal constructor( } fun onStateChanged(state: Tunnel.State): Tunnel.State { - if (state != Tunnel.State.UP) onStatisticsChanged(null) + if (state != Tunnel.State.UP) { + onStatisticsChanged(null) + onDhcpChanged(null) + Application.getCoroutineScope().launch { + onPeersReset() + } + } else { + configDetail?.peers?.forEach { + var endpoint: InetEndpoint? = it.peer?.endpoint?.orElse(null) + it.endpoint = Optional.ofNullable(endpoint?.getResolved()?.orElse(null)); + } + } this.state = state notifyPropertyChanged(BR.state) return state @@ -68,6 +87,7 @@ class ObservableTunnel internal constructor( this@ObservableTunnel.state } + private var configDetail: ConfigDetail? = if (config != null) ConfigDetail(config) else null @get:Bindable var config = config @@ -86,7 +106,11 @@ class ObservableTunnel internal constructor( private set suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) { - config ?: manager.getTunnelConfig(this@ObservableTunnel) + config ?: manager.getTunnelConfig(this@ObservableTunnel).config!! + } + + suspend fun getConfigDetailAsync(): ConfigDetail = withContext(Dispatchers.Main.immediate) { + configDetail ?: manager.getTunnelConfig(this@ObservableTunnel) } suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) { @@ -98,10 +122,11 @@ class ObservableTunnel internal constructor( } } - fun onConfigChanged(config: Config?): Config? { + fun onConfigChanged(config: Config?): ConfigDetail? { + this.configDetail = ConfigDetail(config) this.config = config notifyPropertyChanged(BR.config) - return config + return configDetail } @@ -136,6 +161,101 @@ class ObservableTunnel internal constructor( return statistics } + @get:Bindable + var dhcp: Dhcp? = null + private set + + override fun onDhcpChange(newDhcp: Dhcp) { + onDhcpChanged(newDhcp) + } + + fun onDhcpChanged(dhcp: Dhcp?): Dhcp? { + this.dhcp = dhcp + notifyPropertyChanged(BR.dhcp) + return dhcp + } + + // Remove dynamic peers, and reset static peers + fun onPeersReset() { + Log.i(TAG, "ObservableTunnel onPeersReset") + var toRemove: MutableList<PeerDetail> = ArrayList() + + configDetail?.peers?.forEach { + if (it.peer == null) { + toRemove.add(it) + } else { + it.endpoint = Optional.empty() + } + } + + toRemove.forEach { + Log.i(TAG, "ObservableTunnel remove " + it) + configDetail?.peers?.remove(it) + } + } + + override fun onEndpointChange(publicKey: Key, newEndpoint: InetEndpoint?) { + Application.getCoroutineScope().launch { + onEndpointChanged(publicKey, newEndpoint) + } + } + + private fun onEndpointChanged(publicKey: Key, newEndpoint: InetEndpoint?) { + + Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint) + var peer2: PeerDetail = lookupPeer(publicKey) + + if (newEndpoint != null) { + peer2.endpoint = newEndpoint.getResolved() + } else { + var peer3 = peer2.peer + peer2.endpoint = if (peer3 != null) peer3.endpoint else Optional.empty() + } + } + + fun lookupPeer(publicKey: Key): PeerDetail { + configDetail?.peers?.forEach { + if (it.publicKey.equals(publicKey) == true) { + Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer) + return it + } + } + + Log.i(TAG, "ObservableTunnel create peer " + publicKey) + var peer: PeerDetail = PeerDetail(publicKey) + configDetail?.peers?.add(peer) + + return peer + } + + override fun onPersistentKeepaliveChange(publicKey: Key, newPersistentKeepalive: Int?) { + Application.getCoroutineScope().launch { + onPersistentKeepaliveChanged(publicKey, newPersistentKeepalive) + } + } + + private fun onPersistentKeepaliveChanged(publicKey: Key, newPersistentKeepalive: Int?) { + var peer: PeerDetail = lookupPeer(publicKey) + + peer.persistentKeepalive = Optional.ofNullable(newPersistentKeepalive) + } + + override fun onAllowedIpsChange(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) { + Application.getCoroutineScope().launch { + onAllowedIpsChanged(publicKey, addNetworks, removeNetworks) + } + } + + private fun onAllowedIpsChanged(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) { + var peer: PeerDetail = lookupPeer(publicKey) + + removeNetworks?.let() { + peer.allowedIps.removeAll(removeNetworks) + } + addNetworks?.let() { + peer.allowedIps.addAll(addNetworks) + } + } suspend fun deleteAsync() = manager.delete(this) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index ba873fa6..960adcaa 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -24,6 +24,7 @@ import com.wireguard.android.databinding.ObservableSortedKeyedArrayList import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.UserKnobs import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail import com.wireguard.config.Config import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers @@ -94,7 +95,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) } } - suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) { + suspend fun getTunnelConfig(tunnel: ObservableTunnel): ConfigDetail = withContext(Dispatchers.Main.immediate) { tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!! } @@ -156,7 +157,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { tunnel.onConfigChanged(withContext(Dispatchers.IO) { getBackend().setState(tunnel, tunnel.state, config) configStore.save(tunnel.name, config) - })!! + })!!.config!! } suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) { diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt index d617adec..97be8c99 100644 --- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt +++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt @@ -122,6 +122,8 @@ object ErrorMessages { return resources.getString(R.string.bad_config_explanation_udp_port) } else if (bce.location == BadConfigException.Location.MTU) { return resources.getString(R.string.bad_config_explanation_positive_number) + } else if (bce.location == BadConfigException.Location.HTTP_PROXY) { + return resources.getString(R.string.bad_config_explanation_http_proxy) } else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) { return resources.getString(R.string.bad_config_explanation_pka) } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt new file mode 100644 index 00000000..af95a86a --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt @@ -0,0 +1,22 @@ +package com.wireguard.android.viewmodel + +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableList + +import com.wireguard.config.Config + +class ConfigDetail { + val config: Config? + val peers: ObservableList<PeerDetail> = ObservableArrayList() + + constructor(other: Config?) { + config = other + if (other != null) { + other.peers.forEach { + val detail = PeerDetail(it) + peers.add(detail) + detail.bind(this) + } + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt index 004ebed1..81a548f9 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt @@ -4,6 +4,8 @@ */ package com.wireguard.android.viewmodel +import android.net.Uri +import android.os.Build import android.os.Parcel import android.os.Parcelable import androidx.databinding.BaseObservable @@ -18,6 +20,12 @@ import com.wireguard.crypto.Key import com.wireguard.crypto.KeyFormatException import com.wireguard.crypto.KeyPair +object Constants { + const val HTTP_PROXY_NONE = "None" + const val HTTP_PROXY_MANUAL = "Manual" + const val HTTP_PROXY_PAC = "Proxy Auto-Config" +} + class InterfaceProxy : BaseObservable, Parcelable { @get:Bindable val excludedApplications: ObservableList<String> = ObservableArrayList() @@ -54,6 +62,44 @@ class InterfaceProxy : BaseObservable, Parcelable { } @get:Bindable + var httpProxyMenu: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyMenu) + notifyPropertyChanged(BR.httpProxyManualVisibility) + notifyPropertyChanged(BR.httpProxyPacVisibility) + } + + @get:Bindable + var httpProxyManualVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_MANUAL) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable + var httpProxyHostname: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyHostname) + } + + @get:Bindable + var httpProxyPort: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPort) + } + + @get:Bindable + var httpProxyPac: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPac) + } + + @get:Bindable + var httpProxyPacVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_PAC) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable var privateKey: String = "" set(value) { field = value @@ -76,6 +122,10 @@ class InterfaceProxy : BaseObservable, Parcelable { parcel.readStringList(includedApplications) listenPort = parcel.readString() ?: "" mtu = parcel.readString() ?: "" + httpProxyMenu = parcel.readString() ?: "" + httpProxyHostname = parcel.readString() ?: "" + httpProxyPort = parcel.readString() ?: "" + httpProxyPac = parcel.readString() ?: "" privateKey = parcel.readString() ?: "" } @@ -87,6 +137,10 @@ class InterfaceProxy : BaseObservable, Parcelable { includedApplications.addAll(other.includedApplications) listenPort = other.listenPort.map { it.toString() }.orElse("") mtu = other.mtu.map { it.toString() }.orElse("") + httpProxyHostname = other.httpProxy.map { if (it.getHost().startsWith('[') && it.getHost().endsWith(']')) it.getHost().substring(1, it.getHost().length-1) else it.getHost() }.orElse("") + httpProxyPort = other.httpProxy.map { if (it.getPort() <= 0) "8080" else it.getPort().toString() }.orElse("") + httpProxyPac = other.httpProxy.map { it.getPacFileUrl().toString() }.orElse("") + httpProxyMenu = other.httpProxy.map { if (it.getPacFileUrl() != Uri.EMPTY) Constants.HTTP_PROXY_PAC else if (it.getHost() != "") Constants.HTTP_PROXY_MANUAL else Constants.HTTP_PROXY_NONE }.orElse(Constants.HTTP_PROXY_NONE) val keyPair = other.keyPair privateKey = keyPair.privateKey.toBase64() } @@ -111,6 +165,20 @@ class InterfaceProxy : BaseObservable, Parcelable { if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications) if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort) if (mtu.isNotEmpty()) builder.parseMtu(mtu) + if (Constants.HTTP_PROXY_MANUAL.equals(httpProxyMenu) && httpProxyHostname.isNotEmpty()) { + var httpProxy: String + if (httpProxyHostname.contains(":")) { + httpProxy = "[" + httpProxyHostname + "]" + } else { + httpProxy = httpProxyHostname + } + if (httpProxyPort.isNotEmpty()) { + httpProxy += ":" + httpProxyPort; + } + builder.parseHttpProxy(httpProxy) + } else if (Constants.HTTP_PROXY_PAC.equals(httpProxyMenu) && httpProxyPac.isNotEmpty()) { + builder.parseHttpProxy("pac:" + httpProxyPac) + } if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey) return builder.build() } @@ -122,6 +190,10 @@ class InterfaceProxy : BaseObservable, Parcelable { dest.writeStringList(includedApplications) dest.writeString(listenPort) dest.writeString(mtu) + dest.writeString(httpProxyMenu) + dest.writeString(httpProxyHostname) + dest.writeString(httpProxyPort) + dest.writeString(httpProxyPac) dest.writeString(privateKey) } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt new file mode 100644 index 00000000..3b6f42be --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt @@ -0,0 +1,89 @@ +package com.wireguard.android.viewmodel + +import android.util.Log +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.databinding.Observable +import androidx.databinding.ObservableList +import androidx.databinding.ObservableArrayList + +import com.wireguard.android.BR +import com.wireguard.config.InetEndpoint +import com.wireguard.config.InetNetwork +import com.wireguard.config.Peer +import com.wireguard.crypto.Key; + +import java.util.Optional; + +import kotlin.collections.LinkedHashSet + + +class PeerDetail : BaseObservable { + var peer: Peer? + private var owner: ConfigDetail? = null + + @get:Bindable + var publicKey: Key + + @get:Bindable + var allowedIps: ObservableList<InetNetwork> = ObservableArrayList<InetNetwork>() + + @get:Bindable + var endpoint: Optional<InetEndpoint> = Optional.empty() + get() { + if (!field.isEmpty()) { + return field + } else { + return Optional.ofNullable(peer?.endpoint?.get()) + } + } + + set(value) { + Log.i(TAG, "notifyPropertyChanged endpoint " + this + ", " + value) + field = value + notifyPropertyChanged(BR.endpoint) + } + + @get:Bindable + var persistentKeepalive: Optional<Int> = Optional.empty() + set(value) { + field = value + notifyPropertyChanged(BR.persistentKeepalive) + } + + constructor(other: Peer) { + peer = other + publicKey = other.getPublicKey() + allowedIps.addAll(other.getAllowedIps()) + endpoint = other.getEndpoint(); + persistentKeepalive = other.getPersistentKeepalive() + } + + constructor(publicKey: Key) { + peer = null + this.publicKey = publicKey + } + + fun bind(owner: ConfigDetail) { + this.owner = owner + } + + override fun addOnPropertyChangedCallback (callback: Observable.OnPropertyChangedCallback) { + Log.i(TAG, "addOnPropertyChangedCallback " + this + ", " + callback) + super.addOnPropertyChangedCallback(callback) + } + + /** + * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is + * identified by its public key and (if known) its endpoint. + * + * @return a concise single-line identifier for the {@code Peer} + */ + override fun toString(): String { + return "(Peer " + publicKey.toBase64() + ")" + } + + companion object { + private const val TAG = "WireGuard/PeerDetail" + } +} diff --git a/ui/src/main/res/layout/http_proxy_menu_item.xml b/ui/src/main/res/layout/http_proxy_menu_item.xml new file mode 100644 index 00000000..8ad5c026 --- /dev/null +++ b/ui/src/main/res/layout/http_proxy_menu_item.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:ellipsize="end" + android:maxLines="1" +/> diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index 332df04a..9b17a06c 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.backend.Tunnel.State" /> <import type="com.wireguard.android.util.ClipboardUtils" /> @@ -19,7 +21,7 @@ <variable name="config" - type="com.wireguard.config.Config" /> + type="com.wireguard.android.viewmodel.ConfigDetail" /> </data> <ScrollView @@ -116,7 +118,7 @@ android:nextFocusForward="@id/addresses_text" android:onClick="@{ClipboardUtils::copyTextView}" android:singleLine="true" - android:text="@{config.interface.keyPair.publicKey.toBase64}" + android:text="@{config.config.interface.keyPair.publicKey.toBase64}" android:textAppearance="?attr/textAppearanceBodyLarge" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" @@ -129,7 +131,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/addresses_text" android:text="@string/addresses" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -139,14 +141,41 @@ android:layout_height="wrap_content" android:contentDescription="@string/addresses" android:nextFocusUp="@id/public_key_text" + android:nextFocusDown="@id/dynamic_addresses_text" + android:nextFocusForward="@id/dynamic_addresses_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.addresses}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/addresses_label" + tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> + + <TextView + android:id="@+id/dynamic_addresses_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/dynamic_addresses_text" + android:text="@string/dynamic_addresses" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + + <TextView + android:id="@+id/dynamic_addresses_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/dynamic_addresses" + android:nextFocusUp="@id/addresses_text" android:nextFocusDown="@id/dns_servers_text" android:nextFocusForward="@id/dns_servers_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.addresses}" + android:text="@{tunnel.dhcp}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/addresses_label" + app:layout_constraintTop_toBottomOf="@+id/dynamic_addresses_label" tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> <TextView @@ -156,22 +185,22 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_servers_text" android:text="@string/dns_servers" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + app:layout_constraintTop_toBottomOf="@id/dynamic_addresses_text" /> <TextView android:id="@+id/dns_servers_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/dns_servers" - android:nextFocusUp="@id/addresses_text" + android:nextFocusUp="@id/dynamic_addresses_text" android:nextFocusDown="@id/dns_search_domains_text" android:nextFocusForward="@id/dns_search_domains_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsServers}" + android:text="@{config.config.interface.dnsServers}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" tools:text="8.8.8.8, 8.8.4.4" /> @@ -183,7 +212,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_search_domain_text" android:text="@string/dns_search_domains" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> @@ -196,9 +225,9 @@ android:nextFocusDown="@id/listen_port_text" android:nextFocusForward="@id/listen_port_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsSearchDomains}" + android:text="@{config.config.interface.dnsSearchDomains}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label" tools:text="zx2c4.com" /> @@ -210,7 +239,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/listen_port_text" android:text="@string/listen_port" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -223,12 +252,12 @@ android:contentDescription="@string/listen_port" android:nextFocusRight="@id/mtu_text" android:nextFocusUp="@id/dns_search_domains_text" - android:nextFocusDown="@id/applications_text" + android:nextFocusDown="@id/http_proxy_text" android:nextFocusForward="@id/mtu_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.listenPort}" + android:text="@{config.config.interface.listenPort}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -242,7 +271,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/mtu_text" android:text="@string/mtu" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintLeft_toRightOf="@id/listen_port_label" @@ -256,11 +285,11 @@ android:contentDescription="@string/mtu" android:nextFocusLeft="@id/listen_port_text" android:nextFocusUp="@id/dns_servers_text" - android:nextFocusForward="@id/applications_text" + android:nextFocusForward="@id/http_proxy_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.mtu}" + android:text="@{config.config.interface.mtu}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toEndOf="@id/listen_port_label" @@ -276,15 +305,42 @@ app:constraint_referenced_ids="listen_port_text,mtu_text" /> <TextView + android:id="@+id/http_proxy_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/http_proxy_text" + android:text="@string/http_proxy" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + + <TextView + android:id="@+id/http_proxy_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:contentDescription="@string/http_proxy" + android:nextFocusUp="@id/listen_port_text" + android:nextFocusDown="@id/applications_text" + android:nextFocusForward="@id/applications_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.httpProxy}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintTop_toBottomOf="@id/http_proxy_label" + app:layout_constraintStart_toStartOf="parent" + tools:text="http://example.com:8888" /> + + <TextView android:id="@+id/applications_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:labelFor="@+id/applications_text" android:text="@string/applications" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" /> <TextView android:id="@+id/applications_text" @@ -295,9 +351,9 @@ android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}" + android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/applications_label" tools:text="8 excluded" /> @@ -311,6 +367,7 @@ android:layout_marginTop="8dp" android:divider="@null" android:orientation="vertical" + app:fragment="@{fragment}" app:items="@{config.peers}" app:layout="@{@layout/tunnel_detail_peer}" app:layout_constraintStart_toStartOf="parent" @@ -318,4 +375,4 @@ tools:ignore="UselessLeaf" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> -</layout>
\ No newline at end of file +</layout> diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index 25081cea..89bb85ec 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -9,7 +9,7 @@ <variable name="item" - type="com.wireguard.config.Peer" /> + type="com.wireguard.android.viewmodel.PeerDetail" /> </data> <com.google.android.material.card.MaterialCardView @@ -64,7 +64,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/pre_shared_key_text" android:text="@string/pre_shared_key" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -81,7 +81,7 @@ android:singleLine="true" android:text="@string/pre_shared_key_enabled" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label" tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" /> diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml index 0350486b..42222399 100644 --- a/ui/src/main/res/layout/tunnel_editor_fragment.xml +++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.util.ClipboardUtils" /> <import type="com.wireguard.android.widget.KeyInputFilter" /> @@ -210,7 +212,7 @@ android:imeOptions="actionNext" android:inputType="textNoSuggestions|textVisiblePassword" android:nextFocusUp="@id/addresses_label_text" - android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" android:nextFocusForward="@id/mtu_text" android:text="@={config.interface.dnsServers}" /> </com.google.android.material.textfield.TextInputLayout> @@ -235,19 +237,112 @@ android:imeOptions="actionDone" android:inputType="number" android:nextFocusUp="@id/listen_port_text" - android:nextFocusDown="@id/set_excluded_applications" - android:nextFocusForward="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" + android:nextFocusForward="@id/http_proxy_hostname_text" android:text="@={config.interface.mtu}" android:textAlignment="center" /> </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_menu" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dns_servers_label_layout"> + + <AutoCompleteTextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="none" + android:text="@={config.interface.httpProxyMenu}" + /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_hostname_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_hostname" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:layout_constraintEnd_toStartOf="@id/http_proxy_port_label_layout" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintHorizontal_weight="0.7" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_hostname_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/http_proxy_port_text" + android:text="@={config.interface.httpProxyHostname}" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_port_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_port" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_weight="0.3" + app:layout_constraintStart_toEndOf="@id/http_proxy_hostname_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_port_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/http_proxy_pac_label_layout" + android:nextFocusForward="@id/http_proxy_pac_label_layout" + android:text="@={config.interface.httpProxyPort}" + android:textAlignment="center" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_pac_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_pac" + android:visibility="@{config.interface.httpProxyPacVisibility}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_hostname_label_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_pac_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/http_proxy_hostname_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/set_excluded_applications" + android:text="@={config.interface.httpProxyPac}" /> + </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.button.MaterialButton android:id="@+id/set_excluded_applications" style="@style/Widget.Material3.Button.TextButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" - android:nextFocusUp="@id/dns_servers_text" + android:nextFocusUp="@id/http_proxy_hostname_text" android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}" @@ -256,7 +351,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/mtu_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_pac_label_layout" app:rippleColor="?attr/colorSecondary" tools:text="4 excluded applications" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index df3d3340..1cb642e2 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -63,10 +63,11 @@ <string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string> <string name="allow_remote_control_intents_title">Allow remote control apps</string> <string name="allowed_ips">Allowed IPs</string> - <string name="app_name" translatable="false">WireGuard</string> + <string name="app_name" translatable="false">(m7n.eu) WireGuard</string> <string name="bad_config_context">%1$s\'s %2$s</string> <string name="bad_config_context_top_level">%s</string> <string name="bad_config_error">%1$s in %2$s</string> + <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string> <string name="bad_config_explanation_pka">: Must be positive and no more than 65535</string> <string name="bad_config_explanation_positive_number">: Must be positive</string> <string name="bad_config_explanation_udp_port">: Must be a valid UDP port number</string> @@ -104,6 +105,7 @@ <string name="dark_theme_summary_on">Currently using dark (night) theme</string> <string name="dark_theme_title">Use dark theme</string> <string name="delete">Delete</string> + <string name="dynamic_addresses">Dynamic addresses</string> <string name="tv_delete">Select tunnel to delete</string> <string name="tv_select_a_storage_drive">Select a storage drive</string> <string name="tv_no_file_picker">Please install a file management utility to browse files</string> @@ -130,6 +132,10 @@ <string name="hint_optional">(optional)</string> <string name="hint_optional_discouraged">(optional, not recommended)</string> <string name="hint_random">(random)</string> + <string name="http_proxy">Proxy</string> + <string name="http_proxy_hostname">Proxy hostname</string> + <string name="http_proxy_pac">Proxy Auto-Config URL</string> + <string name="http_proxy_port">Proxy port</string> <string name="illegal_filename_error">Illegal file name “%s”</string> <string name="import_error">Unable to import tunnel: %s</string> <string name="import_from_qr_code">Import Tunnel from QR Code</string> |