summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.idea/codeStyles/Project.xml1
-rw-r--r--gradle.properties4
-rw-r--r--gradle/libs.versions.toml18
-rw-r--r--settings.gradle.kts1
-rw-r--r--tunnel/build.gradle.kts63
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Backend.java5
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Bgp.java285
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java31
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java532
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java17
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java6
-rw-r--r--tunnel/src/main/java/com/wireguard/config/BadConfigException.java9
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Config.java12
-rw-r--r--tunnel/src/main/java/com/wireguard/config/HttpProxy.java78
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetEndpoint.java49
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetNetwork.java2
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Interface.java33
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Peer.java13
-rw-r--r--tunnel/src/main/java/com/wireguard/util/Resolver.java138
-rw-r--r--tunnel/src/main/proto/libwg.proto125
-rw-r--r--tunnel/tools/libwg-go/.gitignore3
-rw-r--r--tunnel/tools/libwg-go/Makefile27
-rw-r--r--tunnel/tools/libwg-go/api-android.go56
-rw-r--r--tunnel/tools/libwg-go/dhcp.go192
-rw-r--r--tunnel/tools/libwg-go/go.mod23
-rw-r--r--tunnel/tools/libwg-go/go.sum179
-rw-r--r--tunnel/tools/libwg-go/http-proxy.go716
-rw-r--r--tunnel/tools/libwg-go/jni.c19
-rw-r--r--tunnel/tools/libwg-go/service.go316
-rw-r--r--ui/build.gradle.kts17
-rw-r--r--ui/src/googleplay/AndroidManifest.xml8
-rw-r--r--ui/src/main/AndroidManifest.xml39
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt16
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt13
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt66
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/MainActivity.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt17
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt3
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt16
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt42
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt24
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt9
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt1
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt26
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt66
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt27
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt141
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt50
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt16
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt78
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Updater.kt125
-rw-r--r--ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt36
-rw-r--r--ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt85
-rw-r--r--ui/src/main/java/com/wireguard/android/util/Extensions.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt1
-rw-r--r--ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt15
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt22
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt72
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt85
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt40
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt15
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt13
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt8
-rw-r--r--ui/src/main/res/drawable/list_item_background.xml3
-rw-r--r--ui/src/main/res/layout/config_naming_dialog_fragment.xml4
-rw-r--r--ui/src/main/res/layout/http_proxy_menu_item.xml8
-rw-r--r--ui/src/main/res/layout/tunnel_detail_fragment.xml111
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml6
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml105
-rw-r--r--ui/src/main/res/layout/tunnel_list_fragment.xml4
-rw-r--r--ui/src/main/res/resources.properties1
-rw-r--r--ui/src/main/res/values-de/strings.xml11
-rw-r--r--ui/src/main/res/values-et-rEE/strings.xml7
-rw-r--r--ui/src/main/res/values-night/themes.xml2
-rw-r--r--ui/src/main/res/values-ru/strings.xml7
-rw-r--r--ui/src/main/res/values-v23/styles.xml1
-rw-r--r--ui/src/main/res/values-v27/styles.xml1
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--ui/src/main/res/values/strings.xml12
-rw-r--r--ui/src/main/res/values/themes.xml2
-rw-r--r--ui/src/main/res/xml/preferences.xml4
90 files changed, 4015 insertions, 480 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index d19645e3..076a7f02 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -468,6 +468,7 @@
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ <option name="RIGHT_MARGIN" value="160" />
</codeStyleSettings>
</code_scheme>
</component> \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 6e4f2a99..186542c6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
-wireguardVersionCode=502
-wireguardVersionName=1.0.20230504
+wireguardVersionCode=504
+wireguardVersionName=1.0.20230510
wireguardPackageName=com.wireguard.android
wireguardApplicationID=eu.m7n.wireguard.android
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c7858b7c..5bb982d9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,11 @@
[versions]
-agp = "8.0.1"
+agp = "8.1.0-beta02"
+grpc = "1.55.1"
+grgit = "5.2.0"
kotlin = "1.8.21"
+protobuf = "0.9.3"
+protoc = "3.22.4"
+protocgengrpc = '1.55.1'
[libraries]
androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.1"
@@ -16,14 +21,21 @@ androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.5.7"
androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0"
desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3"
-google-material = "com.google.android.material:material:1.8.0"
+google-material = "com.google.android.material:material:1.9.0"
+grpc-android = "io.grpc:grpc-android:1.55.1"
+grpc-okhttp = "io.grpc:grpc-okhttp:1.55.1"
+grpc-protobuf-lite = "io.grpc:grpc-protobuf-lite:1.55.1"
+grpc-stub = "io.grpc:grpc-stub:1.55.1"
+javax-annotation-api = "javax.annotation:javax.annotation-api:1.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.6.4"
+kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
[plugins]
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" }
+ajoberstar-grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
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 686051bf..230d38e5 100644
--- a/tunnel/build.gradle.kts
+++ b/tunnel/build.gradle.kts
@@ -1,4 +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()
@@ -6,6 +8,7 @@ val pkg: String = providers.gradleProperty("wireguardPackageName").get()
plugins {
alias(libs.plugins.android.library)
`maven-publish`
+ alias(libs.plugins.google.protobuf)
signing
}
@@ -50,9 +53,6 @@ android {
}
}
}
- create("googleplay") {
- initWith(getByName("release"))
- }
}
lint {
disable += "LongLogTag"
@@ -67,12 +67,69 @@ android {
}
dependencies {
+ implementation(project(":bgp-java"))
implementation(libs.androidx.annotation)
implementation(libs.androidx.collection)
+ 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/java/com/wireguard/android/backend/Backend.java b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
index edf98b9e..5ffdf8e2 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,11 @@
package com.wireguard.android.backend;
import com.wireguard.config.Config;
+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 +67,6 @@ 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);
}
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..a6a8b420
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java
@@ -0,0 +1,285 @@
+/*
+ * 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.onAllowedIpsChange(publicKey, addNetworks, removeNetworks);
+ // backend.addAllowedIps(tunnel, publicKey, addNetworks);
+ // backend.removeAllowedIps(tunnel, publicKey, addNetworks); // TODO
+ } 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..59a3e69c
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util.Set;
+
+/**
+ * Class representing DHCP info for a {@link Tunnel} instance.
+ */
+@NonNullForAll
+public class Dhcp {
+ private Set<InetNetwork> addresses;
+
+ Dhcp(Set<InetNetwork> addresses) {
+ this.addresses = addresses;
+ }
+
+ public Set<InetNetwork> getAddresses() {
+ return addresses;
+ }
+
+ 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..24f1adc8 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
@@ -7,31 +7,90 @@ package com.wireguard.android.backend;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.LocalSocketAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ProxyInfo;
+import android.net.TrafficStats;
+import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.system.OsConstants;
import android.util.Log;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Empty;
+
import com.wireguard.android.backend.BackendException.Reason;
import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.backend.gen.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.Instant;
+import java.nio.ByteOrder;
import java.util.Collections;
+import java.util.LinkedHashSet;
+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;
@@ -44,12 +103,20 @@ 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;
@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 ManagedChannel channel;
+ private ConnectivityManager connectivityManager;
+ private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback();
+ private ConnectivityManager.NetworkCallback vpnNetworkCallback;
+ @Nullable private Network activeNetwork;
+ private boolean obtainDhcpLease = false;
+ @Nullable private Bgp bgp;
/**
* Public constructor for GoBackend.
@@ -59,6 +126,11 @@ 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();
}
/**
@@ -77,12 +149,16 @@ public final class GoBackend implements Backend {
private static native int wgGetSocketV6(int handle);
+ 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 +261,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 +303,278 @@ 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);
+ }
+
+ 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 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());
+ 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{
+ 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());
- }
+ Set<InetNetwork> addresses = new LinkedHashSet<>();
+ if (resp.getLeasesList() != null) {
+ for (final Lease lease: resp.getLeasesList()) {
+ try {
+ InetAddress addr = InetAddress.getByAddress(lease.getAddress().getAddress().toByteArray());
+ Log.i(TAG, "Lease: " + addr);
+ addresses.add(new InetNetwork(addr, 128));
+ } catch (UnknownHostException ex) {
+ // Ignore
}
- break;
}
+ }
- // Build config
- final String goConfig = config.toWgUserspaceString();
+ Dhcp dhcp = new Dhcp(addresses);
- // Create the vpn tunnel with android API
+ // Replace the vpn tunnel
+ final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getAddresses());
+
+ 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");
+
+ 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<InetNetwork> 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 InetNetwork lease: leases) {
+ builder.addAddress(lease.getAddress(), lease.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 +584,111 @@ 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 (!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 +703,15 @@ public final class GoBackend implements Backend {
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
+
+ obtainDhcpLease = true;
+
+ 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 +721,16 @@ public final class GoBackend implements Backend {
currentTunnel = null;
currentTunnelHandle = -1;
currentConfig = 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();
@@ -407,10 +796,21 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
if (owner != null) {
+ if (owner.bgp != null) {
+ owner.bgp.stopServer();
+ owner.bgp = 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 +836,48 @@ 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) {
+ 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 onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+ Log.w(TAG, "onLinkPropertiesChanged: " + network + " is default:" + (network.equals(activeNetwork)));
+ if (network.equals(activeNetwork) && currentConfig != null && currentTunnelHandle > -1) {
+ final Resolver resolver = new Resolver(network, linkProperties);
+ final String goConfig = currentConfig.toWgUserspaceStringWithChangedEndpoints(resolver);
+ Log.w(TAG, "is default network, config:" + goConfig);
+
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ TunnelHandle tunnel = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build();
+ IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(tunnel).setConfig(goConfig).build();
+ IpcSetResponse resp = stub.ipcSet(request);
+ }
+ }
+ }
}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
index 766df443..fc94375b 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,15 @@ 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 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..2a3ee588 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,7 @@ 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.InetNetwork;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
@@ -167,6 +168,11 @@ public final class WgQuickBackend implements Backend {
return state;
}
+ @Override
+ public void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks) {
+ 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..5cd03fc0 100644
--- a/tunnel/src/main/java/com/wireguard/config/Config.java
+++ b/tunnel/src/main/java/com/wireguard/config/Config.java
@@ -9,6 +9,7 @@ import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.util.NonNullForAll;
+import com.wireguard.util.Resolver;
import java.io.BufferedReader;
import java.io.IOException;
@@ -173,12 +174,19 @@ public final class Config {
*
* @return the {@code Config} represented as a series of "key=value" lines
*/
- public String toWgUserspaceString() {
+ public String toWgUserspaceString(Resolver resolver) {
final StringBuilder sb = new StringBuilder();
sb.append(interfaze.toWgUserspaceString());
sb.append("replace_peers=true\n");
for (final Peer peer : peers)
- sb.append(peer.toWgUserspaceString());
+ sb.append(peer.toWgUserspaceString(resolver));
+ return sb.toString();
+ }
+
+ public String toWgUserspaceStringWithChangedEndpoints(Resolver resolver) {
+ final StringBuilder sb = new StringBuilder();
+ for (final Peer peer : peers)
+ sb.append(peer.toWgUserspaceStringWithChangedEndpoint(resolver));
return sb.toString();
}
diff --git a/tunnel/src/main/java/com/wireguard/config/HttpProxy.java b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java
new file mode 100644
index 00000000..d45914f8
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2022 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.util.NonNullForAll;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.net.ProxyInfo;
+import android.net.Uri;
+
+@NonNullForAll
+public final class HttpProxy {
+ public static final int DEFAULT_PROXY_PORT = 8080;
+
+ private ProxyInfo pi;
+
+ protected HttpProxy(ProxyInfo pi) {
+ this.pi = pi;
+ }
+
+ public ProxyInfo getProxyInfo() {
+ return pi;
+ }
+
+ public String getHost() {
+ return pi.getHost();
+ }
+
+ public Uri getPacFileUrl() {
+ return pi.getPacFileUrl();
+ }
+
+ public int getPort() {
+ return pi.getPort();
+ }
+
+ public static HttpProxy parse(final String httpProxy) throws BadConfigException {
+ try {
+ if (httpProxy.startsWith("pac:")) {
+ return new HttpProxy(ProxyInfo.buildPacProxy(Uri.parse(httpProxy.substring(4))));
+ } else {
+ final String urlStr;
+ if (!httpProxy.contains("://")) {
+ urlStr = "http://" + httpProxy;
+ } else {
+ urlStr = httpProxy;
+ }
+ URL url = new URL(urlStr);
+ return new HttpProxy(ProxyInfo.buildDirectProxy(url.getHost(), url.getPort() <= 0 ? DEFAULT_PROXY_PORT : url.getPort()));
+ }
+ } catch (final MalformedURLException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, httpProxy, e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ if (pi.getPacFileUrl() != null && pi.getPacFileUrl() != Uri.EMPTY)
+ sb.append("pac:").append(pi.getPacFileUrl());
+ else {
+ sb.append("http://").append(pi.getHost()).append(':');
+ if (pi.getPort() <= 0)
+ sb.append(DEFAULT_PROXY_PORT);
+ else
+ sb.append(pi.getPort());
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
index d1db432b..dffd534b 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,34 @@ public final class InetEndpoint {
*
* @return the resolved endpoint, or {@link Optional#empty()}
*/
- public Optional<InetEndpoint> getResolved() {
- if (isResolved)
+ public Optional<InetEndpoint> getResolved(Resolver resolver) {
+ return getResolved(resolver, false, false);
+ }
+
+ public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) {
+ return getResolved(resolver, force, false);
+ }
+
+ public Optional<InetEndpoint> getResolvedIfChanged(Resolver resolver) {
+ return getResolved(resolver, true, true);
+ }
+
+ public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force, Boolean ifChanged) {
+ if (!force && isResolved)
return Optional.of(this);
synchronized (lock) {
//TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
- if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
+ if (force || Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
try {
- // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
- final InetAddress[] candidates = InetAddress.getAllByName(host);
- InetAddress address = candidates[0];
- for (final InetAddress candidate : candidates) {
- if (candidate instanceof Inet4Address) {
- address = candidate;
- break;
- }
- }
- resolved = new InetEndpoint(address.getHostAddress(), true, port);
+ InetAddress address = resolver.resolve(host);
+ InetEndpoint resolvedNow = new InetEndpoint(address.getHostAddress(), true, port);
lastResolution = Instant.now();
+
+ if (ifChanged && resolvedNow.equals(resolved)) {
+ return Optional.empty();
+ }
+
+ resolved = resolvedNow;
} catch (final UnknownHostException e) {
resolved = null;
}
diff --git a/tunnel/src/main/java/com/wireguard/config/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..d7b75f16 100644
--- a/tunnel/src/main/java/com/wireguard/config/Peer.java
+++ b/tunnel/src/main/java/com/wireguard/config/Peer.java
@@ -11,6 +11,7 @@ import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
+import com.wireguard.util.Resolver;
import java.util.Collection;
import java.util.Collections;
@@ -190,18 +191,26 @@ public final class Peer {
*
* @return the {@code Peer} represented as a series of "key=value" lines
*/
- public String toWgUserspaceString() {
+ public String toWgUserspaceString(Resolver resolver) {
final StringBuilder sb = new StringBuilder();
// The order here is important: public_key signifies the beginning of a new peer.
sb.append("public_key=").append(publicKey.toHex()).append('\n');
for (final InetNetwork allowedIp : allowedIps)
sb.append("allowed_ip=").append(allowedIp).append('\n');
- endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ endpoint.flatMap(ep -> ep.getResolved(resolver)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
return sb.toString();
}
+ public String toWgUserspaceStringWithChangedEndpoint(Resolver resolver) {
+ final StringBuilder sb = new StringBuilder();
+ // The order here is important: public_key signifies the beginning of a new peer.
+ sb.append("public_key=").append(publicKey.toHex()).append('\n');
+ endpoint.flatMap(ep -> ep.getResolved(resolver, true)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ return sb.toString();
+ }
+
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// See wg(8)
diff --git a/tunnel/src/main/java/com/wireguard/util/Resolver.java b/tunnel/src/main/java/com/wireguard/util/Resolver.java
new file mode 100644
index 00000000..654e01f5
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java
@@ -0,0 +1,138 @@
+/*
+ * 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");
+ continue;
+ }
+
+ Log.w(TAG, "Local address: " + sock.getLocalAddress());
+
+ if (candidate instanceof Inet4Address) {
+ // Accept IPv4 as preferred address.
+ address = candidate;
+ break;
+ }
+
+ Inet6Address local = (Inet6Address)sock.getLocalAddress();
+ InetSocketAddress remoteSockAddr = (InetSocketAddress)sock.getRemoteSocketAddress();
+ Inet6Address remote = (Inet6Address)remoteSockAddr.getAddress();
+ sock.close();
+
+ if (isPreferredIPv6(local, remote)) {
+ address = candidate;
+ break;
+ }
+ }
+ Log.w(TAG, "Resolved \"" + host + "\" to: " + address);
+ return address;
+ }
+}
diff --git a/tunnel/src/main/proto/libwg.proto b/tunnel/src/main/proto/libwg.proto
new file mode 100644
index 00000000..4fa4468b
--- /dev/null
+++ b/tunnel/src/main/proto/libwg.proto
@@ -0,0 +1,125 @@
+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);
+}
+
+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;
+}
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 f1936e1d..5ae4548d 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.20.3
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
@@ -27,6 +28,13 @@ GO_HASH_darwin-amd64 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76
GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95
GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca
+PROTOC_VERSION := 3.22.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..78ee7f54 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,40 @@ 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)
}
func main() {}
diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go
new file mode 100644
index 00000000..b7919151
--- /dev/null
+++ b/tunnel/tools/libwg-go/dhcp.go
@@ -0,0 +1,192 @@
+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"
+)
+
+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 {
+ addr :=opt.(*dhcpv6.OptIAAddress).IPv6Addr
+ lease := &gen.Lease{
+ Address: &gen.InetAddress{
+ Address: addr,
+ },
+ // PreferredLifetime: ,
+ // 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..8f17923c 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-20211114080932-d06c3be7c11b
+ github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
+ golang.org/x/net v0.7.0
+ golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89
+ golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d
+ google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84
+ 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.6.0 // indirect
+ golang.org/x/text v0.7.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..dfd31072 100644
--- a/tunnel/tools/libwg-go/go.sum
+++ b/tunnel/tools/libwg-go/go.sum
@@ -1,10 +1,173 @@
-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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+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/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/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-20211114080932-d06c3be7c11b h1:1XqENn2YoYZd6w3Awx+7oa+aR87DFIZJFLF2n1IojA0=
+github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b/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/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+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/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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+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.5.1-0.20230222185716-a3b23cc77e89 h1:260HNjMTPDya+jq5AM1zZLgG9pv9GASPAGiEEJUbRg4=
+golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89/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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+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/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84 h1:hZAzgyItS2MPyqvdC8wQZI99ZLGP9Vwijyfr0dmYWc4=
+google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+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=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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..9c457286
--- /dev/null
+++ b/tunnel/tools/libwg-go/http-proxy.go
@@ -0,0 +1,716 @@
+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
+ 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
+ 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)
+
+ 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..20bfb332 100644
--- a/tunnel/tools/libwg-go/jni.c
+++ b/tunnel/tools/libwg-go/jni.c
@@ -14,6 +14,8 @@ extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle);
extern char *wgGetConfig(int handle);
extern char *wgVersion();
+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 +71,20 @@ JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion
free(version);
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..f5460486
--- /dev/null
+++ b/tunnel/tools/libwg-go/service.go
@@ -0,0 +1,316 @@
+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
+}
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index 58a6a687..25122e59 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -1,7 +1,11 @@
@file:Suppress("UnstableApiUsage")
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+// Grotesque workaround for https://issuetracker.google.com/issues/279780940
+System.setProperty("com.android.tools.r8.disableApiModeling", "1")
+
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
val appID: String = providers.gradleProperty("wireguardApplicationID").get()
@@ -9,6 +13,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt)
+ alias(libs.plugins.ajoberstar.grgit)
}
android {
@@ -24,9 +29,11 @@ android {
minSdk = 21
targetSdk = 33
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
- versionName = providers.gradleProperty("wireguardVersionName").get()
+ versionName = grgit.describe {
+ tags = true
+ always = true
+ }.replace('-', '.')
buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
- buildConfigField("boolean", "IS_GOOGLE_PLAY", false.toString())
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -50,6 +57,7 @@ android {
resources {
excludes += "DebugProbesKt.bin"
excludes += "kotlin-tooling-metadata.json"
+ excludes += "META-INF/*.version"
}
}
}
@@ -60,9 +68,12 @@ android {
}
create("googleplay") {
initWith(getByName("release"))
- buildConfigField("boolean", "IS_GOOGLE_PLAY", true.toString())
+ matchingFallbacks += "release"
}
}
+ androidResources {
+ generateLocaleConfig = true
+ }
lint {
disable += "LongLogTag"
warning += "MissingTranslation"
diff --git a/ui/src/googleplay/AndroidManifest.xml b/ui/src/googleplay/AndroidManifest.xml
index 1343edbb..6d64f732 100644
--- a/ui/src/googleplay/AndroidManifest.xml
+++ b/ui/src/googleplay/AndroidManifest.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
- <application>
- <receiver android:name=".updater.Updater$AppUpdatedReceiver" tools:node="remove" />
- </application>
+
+ <uses-permission
+ android:name="android.permission.REQUEST_INSTALL_PACKAGES"
+ tools:node="remove" />
</manifest>
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index 42341226..05f20984 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -3,7 +3,9 @@
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.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -45,10 +47,12 @@
<activity
android:name=".activity.TunnelToggleActivity"
- android:theme="@style/NoBackgroundTheme"
- android:excludeFromRecents="true"/>
+ android:excludeFromRecents="true"
+ android:theme="@style/NoBackgroundTheme" />
- <activity android:name=".activity.MainActivity" android:exported="true">
+ <activity
+ android:name=".activity.MainActivity"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -62,8 +66,8 @@
<activity
android:name=".activity.TvMainActivity"
- android:theme="@style/TvTheme"
- android:exported="true">
+ android:exported="true"
+ android:theme="@style/TvTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
@@ -87,8 +91,8 @@
<activity
android:name=".activity.LogViewerActivity"
- android:label="@string/log_viewer_title"
- android:exported="false">
+ android:exported="false"
+ android:label="@string/log_viewer_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
@@ -100,14 +104,18 @@
android:exported="false"
android:grantUriPermissions="true" />
- <receiver android:name=".BootShutdownReceiver" android:exported="true">
+ <receiver
+ android:name=".BootShutdownReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
- <receiver android:name=".updater.Updater$AppUpdatedReceiver" android:exported="true">
+ <receiver
+ android:name=".updater.Updater$AppUpdatedReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
@@ -115,8 +123,8 @@
<receiver
android:name=".model.TunnelManager$IntentReceiver"
- android:permission="${applicationId}.permission.CONTROL_TUNNELS"
- android:exported="true">
+ android:exported="true"
+ android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
@@ -126,9 +134,9 @@
<service
android:name=".QuickTileService"
+ android:exported="true"
android:icon="@drawable/ic_tile"
- android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
- android:exported="true">
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
@@ -149,5 +157,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/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
index ed208c50..d1bad2bd 100644
--- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -75,6 +75,7 @@ class QuickTileService : TileService() {
}
override fun onCreate() {
+ isAdded = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
iconOff = iconOn
@@ -98,6 +99,11 @@ class QuickTileService : TileService() {
iconOff = Icon.createWithBitmap(b)
}
+ override fun onDestroy() {
+ super.onDestroy()
+ isAdded = false
+ }
+
override fun onStartListening() {
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
@@ -109,6 +115,14 @@ class QuickTileService : TileService() {
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
}
+ override fun onTileAdded() {
+ isAdded = true
+ }
+
+ override fun onTileRemoved() {
+ isAdded = false
+ }
+
private fun updateTile() {
// Update the tunnel.
val newTunnel = Application.getTunnelManager().lastUsedTunnel
@@ -157,5 +171,7 @@ class QuickTileService : TileService() {
companion object {
private const val TAG = "WireGuard/QuickTileService"
+ var isAdded: Boolean = false
+ private set
}
}
diff --git a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
index cfd34e42..56810377 100644
--- a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
@@ -67,7 +67,8 @@ abstract class BaseActivity : AppCompatActivity() {
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
fun removeOnSelectedTunnelChangedListener(
- listener: OnSelectedTunnelChangedListener) {
+ listener: OnSelectedTunnelChangedListener
+ ) {
selectionChangeRegistry.remove(listener)
}
@@ -77,17 +78,17 @@ abstract class BaseActivity : AppCompatActivity() {
private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() {
override fun onNotifyCallback(
- listener: OnSelectedTunnelChangedListener,
- oldTunnel: ObservableTunnel?,
- ignored: Int,
- newTunnel: ObservableTunnel?
+ listener: OnSelectedTunnelChangedListener,
+ oldTunnel: ObservableTunnel?,
+ ignored: Int,
+ newTunnel: ObservableTunnel?
) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
}
}
private class SelectionChangeRegistry :
- CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
+ CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
companion object {
private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
index 9deed440..155dff36 100644
--- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
@@ -112,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() {
}
binding.shareFab.setOnClickListener {
- revokeLastUri()
- val key = KeyPair().privateKey.toHex()
- LOGS[key] = rawLogBytes()
- lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
- val shareIntent = ShareCompat.IntentBuilder(this)
+ lifecycleScope.launch {
+ revokeLastUri()
+ val key = KeyPair().privateKey.toHex()
+ LOGS[key] = rawLogBytes()
+ lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
+ val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity)
.setType("text/plain")
.setSubject(getString(R.string.log_export_subject))
.setStream(lastUri)
.setChooserTitle(R.string.log_export_title)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- revokeLastActivityResultLauncher.launch(shareIntent)
+ grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ revokeLastActivityResultLauncher.launch(shareIntent)
+ }
}
}
@@ -140,22 +142,26 @@ class LogViewerActivity : AppCompatActivity() {
finish()
true
}
+
R.id.save_log -> {
saveButton?.isEnabled = false
lifecycleScope.launch { saveLog() }
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
private val downloadsFileSaver = DownloadsFileSaver(this)
- private fun rawLogBytes() : ByteArray {
+ private suspend fun rawLogBytes(): ByteArray {
val builder = StringBuilder()
- for (i in 0 until rawLogLines.size()) {
- builder.append(rawLogLines[i])
- builder.append('\n')
+ withContext(Dispatchers.IO) {
+ for (i in 0 until rawLogLines.size()) {
+ builder.append(rawLogLines[i])
+ builder.append('\n')
+ }
}
return builder.toString().toByteArray(Charsets.UTF_8)
}
@@ -175,12 +181,14 @@ class LogViewerActivity : AppCompatActivity() {
saveButton?.isEnabled = true
if (outputFile == null)
return
- Snackbar.make(findViewById(android.R.id.content),
- if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
- else getString(R.string.log_export_error, ErrorMessages[exception]),
- if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
- .setAnchorView(binding.shareFab)
- .show()
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
+ else getString(R.string.log_export_error, ErrorMessages[exception]),
+ if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
+ )
+ .setAnchorView(binding.shareFab)
+ .show()
}
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
@@ -283,7 +291,8 @@ class LogViewerActivity : AppCompatActivity() {
*
* <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre>
*/
- private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
+ private val THREADTIME_LINE: Pattern =
+ Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
private const val TAG = "WireGuard/LogViewerActivity"
}
@@ -306,7 +315,7 @@ class LogViewerActivity : AppCompatActivity() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.log_viewer_entry, parent, false)
+ .inflate(R.layout.log_viewer_entry, parent, false)
return ViewHolder(view)
}
@@ -317,8 +326,10 @@ class LogViewerActivity : AppCompatActivity() {
else
SpannableString("${line.tag}: ${line.msg}").apply {
setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- setSpan(ForegroundColorSpan(levelToColor(line.level)),
- 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ setSpan(
+ ForegroundColorSpan(levelToColor(line.level)),
+ 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
}
holder.layout.apply {
findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
@@ -340,11 +351,11 @@ class LogViewerActivity : AppCompatActivity() {
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? =
- logForUri(uri)?.let {
- val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
- m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
- m
- }
+ logForUri(uri)?.let {
+ val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
+ m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
+ m
+ }
override fun onCreate(): Boolean = true
@@ -354,7 +365,8 @@ class LogViewerActivity : AppCompatActivity() {
override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" }
- override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
+ override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? =
+ getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (mode != "r") return null
diff --git a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
index b6c67e88..80c4868c 100644
--- a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
@@ -77,6 +77,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
onBackPressedDispatcher.onBackPressed()
true
}
+
R.id.menu_action_edit -> {
supportFragmentManager.commit {
replace(R.id.detail_container, TunnelEditorFragment())
@@ -91,12 +92,15 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
startActivity(Intent(this, SettingsActivity::class.java))
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
- override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
- newTunnel: ObservableTunnel?): Boolean {
+ override fun onSelectedTunnelChanged(
+ oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?
+ ): Boolean {
val fragmentManager = supportFragmentManager
if (fragmentManager.isStateSaved) {
return false
diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
index 53b25938..bd6e1f78 100644
--- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
@@ -4,9 +4,11 @@
*/
package com.wireguard.android.activity
+import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
+import android.service.quicksettings.TileService
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
@@ -14,6 +16,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.wireguard.android.Application
+import com.wireguard.android.QuickTileService
import com.wireguard.android.R
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.preference.PreferencesPreferenceDataStore
@@ -47,7 +50,13 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
addPreferencesFromResource(R.xml.preferences)
- preferenceScreen.initialExpandedChildrenCount = 4
+ preferenceScreen.initialExpandedChildrenCount = 5
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || QuickTileService.isAdded) {
+ val quickTile = preferenceManager.findPreference<Preference>("quick_tile")
+ quickTile?.parent?.removePreference(quickTile)
+ --preferenceScreen.initialExpandedChildrenCount
+ }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val darkTheme = preferenceManager.findPreference<Preference>("dark_theme")
darkTheme?.parent?.removePreference(darkTheme)
@@ -62,9 +71,9 @@ class SettingsActivity : AppCompatActivity() {
zipExporter?.parent?.removePreference(zipExporter)
}
val wgQuickOnlyPrefs = arrayOf(
- preferenceManager.findPreference("tools_installer"),
- preferenceManager.findPreference("restore_on_boot"),
- preferenceManager.findPreference<Preference>("multiple_tunnels")
+ preferenceManager.findPreference("tools_installer"),
+ preferenceManager.findPreference("restore_on_boot"),
+ preferenceManager.findPreference<Preference>("multiple_tunnels")
).filterNotNull()
wgQuickOnlyPrefs.forEach { it.isVisible = false }
lifecycleScope.launch {
diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
index ee95ce40..59b9349f 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
@@ -24,7 +24,8 @@ import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
- private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
+ private val permissionActivityResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
index 4545672f..4c86b4c8 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
@@ -211,12 +211,13 @@ class TvMainActivity : AppCompatActivity() {
try {
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
- MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false).setPositiveButton(android.R.string.ok) { _, _ ->
- try {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
- } catch (_: Throwable) {
- }
- }.show()
+ MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
+ } catch (_: Throwable) {
+ }
+ }.show()
}
}
}
@@ -359,6 +360,7 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
}
+
filesRoot.get()?.isNotEmpty() == true -> {
files.clear()
filesRoot.set("")
@@ -372,7 +374,7 @@ class TvMainActivity : AppCompatActivity() {
private suspend fun updateStats() {
binding.tunnelList.forEach { viewItem ->
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
- ?: return@forEach
+ ?: return@forEach
try {
val tunnel = listItem.item!!
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
diff --git a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
index 30a2674f..17e3221b 100644
--- a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
+++ b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
@@ -40,9 +40,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun enumerate(): Set<String> {
return context.fileList()
- .filter { it.endsWith(".conf") }
- .map { it.substring(0, it.length - ".conf".length) }
- .toSet()
+ .filter { it.endsWith(".conf") }
+ .map { it.substring(0, it.length - ".conf".length) }
+ .toSet()
}
private fun fileFor(name: String): File {
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 fd7bc72c..afba41cb 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
@@ -47,9 +47,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout", "fragment")
- fun <E> setItems(view: LinearLayout,
- oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
- newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?) {
+ fun <E> setItems(
+ view: LinearLayout,
+ oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
+ newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?
+ ) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
@@ -73,9 +75,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout")
- fun <E> setItems(view: LinearLayout,
- oldList: Iterable<E>?, oldLayoutId: Int,
- newList: Iterable<E>?, newLayoutId: Int) {
+ fun <E> setItems(
+ view: LinearLayout,
+ oldList: Iterable<E>?, oldLayoutId: Int,
+ newList: Iterable<E>?, newLayoutId: Int
+ ) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
view.removeAllViews()
@@ -93,11 +97,13 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
- fun <K, E : Keyed<out K>> setItems(view: RecyclerView,
- oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
- @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
- newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
- newRowConfigurationHandler: RowConfigurationHandler<*, *>?) {
+ fun <K, E : Keyed<out K>> setItems(
+ view: RecyclerView,
+ oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
+ @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
+ newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
+ newRowConfigurationHandler: RowConfigurationHandler<*, *>?
+ ) {
if (view.layoutManager == null)
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
if (oldList === newList && oldLayoutId == newLayoutId)
@@ -123,16 +129,20 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("onBeforeCheckedChanged")
- fun setOnBeforeCheckedChanged(view: ToggleSwitch,
- listener: OnBeforeCheckedChangeListener?) {
+ fun setOnBeforeCheckedChanged(
+ view: ToggleSwitch,
+ listener: OnBeforeCheckedChangeListener?
+ ) {
view.setOnBeforeCheckedChangeListener(listener)
}
@JvmStatic
@BindingAdapter("onFocusChange")
- fun setOnFocusChange(view: EditText,
- listener: View.OnFocusChangeListener?) {
- view.setOnFocusChangeListener(listener)
+ fun setOnFocusChange(
+ view: EditText,
+ listener: View.OnFocusChangeListener?
+ ) {
+ view.onFocusChangeListener = listener
}
@JvmStatic
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
index 93333cb6..da153bbe 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
@@ -61,8 +61,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeChanged(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount) {
@@ -75,8 +77,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeInserted(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount)
@@ -86,8 +90,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
- toPosition: Int, itemCount: Int) {
+ override fun onItemRangeMoved(
+ sender: ObservableList<T>, fromPosition: Int,
+ toPosition: Int, itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
val views = arrayOfNulls<View>(itemCount)
@@ -99,8 +105,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeRemoved(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
listener.container.removeViews(positionStart, itemCount)
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
index 531cf90d..1cd19934 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
@@ -49,7 +49,8 @@ class AppListDialogFragment : DialogFragment() {
packageInfos.forEach {
val packageName = it.packageName
val appInfo = it.applicationInfo
- val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
+ val appData =
+ ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
applicationData.add(appData)
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
@@ -143,10 +144,12 @@ class AppListDialogFragment : DialogFragment() {
selectedApps.add(data.packageName)
}
}
- setFragmentResult(REQUEST_SELECTION, bundleOf(
+ setFragmentResult(
+ REQUEST_SELECTION, bundleOf(
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
- ))
+ )
+ )
dismiss()
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
index b70d53be..d5c1723f 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
@@ -99,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
- .setAnchorView(view.findViewById(R.id.create_fab))
- .show()
+ .setAnchorView(view.findViewById(R.id.create_fab))
+ .show()
else
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
index 5fa7297b..34c96505 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
@@ -37,6 +37,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
}
}
}
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val configText = requireArguments().getString(KEY_CONFIG_TEXT)
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 57e6828a..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
@@ -40,8 +42,10 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
menuInflater.inflate(R.menu.tunnel_detail, menu)
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
binding?.executePendingBindings()
@@ -77,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
}
@@ -110,16 +116,18 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
val statistics = tunnel.getStatisticsAsync()
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
- ?: continue
+ ?: continue
val publicKey = peer.item!!.publicKey
val peerStats = statistics.peer(publicKey)
if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
} else {
- peer.transferText.text = getString(R.string.transfer_rx_tx,
+ peer.transferText.text = getString(
+ R.string.transfer_rx_tx,
QuantityFormatter.formatBytes(peerStats.rxBytes),
- QuantityFormatter.formatBytes(peerStats.txBytes))
+ QuantityFormatter.formatBytes(peerStats.txBytes)
+ )
peer.transferLabel.visibility = View.VISIBLE
peer.transferText.visibility = View.VISIBLE
}
@@ -135,7 +143,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
} catch (e: Throwable) {
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
- ?: continue
+ ?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
peer.latestHandshakeLabel.visibility = View.GONE
@@ -143,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 7a8b822e..6be27f94 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -5,7 +5,6 @@
package com.wireguard.android.fragment
import android.content.Context
-import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
@@ -17,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
@@ -33,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
@@ -44,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)
}
@@ -67,22 +86,27 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
}
}
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- }
-
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.config_editor, menu)
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
binding?.apply {
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
}
@@ -103,8 +127,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
val focusedView = activity.currentFocus
if (focusedView != null) {
val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
- inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
- InputMethodManager.HIDE_NOT_ALWAYS)
+ inputManager?.hideSoftInputFromWindow(
+ focusedView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
}
parentFragmentManager.popBackStackImmediate()
@@ -138,6 +164,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
onTunnelCreated(null, e)
}
}
+
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
try {
@@ -147,6 +174,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
onTunnelRenamed(tunnel!!, newConfig, e)
}
}
+
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
try {
@@ -202,8 +230,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
super.onSaveInstanceState(outState)
}
- override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
- newTunnel: ObservableTunnel?) {
+ override fun onSelectedTunnelChanged(
+ oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?
+ ) {
tunnel = newTunnel
if (binding == null) return
binding!!.config = ConfigProxy()
@@ -240,8 +270,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
}
}
- private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
- throwable: Throwable?) {
+ private suspend fun onTunnelRenamed(
+ renamedTunnel: ObservableTunnel, newConfig: Config,
+ throwable: Throwable?
+ ) {
val ctx = activity ?: Application.get()
if (throwable == null) {
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
@@ -298,13 +330,15 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
haveShownKeys = true
showPrivateKey(edit)
}
+
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
- binding!!.mainContainer,
- it.message,
- Snackbar.LENGTH_SHORT
+ binding!!.mainContainer,
+ it.message,
+ Snackbar.LENGTH_SHORT
).show()
}
+
is BiometricAuthenticator.Result.Cancelled -> {}
}
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
index 8c5389d9..cba7c476 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -81,6 +81,8 @@ class TunnelListFragment : BaseFragment() {
}
}
+ private val snackbarUpdateShower = SnackbarUpdateShower(this)
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
@@ -91,8 +93,10 @@ class TunnelListFragment : BaseFragment() {
}
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
val bottomSheet = AddTunnelsSheet()
@@ -105,26 +109,29 @@ class TunnelListFragment : BaseFragment() {
AddTunnelsSheet.REQUEST_CREATE -> {
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
}
+
AddTunnelsSheet.REQUEST_IMPORT -> {
tunnelFileImportResultLauncher.launch("*/*")
}
+
AddTunnelsSheet.REQUEST_SCAN -> {
- qrImportResultLauncher.launch(ScanOptions()
+ qrImportResultLauncher.launch(
+ ScanOptions()
.setOrientationLocked(false)
.setBeepEnabled(false)
- .setPrompt(getString(R.string.qr_code_hint)))
+ .setPrompt(getString(R.string.qr_code_hint))
+ )
}
}
}
bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
}
executePendingBindings()
+ snackbarUpdateShower.attach(mainContainer, createFab)
}
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
backPressedCallback?.isEnabled = false
- SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab)
-
return binding?.root
}
@@ -191,8 +198,8 @@ class TunnelListFragment : BaseFragment() {
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
- .setAnchorView(binding.createFab)
- .show()
+ .setAnchorView(binding.createFab)
+ .show()
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
}
@@ -234,6 +241,7 @@ class TunnelListFragment : BaseFragment() {
mode.finish()
true
}
+
R.id.menu_action_select_all -> {
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
@@ -243,6 +251,7 @@ class TunnelListFragment : BaseFragment() {
}
true
}
+
else -> false
}
}
@@ -308,7 +317,7 @@ class TunnelListFragment : BaseFragment() {
private fun animateFab(view: View?, show: Boolean) {
view ?: return
val animation = AnimationUtils.loadAnimation(
- context, if (show) R.anim.scale_up else R.anim.scale_down
+ context, if (show) R.anim.scale_up else R.anim.scale_down
)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
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 c3e3405e..5c34b57f 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
@@ -21,10 +29,10 @@ import kotlinx.coroutines.withContext
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
class ObservableTunnel internal constructor(
- private val manager: TunnelManager,
- private var name: String,
- config: Config?,
- state: Tunnel.State
+ private val manager: TunnelManager,
+ private var name: String,
+ config: Config?,
+ state: Tunnel.State
) : BaseObservable(), Keyed<String>, Tunnel {
override val key
get() = name
@@ -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
}
@@ -137,6 +162,106 @@ class ObservableTunnel internal constructor(
}
+ @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 peer: PeerDetail? = null
+
+ configDetail?.peers?.forEach {
+ if (it.publicKey.equals(publicKey) == true) {
+ Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer)
+ peer = it;
+ }
+ }
+
+ if (peer == null) {
+ Log.i(TAG, "ObservableTunnel create peer " + publicKey)
+ peer = PeerDetail(publicKey)
+ configDetail?.peers?.add(peer)
+ }
+
+ var peer2: PeerDetail = peer!!
+
+ 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 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 e7bb751b..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) })!!
}
@@ -140,7 +141,8 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
if (previouslyRunning.isEmpty()) return
withContext(Dispatchers.IO) {
try {
- tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
+ tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }
+ .awaitAll()
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
@@ -155,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/preference/DonatePreference.kt b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
index 59980dcb..16920923 100644
--- a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
@@ -12,8 +12,8 @@ import android.util.AttributeSet
import android.widget.Toast
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.wireguard.android.BuildConfig
import com.wireguard.android.R
+import com.wireguard.android.updater.Updater
import com.wireguard.android.util.ErrorMessages
class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
@@ -23,7 +23,7 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont
override fun onClick() {
/* Google Play Store forbids links to our donation page. */
- if (BuildConfig.IS_GOOGLE_PLAY) {
+ if (Updater.installerIsGooglePlay(context)) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.donate_title)
.setMessage(R.string.donate_google_play_disappointment)
diff --git a/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
new file mode 100644
index 00000000..9081818b
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import android.app.StatusBarManager
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.util.AttributeSet
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.preference.Preference
+import com.wireguard.android.QuickTileService
+import com.wireguard.android.R
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
+
+ override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
+
+ override fun onClick() {
+ val statusBarManager = context.getSystemService(StatusBarManager::class.java)
+ statusBarManager.requestAddTileService(
+ ComponentName(context, QuickTileService::class.java),
+ context.getString(R.string.quick_settings_tile_action),
+ Icon.createWithResource(context, R.drawable.ic_tile),
+ context.mainExecutor
+ ) {
+ when (it) {
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
+ parent?.removePreference(this)
+ --preferenceManager.preferenceScreen.initialExpandedChildrenCount
+ }
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
+ Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
index ced95b69..220796e0 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
@@ -70,14 +70,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
val message = context.getString(R.string.zip_export_error, error)
Log.e(TAG, message, e)
Snackbar.make(
- activity.findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show()
+ activity.findViewById(android.R.id.content),
+ message, Snackbar.LENGTH_LONG
+ ).show()
isEnabled = true
}
}
}
- override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
+ override fun getSummary() =
+ if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
override fun getTitle() = context.getString(R.string.zip_export_title)
@@ -91,13 +93,15 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
isEnabled = false
exportZip()
}
+
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
- activity.findViewById(android.R.id.content),
- it.message,
- Snackbar.LENGTH_SHORT
+ activity.findViewById(android.R.id.content),
+ it.message,
+ Snackbar.LENGTH_SHORT
).show()
}
+
is BiometricAuthenticator.Result.Cancelled -> {}
}
}
diff --git a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
index d3c1b4f8..8c1a8124 100644
--- a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
+++ b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
@@ -5,13 +5,15 @@
package com.wireguard.android.updater
+import android.content.Intent
+import android.net.Uri
import android.view.View
+import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
-import com.wireguard.android.BuildConfig
import com.wireguard.android.R
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.QuantityFormatter
@@ -21,15 +23,21 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
-object SnackbarUpdateShower {
- private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) {
- val actionSnackbar = makeSnackbar(activity, view, anchor)
- val statusSnackbar = makeSnackbar(activity, view, anchor)
- var showingAction: Boolean = false
- var showingStatus: Boolean = false
+class SnackbarUpdateShower(private val fragment: Fragment) {
+ private var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
+ private val intentLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ lastUserIntervention?.markAsDone()
+ }
+
+ private class SwapableSnackbar(fragment: Fragment, view: View, anchor: View?) {
+ private val actionSnackbar = makeSnackbar(fragment, view, anchor)
+ private val statusSnackbar = makeSnackbar(fragment, view, anchor)
+ private var showingAction: Boolean = false
+ private var showingStatus: Boolean = false
+ private var permanentAction: Boolean = false
- private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar {
- val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE)
+ private fun makeSnackbar(fragment: Fragment, view: View, anchor: View?): Snackbar {
+ val snackbar = Snackbar.make(fragment.requireContext(), view, "", Snackbar.LENGTH_INDEFINITE)
if (anchor != null)
snackbar.anchorView = anchor
snackbar.setTextMaxLines(6)
@@ -41,12 +49,11 @@ object SnackbarUpdateShower {
snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
super.onDismissed(snackbar, event)
- if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
- (snackbar == actionSnackbar && !showingAction) ||
- (snackbar == statusSnackbar && !showingStatus)
+ if (event == DISMISS_EVENT_MANUAL || (event == DISMISS_EVENT_ACTION && !permanentAction) ||
+ (snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus)
)
return
- activity.lifecycleScope.launch {
+ fragment.lifecycleScope.launch {
delay(5.seconds)
snackbar?.show()
}
@@ -55,11 +62,12 @@ object SnackbarUpdateShower {
return snackbar
}
- fun showAction(text: String, action: String, listener: View.OnClickListener) {
+ fun showAction(text: String, action: String, permanent: Boolean = false, listener: View.OnClickListener) {
if (showingStatus) {
showingStatus = false
statusSnackbar.dismiss()
}
+ permanentAction = permanent
actionSnackbar.setText(text)
actionSnackbar.setAction(action, listener)
if (!showingAction) {
@@ -88,17 +96,9 @@ object SnackbarUpdateShower {
}
}
- fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) {
- if (BuildConfig.IS_GOOGLE_PLAY)
- return
-
- val snackbar = SwapableSnackbar(activity, view, anchor)
- val context = activity.applicationContext
-
- var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
- val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- lastUserIntervention?.markAsDone()
- }
+ fun attach(view: View, anchor: View?) {
+ val snackbar = SwapableSnackbar(fragment, view, anchor)
+ val context = fragment.requireContext()
Updater.state.onEach { progress ->
when (progress) {
@@ -106,10 +106,7 @@ object SnackbarUpdateShower {
snackbar.dismiss()
is Updater.Progress.Available ->
- snackbar.showAction(
- context.getString(R.string.updater_avalable),
- context.getString(R.string.updater_action)
- ) {
+ snackbar.showAction(context.getString(R.string.updater_avalable), context.getString(R.string.updater_action)) {
progress.update()
}
@@ -145,16 +142,23 @@ object SnackbarUpdateShower {
}
is Updater.Progress.Failure -> {
- snackbar.showText(
- context.getString(
- R.string.updater_failure,
- ErrorMessages[progress.error]
- )
- )
+ snackbar.showText(context.getString(R.string.updater_failure, ErrorMessages[progress.error]))
delay(5.seconds)
progress.retry()
}
+
+ is Updater.Progress.Corrupt -> {
+ snackbar.showAction(context.getString(R.string.updater_corrupt), context.getString(R.string.updater_corrupt_navigate), true) {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(progress.downloadUrl)
+ try {
+ context.startActivity(intent)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
}
- }.launchIn(activity.lifecycleScope)
+ }.launchIn(fragment.lifecycleScope)
}
} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
index aa3256d4..7fc58e08 100644
--- a/ui/src/main/java/com/wireguard/android/updater/Updater.kt
+++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
@@ -4,12 +4,14 @@
*/
package com.wireguard.android.updater
+import android.Manifest
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
import android.os.Build
import android.util.Base64
import android.util.Log
@@ -44,17 +46,32 @@ import kotlin.time.Duration.Companion.seconds
object Updater {
private const val TAG = "WireGuard/Updater"
- private const val LATEST_VERSION_URL =
- "https://download.wireguard.com/android-client/latest.sig"
- private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s"
- private val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID.removeSuffix(".debug") + "-"
+ private const val UPDATE_URL_FMT = "https://download.wireguard.com/android-client/%s"
+ private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
private const val APK_NAME_SUFFIX = ".apk"
- private const val RELEASE_PUBLIC_KEY_BASE64 =
- "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
private val CURRENT_VERSION = Version(BuildConfig.VERSION_NAME.removeSuffix("-debug"))
+ private val LATEST_FILE = if (BuildConfig.DEBUG) "latest-debug.sig" else "latest.sig"
+ private val RELEASE_PUBLIC_KEY_BASE64 =
+ if (BuildConfig.DEBUG) "RWTKFCNaLimMkuolGwAw12XT6sx+nnS7KE1wmhJ7YHXvAPedtPR/rofU"
+ else "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
private val updaterScope = CoroutineScope(Job() + Dispatchers.IO)
+ private fun installer(context: Context): String = try {
+ val packageName = context.packageName
+ val pm = context.packageManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getInstallerPackageName(packageName) ?: ""
+ }
+ } catch (_: Throwable) {
+ ""
+ }
+
+ fun installerIsGooglePlay(context: Context): Boolean = installer(context) == "com.android.vending"
+
sealed class Progress {
object Complete : Progress()
class Available(val version: String) : Progress() {
@@ -101,6 +118,11 @@ object Updater {
}
}
}
+
+ class Corrupt(private val betterFile: String?) : Progress() {
+ val downloadUrl: String
+ get() = UPDATE_URL_FMT.format(betterFile ?: "")
+ }
}
private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
@@ -131,7 +153,10 @@ object Updater {
throw InvalidParameterException("Version has no parts")
parts = ULongArray(strParts.size)
for (i in parts.indices) {
- parts[i] = strParts[i].toULong()
+ if (strParts[i][0] == 'g')
+ parts[i] = strParts[i].substring(1).toULong(16)
+ else
+ parts[i] = strParts[i].toULong()
}
}
@@ -200,7 +225,7 @@ object Updater {
}
private fun checkForUpdates(): Update? {
- val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection
+ val connection = URL(UPDATE_URL_FMT.format(LATEST_FILE)).openConnection() as HttpURLConnection
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK)
@@ -219,12 +244,7 @@ object Updater {
val receiver = InstallReceiver()
val context = Application.get().applicationContext
val pendingIntent = withContext(Dispatchers.Main) {
- ContextCompat.registerReceiver(
- context,
- receiver,
- IntentFilter(receiver.sessionId),
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
+ ContextCompat.registerReceiver(context, receiver, IntentFilter(receiver.sessionId), ContextCompat.RECEIVER_NOT_EXPORTED)
PendingIntent.getBroadcast(
context,
0,
@@ -241,24 +261,20 @@ object Updater {
}
emitProgress(Progress.Downloading(0UL, 0UL), true)
- val connection =
- URL(APK_PATH_URL.format(update.fileName)).openConnection() as HttpURLConnection
+ val connection = URL(UPDATE_URL_FMT.format(update.fileName)).openConnection() as HttpURLConnection
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK)
throw IOException("Update could not be fetched: ${connection.responseCode}")
var downloadedByteLen: ULong = 0UL
- val totalByteLen =
- (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong()
- .toULong()
+ val totalByteLen = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong().toULong()
val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
val digest = MessageDigest.getInstance("SHA-256")
emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
val installer = context.packageManager.packageInstaller
- val params =
- PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */
@@ -316,18 +332,10 @@ object Updater {
if (sessionId != intent.action)
return
- when (val status =
- intent.getIntExtra(
- PackageInstaller.EXTRA_STATUS,
- PackageInstaller.STATUS_FAILURE_INVALID
- )) {
+ when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
- val userIntervention = IntentCompat.getParcelableExtra(
- intent,
- Intent.EXTRA_INTENT,
- Intent::class.java
- )!!
+ val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
Application.getCoroutineScope().launch {
emitProgress(Progress.NeedsUserIntervention(userIntervention, id))
}
@@ -346,9 +354,7 @@ object Updater {
context.applicationContext.packageManager.packageInstaller.abandonSession(id)
} catch (_: SecurityException) {
}
- val message =
- intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
- ?: "Installation error $status"
+ val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
Application.getCoroutineScope().launch {
val e = Exception(message)
Log.e(TAG, "Update failure", e)
@@ -361,13 +367,31 @@ object Updater {
}
fun monitorForUpdates() {
- if (BuildConfig.IS_GOOGLE_PLAY)
+ val context = Application.get()
+
+ if (installerIsGooglePlay(context))
+ return
+
+ if (!if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ @Suppress("DEPRECATION")
+ context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
+ } else {
+ context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
+ }.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
+ ) {
+ updaterScope.launch {
+ val update = try {
+ checkForUpdates()
+ } catch (_: Throwable) {
+ null
+ }
+ emitProgress(Progress.Corrupt(update?.fileName))
+ }
return
+ }
updaterScope.launch {
- if (UserKnobs.updaterNewerVersionSeen.firstOrNull()
- ?.let { Version(it) > CURRENT_VERSION } == true
- )
+ if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { Version(it) > CURRENT_VERSION } == true)
return@launch
var waitTime = 15
@@ -387,8 +411,10 @@ object Updater {
}
UserKnobs.updaterNewerVersionSeen.onEach { ver ->
- if (ver != null && Version(ver) > CURRENT_VERSION && UserKnobs.updaterNewerVersionConsented.firstOrNull()
- ?.let { Version(it) > CURRENT_VERSION } != true
+ if (
+ ver != null &&
+ Version(ver) > CURRENT_VERSION &&
+ UserKnobs.updaterNewerVersionConsented.firstOrNull()?.let { Version(it) > CURRENT_VERSION } != true
)
emitProgress(Progress.Available(ver))
}.launchIn(Application.getCoroutineScope())
@@ -403,25 +429,10 @@ object Updater {
class AppUpdatedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
- if (BuildConfig.IS_GOOGLE_PLAY)
- return
-
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED)
return
- val installer = try {
- val packageName = context.packageName
- val pm = context.packageManager
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
- } else {
- @Suppress("DEPRECATION")
- pm.getInstallerPackageName(packageName) ?: ""
- }
- } catch (_: Throwable) {
- ""
- }
- if (installer != context.packageName)
+ if (installer(context) != context.packageName)
return
/* TODO: does not work because of restrictions placed on broadcast receivers. */
@@ -431,4 +442,4 @@ object Updater {
context.startActivity(start)
}
}
-} \ No newline at end of file
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
index 430e904d..2f90b2bb 100644
--- a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
+++ b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
@@ -13,5 +13,5 @@ object AdminKnobs {
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
val disableConfigExport: Boolean
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
- ?: false
+ ?: false
}
diff --git a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
index fe36898f..54d4da87 100644
--- a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
+++ b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
@@ -31,25 +31,29 @@ object BiometricAuthenticator {
}
fun authenticate(
- @StringRes dialogTitleRes: Int,
- fragment: Fragment,
- callback: (Result) -> Unit
+ @StringRes dialogTitleRes: Int,
+ fragment: Fragment,
+ callback: (Result) -> Unit
) {
val authCallback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
- callback(when (errorCode) {
- BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
- BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
- Result.Cancelled
- }
- BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
- BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
- Result.HardwareUnavailableOrDisabled
+ callback(
+ when (errorCode) {
+ BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
+ BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
+ Result.Cancelled
+ }
+
+ BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
+ BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
+ Result.HardwareUnavailableOrDisabled
+ }
+
+ else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
}
- else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
- })
+ )
}
override fun onAuthenticationFailed() {
@@ -64,9 +68,9 @@ object BiometricAuthenticator {
}
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(fragment.getString(dialogTitleRes))
- .setAllowedAuthenticators(allowedAuthenticators)
- .build()
+ .setTitle(fragment.getString(dialogTitleRes))
+ .setAllowedAuthenticators(allowedAuthenticators)
+ .build()
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
biometricPrompt.authenticate(promptInfo)
} else {
diff --git a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
index 8538e75e..ace1dc05 100644
--- a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
+++ b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
@@ -46,9 +46,9 @@ class DownloadsFileSaver(private val context: ComponentActivity) {
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
- ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
- ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
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 66027d95..97be8c99 100644
--- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -22,47 +22,47 @@ import java.net.InetAddress
object ErrorMessages {
private val BCE_REASON_MAP = mapOf(
- BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
- BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
- BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
- BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
- BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
- BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
- BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
- BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
+ BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
+ BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
+ BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
+ BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
+ BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
+ BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
+ BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
+ BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
)
private val BE_REASON_MAP = mapOf(
- BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
- BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
- BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
- BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
- BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
- BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
- BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
- BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
+ BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
+ BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
+ BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
+ BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
+ BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
+ BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
+ BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
+ BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
)
private val KFE_FORMAT_MAP = mapOf(
- Key.Format.BASE64 to R.string.key_length_explanation_base64,
- Key.Format.BINARY to R.string.key_length_explanation_binary,
- Key.Format.HEX to R.string.key_length_explanation_hex
+ Key.Format.BASE64 to R.string.key_length_explanation_base64,
+ Key.Format.BINARY to R.string.key_length_explanation_binary,
+ Key.Format.HEX to R.string.key_length_explanation_hex
)
private val KFE_TYPE_MAP = mapOf(
- KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
- KeyFormatException.Type.LENGTH to R.string.key_length_error
+ KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
+ KeyFormatException.Type.LENGTH to R.string.key_length_error
)
private val PE_CLASS_MAP = mapOf(
- InetAddress::class.java to R.string.parse_error_inet_address,
- InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
- InetNetwork::class.java to R.string.parse_error_inet_network,
- Int::class.java to R.string.parse_error_integer
+ InetAddress::class.java to R.string.parse_error_inet_address,
+ InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
+ InetNetwork::class.java to R.string.parse_error_inet_network,
+ Int::class.java to R.string.parse_error_integer
)
private val RSE_REASON_MAP = mapOf(
- RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
- RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
- RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
- RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
- RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
- RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
+ RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
+ RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
+ RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
+ RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
+ RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
+ RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
)
operator fun get(throwable: Throwable?): String {
@@ -80,21 +80,27 @@ object ErrorMessages {
val explanation = getBadConfigExceptionExplanation(resources, rootCause)
resources.getString(R.string.bad_config_error, reason, context) + explanation
}
+
rootCause is BackendException -> {
resources.getString(BE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
+
rootCause is RootShellException -> {
resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
+
rootCause is NotFoundException -> {
resources.getString(R.string.error_no_qr_found)
}
+
rootCause is ChecksumException -> {
resources.getString(R.string.error_qr_checksum)
}
+
rootCause.localizedMessage != null -> {
rootCause.localizedMessage!!
}
+
else -> {
val errorType = rootCause.javaClass.simpleName
resources.getString(R.string.generic_error, errorType)
@@ -102,8 +108,10 @@ object ErrorMessages {
}
}
- private fun getBadConfigExceptionExplanation(resources: Resources,
- bce: BadConfigException): String {
+ private fun getBadConfigExceptionExplanation(
+ resources: Resources,
+ bce: BadConfigException
+ ): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format))
@@ -114,14 +122,18 @@ 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)
}
return ""
}
- private fun getBadConfigExceptionReason(resources: Resources,
- bce: BadConfigException): String {
+ private fun getBadConfigExceptionReason(
+ resources: Resources,
+ bce: BadConfigException
+ ): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
return resources.getString(KFE_TYPE_MAP.getValue(kfe!!.type))
@@ -137,7 +149,8 @@ object ErrorMessages {
var cause = throwable
while (cause.cause != null) {
if (cause is BadConfigException || cause is BackendException ||
- cause is RootShellException) break
+ cause is RootShellException
+ ) break
val nextCause = cause.cause!!
if (nextCause is RemoteException) break
cause = nextCause
diff --git a/ui/src/main/java/com/wireguard/android/util/Extensions.kt b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
index 98f94af9..3bc85051 100644
--- a/ui/src/main/java/com/wireguard/android/util/Extensions.kt
+++ b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
@@ -25,7 +25,7 @@ val Any.applicationScope: CoroutineScope
val Preference.activity: SettingsActivity
get() = context as? SettingsActivity
- ?: throw IllegalStateException("Failed to resolve SettingsActivity")
+ ?: throw IllegalStateException("Failed to resolve SettingsActivity")
val Preference.lifecycleScope: CoroutineScope
get() = activity.lifecycleScope
diff --git a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt
index 135fc1f3..abc025a4 100644
--- a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt
+++ b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt
@@ -55,11 +55,13 @@ class QrCodeFromFileScanner(
multFactor = originalWidth.toFloat() / originalHeight.toFloat()
newWidth = (newHeight * multFactor).toInt()
}
+
originalWidth > originalHeight -> {
newWidth = scaledSize
multFactor = originalHeight.toFloat() / originalWidth.toFloat()
newHeight = (newWidth * multFactor).toInt()
}
+
originalHeight == originalWidth -> {
newHeight = scaledSize
newWidth = scaledSize
diff --git a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
index bc059e1b..f7de2465 100644
--- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
+++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
@@ -15,7 +15,6 @@ import com.wireguard.android.Application
import com.wireguard.android.R
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
-import kotlin.time.DurationUnit
object QuantityFormatter {
fun formatBytes(bytes: Long): String {
diff --git a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
index e66691e8..daefc378 100644
--- a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
+++ b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
@@ -24,7 +24,6 @@ import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
-import java.util.ArrayList
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -135,12 +134,16 @@ object TunnelImporter {
message = context.getString(R.string.import_success, tunnels[0].name)
else if (tunnels.isEmpty() && throwables.size == 1)
else if (throwables.isEmpty())
- message = context.resources.getQuantityString(R.plurals.import_total_success,
- tunnels.size, tunnels.size)
+ message = context.resources.getQuantityString(
+ R.plurals.import_total_success,
+ tunnels.size, tunnels.size
+ )
else if (!throwables.isEmpty())
- message = context.resources.getQuantityString(R.plurals.import_partial_success,
- tunnels.size + throwables.size,
- tunnels.size, tunnels.size + throwables.size)
+ message = context.resources.getQuantityString(
+ R.plurals.import_partial_success,
+ tunnels.size + throwables.size,
+ tunnels.size, tunnels.size + throwables.size
+ )
messageCallback(message)
}
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/ConfigProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
index 0be18a6f..c73b1efc 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
@@ -55,9 +55,9 @@ class ConfigProxy : Parcelable {
val resolvedPeers: MutableCollection<Peer> = ArrayList()
peers.forEach { resolvedPeers.add(it.resolve()) }
return Config.Builder()
- .setInterface(`interface`.resolve())
- .addPeers(resolvedPeers)
- .build()
+ .setInterface(`interface`.resolve())
+ .addPeers(resolvedPeers)
+ .build()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
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..16c3e6a3 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() != null && 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..80b32fd5
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt
@@ -0,0 +1,85 @@
+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()
+
+ 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/java/com/wireguard/android/viewmodel/PeerProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
index 4bf2ce9c..e78d0826 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
@@ -16,8 +16,6 @@ import com.wireguard.config.Attribute
import com.wireguard.config.BadConfigException
import com.wireguard.config.Peer
import java.lang.ref.WeakReference
-import java.util.ArrayList
-import java.util.LinkedHashSet
class PeerProxy : BaseObservable, Parcelable {
private val dnsRoutes: MutableList<String?> = ArrayList()
@@ -240,24 +238,32 @@ class PeerProxy : BaseObservable, Parcelable {
peerProxy.setTotalPeers(sender.size)
}
- override fun onItemRangeChanged(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeChanged(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
// Do nothing.
}
- override fun onItemRangeInserted(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeInserted(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
onChanged(sender)
}
- override fun onItemRangeMoved(sender: ObservableList<PeerProxy?>,
- fromPosition: Int, toPosition: Int,
- itemCount: Int) {
+ override fun onItemRangeMoved(
+ sender: ObservableList<PeerProxy?>,
+ fromPosition: Int, toPosition: Int,
+ itemCount: Int
+ ) {
// Do nothing.
}
- override fun onItemRangeRemoved(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeRemoved(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
onChanged(sender)
}
}
@@ -276,12 +282,12 @@ class PeerProxy : BaseObservable, Parcelable {
@JvmField
val CREATOR: Parcelable.Creator<PeerProxy> = PeerProxyCreator()
private val IPV4_PUBLIC_NETWORKS = setOf(
- "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
- "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
- "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
- "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
- "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
- "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
+ "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
+ "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
+ "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
+ "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
+ "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
+ "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
)
private val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
diff --git a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
index bf42166d..8c822dcb 100644
--- a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
@@ -13,10 +13,12 @@ import com.wireguard.crypto.Key
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
class KeyInputFilter : InputFilter {
- override fun filter(source: CharSequence,
- sStart: Int, sEnd: Int,
- dest: Spanned,
- dStart: Int, dEnd: Int): CharSequence? {
+ override fun filter(
+ source: CharSequence,
+ sStart: Int, sEnd: Int,
+ dest: Spanned,
+ dStart: Int, dEnd: Int
+ ): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,8 +28,9 @@ class KeyInputFilter : InputFilter {
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
- dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
- dLength + (sIndex - sStart) < Key.Format.BASE64.length) {
+ dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
+ dLength + (sIndex - sStart) < Key.Format.BASE64.length
+ ) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
diff --git a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
index 511cd287..91c7da0c 100644
--- a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
@@ -11,10 +11,10 @@ import android.widget.RelativeLayout
import com.wireguard.android.R
class MultiselectableRelativeLayout @JvmOverloads constructor(
- context: Context? = null,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ context: Context? = null,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
private var multiselected = false
diff --git a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
index 7af514d9..e21ebaba 100644
--- a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
@@ -13,10 +13,12 @@ import com.wireguard.android.backend.Tunnel
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
class NameInputFilter : InputFilter {
- override fun filter(source: CharSequence,
- sStart: Int, sEnd: Int,
- dest: Spanned,
- dStart: Int, dEnd: Int): CharSequence? {
+ override fun filter(
+ source: CharSequence,
+ sStart: Int, sEnd: Int,
+ dest: Spanned,
+ dStart: Int, dEnd: Int
+ ): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,7 +28,8 @@ class NameInputFilter : InputFilter {
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
- dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
+ dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
+ ) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
index 8fcee9df..0e2eeff1 100644
--- a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
@@ -35,10 +35,10 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
val radiusX = scale(CORNER_RADIUS, width)
val radiusY = scale(CORNER_RADIUS, height)
updateRect(
- scale(LEFT, width),
- scale(TOP, height),
- scale(RIGHT, width),
- scale(TOP + mCurrentSlashLength, height)
+ scale(LEFT, width),
+ scale(TOP, height),
+ scale(RIGHT, width),
+ scale(TOP + mCurrentSlashLength, height)
)
mPath.reset()
// Draw the slash vertically
diff --git a/ui/src/main/res/drawable/list_item_background.xml b/ui/src/main/res/drawable/list_item_background.xml
index 3a77b524..16714e7b 100644
--- a/ui/src/main/res/drawable/list_item_background.xml
+++ b/ui/src/main/res/drawable/list_item_background.xml
@@ -8,8 +8,7 @@
app:state_multiselected="true">
<color android:color="?attr/colorSurfaceVariant" />
</item>
- <item
- android:state_activated="true">
+ <item android:state_activated="true">
<color android:color="?attr/colorControlHighlight" />
</item>
</selector>
diff --git a/ui/src/main/res/layout/config_naming_dialog_fragment.xml b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
index 88deb976..63d3141d 100644
--- a/ui/src/main/res/layout/config_naming_dialog_fragment.xml
+++ b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
+
<import type="com.wireguard.android.widget.NameInputFilter" />
</data>
@@ -24,7 +25,8 @@
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}">
- <requestFocus/>
+
+ <requestFocus />
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
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..425b364d 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.addresses}"
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 &lt; 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 &lt; 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() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; 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() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; 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/layout/tunnel_list_fragment.xml b/ui/src/main/res/layout/tunnel_list_fragment.xml
index 8fc5d523..2ee2ff38 100644
--- a/ui/src/main/res/layout/tunnel_list_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_list_fragment.xml
@@ -60,11 +60,11 @@
android:src="@mipmap/ic_launcher" />
<TextView
- android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
- android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
+ android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
+ android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
android:text="@string/tunnel_list_placeholder"
android:textSize="20sp" />
</LinearLayout>
diff --git a/ui/src/main/res/resources.properties b/ui/src/main/res/resources.properties
new file mode 100644
index 00000000..467b3efe
--- /dev/null
+++ b/ui/src/main/res/resources.properties
@@ -0,0 +1 @@
+unqualifiedResLocale=en-US
diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml
index 696fde62..b9363e33 100644
--- a/ui/src/main/res/values-de/strings.xml
+++ b/ui/src/main/res/values-de/strings.xml
@@ -119,6 +119,7 @@
<string name="error_down">Fehler beim Abschalten des Tunnels: %s</string>
<string name="error_fetching_apps">Fehler beim Abrufen der App-Liste: %s</string>
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
+ <string name="error_prepare">Fehler beim Vorbereiten des Tunnels: %s</string>
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
<string name="exclude_private_ips">Private IPs ausschließen</string>
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
@@ -138,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64-Schlüssel müssen 44 Zeichen enthalten (32 Bytes)</string>
<string name="key_length_explanation_binary">: WireGuard-Schlüssel müssen 32 Bytes groß sein</string>
<string name="key_length_explanation_hex">: WireGuard Hex-Schlüssel müssen 64 Zeichen (32 Bytes) groß sein</string>
+ <string name="latest_handshake">Letzter Handshake</string>
+ <string name="latest_handshake_ago">vor %s</string>
<string name="listen_port">Eingangs-Port</string>
<string name="log_export_error">Konnte Protokoll nicht exportieren: %s</string>
<string name="log_export_subject">WireGuard Android Protokolldatei</string>
@@ -216,6 +219,7 @@
<string name="tunnel_create_success">Tunnel „%s “ erfolgreich erstellt</string>
<string name="tunnel_error_already_exists">Tunnel „%s“ existiert bereits</string>
<string name="tunnel_error_invalid_name">Ungültiger Name</string>
+ <string name="tunnel_list_placeholder">Füge einen Tunnel mit der Schaltfläche unten hinzu</string>
<string name="tunnel_name">Tunnelname</string>
<string name="tunnel_on_error">Tunnel kann nicht eingeschaltet werden (wgTurnOn gab %d zurück)</string>
<string name="tunnel_dns_failure">DNS-Hostname kann nicht aufgelöst werden: „%s“</string>
@@ -224,6 +228,13 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernelmodul</string>
<string name="unknown_error">Unbekannter Fehler</string>
+ <string name="updater_avalable">Ein Anwendungsupdate ist verfügbar. Bitte jetzt aktualisieren.</string>
+ <string name="updater_action">Download &amp; Update</string>
+ <string name="updater_rechecking">Update-Metadaten abrufen…</string>
+ <string name="updater_download_progress">Update wird heruntergeladen: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Update wird heruntergeladen: %s</string>
+ <string name="updater_installing">Installiere Update…</string>
+ <string name="updater_failure">Fehler beim Aktualisieren: %s. Versuche es in Kürze erneut…</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Überprüfe %s Backend-Version</string>
<string name="version_summary_unknown">Unbekannte %s Version</string>
diff --git a/ui/src/main/res/values-et-rEE/strings.xml b/ui/src/main/res/values-et-rEE/strings.xml
index 99d1f7bc..692a0b8a 100644
--- a/ui/src/main/res/values-et-rEE/strings.xml
+++ b/ui/src/main/res/values-et-rEE/strings.xml
@@ -229,6 +229,13 @@ Aitäh veelkord sinu panuse eest.</string>
<string name="type_name_go_userspace">Go kasutajamaa</string>
<string name="type_name_kernel_module">Tuumamoodul</string>
<string name="unknown_error">Tundmatu viga</string>
+ <string name="updater_avalable">Rakenduse uuendus on saadaval. Palun uuenda nüüd.</string>
+ <string name="updater_action">Laadi alla ja uuenda</string>
+ <string name="updater_rechecking">Uuenduse andmete laadimine…</string>
+ <string name="updater_download_progress">Uuenduse allalaadimine: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Uuenduse allalaadimine: %s</string>
+ <string name="updater_installing">Uuenduse paigaldamine…</string>
+ <string name="updater_failure">Uuendamine ebaõnnestus: %s. Uus katse hetke pärast…</string>
<string name="version_summary">%1$s taustsüsteem %2$s</string>
<string name="version_summary_checking">Kontrollin %s taustsüsteemi versiooni</string>
<string name="version_summary_unknown">Tundmatu %s versioon</string>
diff --git a/ui/src/main/res/values-night/themes.xml b/ui/src/main/res/values-night/themes.xml
index 9187e48a..e074cb92 100644
--- a/ui/src/main/res/values-night/themes.xml
+++ b/ui/src/main/res/values-night/themes.xml
@@ -1,5 +1,5 @@
-
<resources>
+
<style name="WireGuardTheme" parent="Theme.Material3.Dark">
<item name="colorPrimary">@color/md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index fd747768..44d75415 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -254,6 +254,13 @@
<string name="type_name_go_userspace">Пользовательское пространство Go</string>
<string name="type_name_kernel_module">Модуль ядра</string>
<string name="unknown_error">Неизвестная ошибка</string>
+ <string name="updater_avalable">Доступно обновление приложения. Пожалуйста, обновите.</string>
+ <string name="updater_action">Загрузить и установить</string>
+ <string name="updater_rechecking">Получение метаданных обновления…</string>
+ <string name="updater_download_progress">Загрузка обновления: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Загрузка обновления: %s</string>
+ <string name="updater_installing">Установка обновления…</string>
+ <string name="updater_failure">Ошибка обновления: %s. Повторите попытку…</string>
<string name="version_summary">%1$s бэкенд %2$s</string>
<string name="version_summary_checking">Проверка версии бэкэнда %s</string>
<string name="version_summary_unknown">Неизвестная версия %s</string>
diff --git a/ui/src/main/res/values-v23/styles.xml b/ui/src/main/res/values-v23/styles.xml
index f6c74dd4..13feb8c3 100644
--- a/ui/src/main/res/values-v23/styles.xml
+++ b/ui/src/main/res/values-v23/styles.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
<style name="AppTheme" parent="AppThemeBase">
<item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
diff --git a/ui/src/main/res/values-v27/styles.xml b/ui/src/main/res/values-v27/styles.xml
index 752801f9..f94cadb1 100644
--- a/ui/src/main/res/values-v27/styles.xml
+++ b/ui/src/main/res/values-v27/styles.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
<style name="AppTheme" parent="AppThemeBase">
<item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml
index 4b30afca..19a9756d 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -215,6 +215,13 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernel module</string>
<string name="unknown_error">未知错误</string>
+ <string name="updater_avalable">有可用的应用程序更新。请立即更新。</string>
+ <string name="updater_action">下载 &amp; 更新</string>
+ <string name="updater_rechecking">正获取更新元数据…</string>
+ <string name="updater_download_progress">正在下载更新: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">正在下载更新: %s</string>
+ <string name="updater_installing">安装更新中...</string>
+ <string name="updater_failure">更新失败: %s。不久后将重试…</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">正在检查 %s backend 版本</string>
<string name="version_summary_unknown">未知的 %s 版本</string>
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index c9e230db..ad204ba5 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -70,6 +70,7 @@
<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>
+ <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string>
<string name="bad_config_reason_invalid_key">Invalid key</string>
<string name="bad_config_reason_invalid_number">Invalid number</string>
<string name="bad_config_reason_invalid_value">Invalid value</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>
@@ -185,6 +191,10 @@
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Add tile to quick settings panel</string>
+ <string name="quick_settings_tile_add_summary">The shortcut tile toggles the most recent tunnel</string>
+ <string name="quick_settings_tile_add_failure">Unable to add shortcut tile: error %d</string>
+ <string name="quick_settings_tile_action">Toggle tunnel</string>
<string name="restore_on_boot_summary_off">Will not bring up enabled tunnels at boot</string>
<string name="restore_on_boot_summary_on">Will bring up enabled tunnels at boot</string>
<string name="restore_on_boot_title">Restore on boot</string>
@@ -236,6 +246,8 @@
<string name="updater_download_progress_nototal">Downloading update: %s</string>
<string name="updater_installing">Installing update…</string>
<string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
+ <string name="updater_corrupt">This application is corrupt. Please re-download it.</string>
+ <string name="updater_corrupt_navigate">Open Website</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Checking %s backend version</string>
<string name="version_summary_unknown">Unknown %s version</string>
diff --git a/ui/src/main/res/values/themes.xml b/ui/src/main/res/values/themes.xml
index e8d36cdd..0153d346 100644
--- a/ui/src/main/res/values/themes.xml
+++ b/ui/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
-
<resources>
+
<style name="WireGuardTheme" parent="Theme.Material3.Light">
<item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml
index aa89f27c..a8b66df7 100644
--- a/ui/src/main/res/xml/preferences.xml
+++ b/ui/src/main/res/xml/preferences.xml
@@ -12,6 +12,7 @@
android:summaryOn="@string/restore_on_boot_summary_on"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" />
+ <com.wireguard.android.preference.QuickTilePreference android:key="quick_tile" />
<Preference
android:key="log_viewer"
android:singleLineTitle="false"
@@ -40,6 +41,5 @@
android:summaryOff="@string/allow_remote_control_intents_summary_off"
android:summaryOn="@string/allow_remote_control_intents_summary_on"
android:title="@string/allow_remote_control_intents_title" />
- <com.wireguard.android.preference.DonatePreference
- android:singleLineTitle="false" />
+ <com.wireguard.android.preference.DonatePreference android:singleLineTitle="false" />
</androidx.preference.PreferenceScreen>