diff options
111 files changed, 4965 insertions, 634 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/build.gradle.kts b/build.gradle.kts index acd4e797..fbcca451 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { tasks { wrapper { - gradleVersion = "8.1.1" - distributionSha256Sum = "e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f" + gradleVersion = "8.3" + distributionSha256Sum = "591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225" } } diff --git a/gradle.properties b/gradle.properties index 6e4f2a99..3e22e0f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -wireguardVersionCode=502 -wireguardVersionName=1.0.20230504 +wireguardVersionCode=510 +wireguardVersionName=1.0.20231018 wireguardPackageName=com.wireguard.android wireguardApplicationID=eu.m7n.wireguard.android diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7858b7c..5471d84a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,29 +1,41 @@ [versions] -agp = "8.0.1" -kotlin = "1.8.21" +agp = "8.2.0-beta06" +grgit = "5.2.0" +grpc = "1.59.0" +kotlin = "1.9.0" +protobuf = "0.9.4" +protoc = "3.24.4" +protocgengrpc = '1.59.0' [libraries] -androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.1" -androidx-annotation = "androidx.annotation:annotation:1.6.0" +androidx-activity-ktx = "androidx.activity:activity-ktx:1.8.0" +androidx-annotation = "androidx.annotation:annotation:1.7.0" androidx-appcompat = "androidx.appcompat:appcompat:1.6.1" androidx-biometric = "androidx.biometric:biometric:1.1.0" -androidx-collection = "androidx.collection:collection:1.2.0" +androidx-collection = "androidx.collection:collection:1.3.0" androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0" -androidx-core-ktx = "androidx.core:core-ktx:1.10.0" +androidx-core-ktx = "androidx.core:core-ktx:1.12.0" androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0" -androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.5.7" -androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" -androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0" +androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.6.1" +androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" +androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.1" desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3" -google-material = "com.google.android.material:material:1.8.0" +google-material = "com.google.android.material:material:1.10.0" +grpc-android = "io.grpc:grpc-android:1.59.0" +grpc-okhttp = "io.grpc:grpc-okhttp:1.59.0" +grpc-protobuf-lite = "io.grpc:grpc-protobuf-lite:1.59.0" +grpc-stub = "io.grpc:grpc-stub:1.59.0" +javax-annotation-api = "javax.annotation:javax.annotation-api:1.3.2" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" -kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.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] +ajoberstar-grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } +google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex c1962a79..7f93135c 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c3425d4..864d6c47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. 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..44a10696 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,11 +8,12 @@ val pkg: String = providers.gradleProperty("wireguardPackageName").get() plugins { alias(libs.plugins.android.library) `maven-publish` + alias(libs.plugins.google.protobuf) signing } android { - compileSdk = 33 + compileSdk = 34 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -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..41060ddf --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.backend; + +import com.wireguard.config.InetNetwork; +import com.wireguard.util.NonNullForAll; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Class representing DHCP info for a {@link Tunnel} instance. + */ +@NonNullForAll +public class Dhcp { + private Set<Lease> leases = new LinkedHashSet<>(); + + public class Lease { + private InetNetwork address; + private Duration validLt; + private Duration preferredLt; + private Instant validTs; + private Instant preferredTs; + + public Lease(InetNetwork address, Duration validLt, Duration preferredLt) { + this.address = address; + this.validLt = validLt; + this.preferredLt = preferredLt; + + Instant now = Instant.now(); + this.validTs = now.plus(validLt); + this.preferredTs = now.plus(preferredLt); + } + + public final InetNetwork getAddress() { + return this.address; + } + + public String toString() { + ZoneId zone = ZoneId.systemDefault(); + LocalTime validLocal = validTs.atZone(zone).toLocalTime().withNano(0); + LocalTime preferredLocal = preferredTs.atZone(zone).toLocalTime().withNano(0); + // TODO add date when needed + return address.toString() + " (valid " + validLocal + ", preferred " + preferredLocal + ")"; + } + } + + public void addLease(InetNetwork address, Duration valid, Duration preferred) { + this.leases.add(new Lease(address, valid, preferred)); + } + + public final Set<Lease> getLeases() { + return leases; + } + + public String toString() { + return "DHCP"; + } +} diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java index 429cb1f1..2af43fb2 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -5,33 +5,99 @@ package com.wireguard.android.backend; +import android.app.AlarmManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.LocalSocketAddress; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.ProxyInfo; +import android.net.TrafficStats; +import android.net.Uri; import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.SystemClock; import android.system.OsConstants; import android.util.Log; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Empty; + import com.wireguard.android.backend.BackendException.Reason; import com.wireguard.android.backend.Tunnel.State; +import com.wireguard.android.backend.gen.CapabilitiesChangedRequest; +import com.wireguard.android.backend.gen.CapabilitiesChangedResponse; +import com.wireguard.android.backend.gen.DhcpRequest; +import com.wireguard.android.backend.gen.DhcpResponse; +import com.wireguard.android.backend.gen.GetConnectionOwnerUidResponse; +import com.wireguard.android.backend.gen.IpcSetRequest; +import com.wireguard.android.backend.gen.IpcSetResponse; +import com.wireguard.android.backend.gen.Lease; +import com.wireguard.android.backend.gen.LibwgGrpc; +import com.wireguard.android.backend.gen.ReverseRequest; +import com.wireguard.android.backend.gen.ReverseResponse; +import com.wireguard.android.backend.gen.StartHttpProxyRequest; +import com.wireguard.android.backend.gen.StartHttpProxyResponse; +import com.wireguard.android.backend.gen.StopHttpProxyRequest; +import com.wireguard.android.backend.gen.StopHttpProxyResponse; +import com.wireguard.android.backend.gen.TunnelHandle; +import com.wireguard.android.backend.gen.VersionRequest; +import com.wireguard.android.backend.gen.VersionResponse; import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; +import com.wireguard.config.HttpProxy; import com.wireguard.config.InetEndpoint; import com.wireguard.config.InetNetwork; import com.wireguard.config.Peer; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; - +import com.wireguard.util.Resolver; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.android.UdsChannelBuilder; +import io.grpc.okhttp.OkHttpChannelBuilder; +import io.grpc.stub.StreamObserver; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.net.URL; +import java.time.Duration; import java.time.Instant; +import java.nio.ByteOrder; import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.SocketFactory; import androidx.annotation.Nullable; import androidx.collection.ArraySet; @@ -44,12 +110,25 @@ import androidx.collection.ArraySet; public final class GoBackend implements Backend { private static final int DNS_RESOLUTION_RETRIES = 10; private static final String TAG = "WireGuard/GoBackend"; + private static final int STATS_TAG = 2; + private static final int MSG_DHCP_EXPIRE = 1; + private static final int MSG_CAPABILITIES_CHANGED = 2; @Nullable private static AlwaysOnCallback alwaysOnCallback; private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>(); private final Context context; @Nullable private Config currentConfig; @Nullable private Tunnel currentTunnel; private int currentTunnelHandle = -1; + private ConnectivityManager connectivityManager; + private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback(); + private ConnectivityManager.NetworkCallback vpnNetworkCallback; + @Nullable private Network activeNetwork; + private ManagedChannel channel; + private boolean obtainDhcpLease = false; + @Nullable private Bgp bgp; + private HandlerThread thread; + private Handler handler; + private NetworkCapabilities activeNetworkCapabilities; /** * Public constructor for GoBackend. @@ -59,6 +138,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 +161,18 @@ public final class GoBackend implements Backend { private static native int wgGetSocketV6(int handle); + private static native int wgSetConfig(int handle, String settings); + + private static native void wgSetFd(int handle, int tunFd); + private static native void wgTurnOff(int handle); private static native int wgTurnOn(String ifName, int tunFd, String settings); private static native String wgVersion(); + private static native int wgStartGrpc(String sockName); + /** * Method to get the names of running tunnels. * @@ -185,7 +275,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 +317,306 @@ 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 void capabilitiesChanged(NetworkCapabilities capabilities) { + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + CapabilitiesChangedRequest.Builder reqBuilder = CapabilitiesChangedRequest.newBuilder(); + + if (capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { + reqBuilder.addCapabilities(CapabilitiesChangedRequest.Capability.NOT_METERED); + } + + CapabilitiesChangedResponse resp = stub.capabilitiesChanged(reqBuilder.build()); + Log.i(TAG, "Capabilities change: " + resp.getError().getMessage()); + } + + private int startHttpProxy(String pacFile) { + LibwgGrpc.LibwgStub asyncStub = LibwgGrpc.newStub(channel); + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + StartHttpProxyRequest.Builder reqBuilder = StartHttpProxyRequest.newBuilder(); + if (pacFile != null && pacFile != "") { + reqBuilder.setPacFileContent(pacFile); + } + + Thread streamer = new Thread(new Runnable() { + public void run() { + try { + Log.i(TAG, "Before streamReverse"); + streamReverse(asyncStub); + Log.i(TAG, "After streamReverse"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } - service.setOwner(this); + }); + + StartHttpProxyRequest req = reqBuilder.build(); + StartHttpProxyResponse resp = stub.startHttpProxy(req); + Log.i(TAG, "Start http proxy listen_port:" + resp.getListenPort() + ", error:" + resp.getError().getMessage()); + capabilitiesChanged(activeNetworkCapabilities); + streamer.start(); + return resp.getListenPort(); + } - if (currentTunnelHandle != -1) { - Log.w(TAG, "Tunnel already up"); - return; + private void stopHttpProxy() { + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + StopHttpProxyRequest req = StopHttpProxyRequest.newBuilder().build(); + StopHttpProxyResponse resp = stub.stopHttpProxy(req); + Log.i(TAG, "Stop http proxy: " + resp.getError().getMessage()); + } + + private static InetSocketAddress toInetSocketAddress(com.wireguard.android.backend.gen.InetSocketAddress sockAddr) { + try { + return new InetSocketAddress(InetAddress.getByAddress(sockAddr.getAddress().getAddress().toByteArray()), sockAddr.getPort()); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + private void Dhcp(VpnService service) throws Exception{ + if (currentConfig == null || currentTunnel == null || currentTunnelHandle < 0) { + return; + } + + obtainDhcpLease = false; + + // Heuristics: Use first ULA address as client address + com.wireguard.android.backend.gen.InetAddress source = null; + + for (final InetNetwork net : currentConfig.getInterface().getAddresses()) { + InetAddress addr = net.getAddress(); + if (addr instanceof Inet6Address) { + if (Resolver.isULA((Inet6Address)addr)) { + source = com.wireguard.android.backend.gen.InetAddress.newBuilder().setAddress(ByteString.copyFrom(addr.getAddress())).build(); + } } + } + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + DhcpRequest.Builder requestBuilder = DhcpRequest.newBuilder(); + if (source != null) { + requestBuilder.setSource(source); + } + DhcpRequest request = requestBuilder.build(); + DhcpResponse resp = stub.dhcp(request); + Log.i(TAG, "Dhcp: " + resp.getError().getMessage()); - dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) { - // Pre-resolve IPs so they're cached when building the userspace string - for (final Peer peer : config.getPeers()) { - final InetEndpoint ep = peer.getEndpoint().orElse(null); - if (ep == null) - continue; - if (ep.getResolved().orElse(null) == null) { - if (i < DNS_RESOLUTION_RETRIES - 1) { - Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again"); - Thread.sleep(1000); - continue dnsRetry; - } else - throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost()); - } + Dhcp dhcp = new Dhcp(); + long delayMillis = 1000 * 3600 * 12; // Max renew 12h + + if (resp.getLeasesList() != null) { + for (final Lease lease: resp.getLeasesList()) { + try { + InetAddress addr = InetAddress.getByAddress(lease.getAddress().getAddress().toByteArray()); + Log.i(TAG, "Lease: " + addr + " " + lease.getValidLifetime().getSeconds() + " " + lease.getPreferredLifetime().getSeconds()); + dhcp.addLease(new InetNetwork(addr, 128), + Duration.ofSeconds(lease.getValidLifetime().getSeconds(), lease.getValidLifetime().getNanos()), + Duration.ofSeconds(lease.getPreferredLifetime().getSeconds(), lease.getPreferredLifetime().getNanos())); + long leaseDelayMillis = lease.getValidLifetime().getSeconds() * 1000 + lease.getValidLifetime().getNanos() / 1000; + delayMillis = Math.min(delayMillis, leaseDelayMillis); + } catch (UnknownHostException ex) { + // Ignore } - break; } + } - // Build config - final String goConfig = config.toWgUserspaceString(); + // Replace the vpn tunnel + final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getLeases()); - // Create the vpn tunnel with android API + Log.i(TAG, "Builder: " + builder); + + try (final ParcelFileDescriptor tun = builder.establish()) { + if (tun == null) + throw new BackendException(Reason.TUN_CREATION_ERROR); + Log.d(TAG, "Go backend " + wgVersion()); + // SetFd + wgSetFd(currentTunnelHandle, tun.detachFd()); + } + if (currentTunnelHandle < 0) + throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle); + + service.protect(wgGetSocketV4(currentTunnelHandle)); + service.protect(wgGetSocketV6(currentTunnelHandle)); + Log.i(TAG, "Dhcp done"); + + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + am.cancel(alarmListener); + am.setWindow(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis * 3 / 4, delayMillis / 4, null, alarmListener, handler); + + if (bgp != null) { + bgp = new Bgp(this, channel, currentTunnel, currentTunnelHandle); + bgp.startServer(); + } + + currentTunnel.onDhcpChange(dhcp); + } + + private int getConnectionOwnerUid(int protocol, InetSocketAddress local, InetSocketAddress remote) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + return connectivityManager.getConnectionOwnerUid(protocol, local, remote); + else + return Process.INVALID_UID; + } + + private void streamReverse(LibwgGrpc.LibwgStub asyncStub) throws InterruptedException { + Log.i(TAG, "In streamReverse"); + final CountDownLatch finishLatch = new CountDownLatch(1); + final AtomicReference<StreamObserver<ReverseRequest>> atomicRequestObserver = new AtomicReference<StreamObserver<ReverseRequest>>(); + // Throwable failed = null; + + StreamObserver<ReverseResponse> responseObserver = new StreamObserver<ReverseResponse>() { + @Override + public void onNext(ReverseResponse resp) { + try { + String pkg = ""; + int uid = getConnectionOwnerUid(resp.getUid().getProtocol(), toInetSocketAddress(resp.getUid().getLocal()), toInetSocketAddress(resp.getUid().getRemote())); + if (uid != Process.INVALID_UID) { + PackageManager pm = context.getPackageManager(); + pkg = pm.getNameForUid(uid); + String[] pkgs = pm.getPackagesForUid(uid); + Log.i(TAG, "reverse onNext uid:" + uid + " package:" + pkg); + if (pkgs != null) { + for (int i=0; i < pkgs.length; i++) { + Log.i(TAG, "getPackagesForUid() = " + pkgs[i]); + } + } + } else { + Log.i(TAG, "Connection not found"); + } + + ReverseRequest req = ReverseRequest.newBuilder() + .setUid(GetConnectionOwnerUidResponse.newBuilder() + .setUid(uid) + .setPackage(pkg != null ? pkg: "") + .build()) + .build(); + + io.grpc.Context.current().fork().run(new Runnable() { + public void run() { + atomicRequestObserver.get().onNext(req); + } + }); + } catch (RuntimeException ex) { + Log.i(TAG, "onNext " + ex); + throw ex; + } + } + + @Override + public void onError(Throwable t) { + // failed = t; + Log.i(TAG, "streamReverse error: " + t); + finishLatch.countDown(); + } + + @Override + public void onCompleted() { + Log.i(TAG, "streamReverse completed"); + finishLatch.countDown(); + } + }; + StreamObserver<ReverseRequest> requestObserver = asyncStub.reverse(responseObserver); + atomicRequestObserver.set(requestObserver); + + // Mark the end of requests + //requestObserver.onCompleted(); + + //requestObserver.onNext(ReverseRequest.getDefaultInstance()); + + Log.i(TAG, "Waiting streamReverse"); + // Receiving happens asynchronously + finishLatch.await(); + + // if (failed != null) { + // throw new RuntimeException(failed); + // } + Log.i(TAG, "Exit streamReverse"); + } + + private VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set<Dhcp.Lease> leases) throws PackageManager.NameNotFoundException { + Log.i(TAG, "Builder 1"); final VpnService.Builder builder = service.getBuilder(); - builder.setSession(tunnel.getName()); + Log.i(TAG, "Builder 2"); + builder.setSession(name); + Log.i(TAG, "Builder 3"); for (final String excludedApplication : config.getInterface().getExcludedApplications()) builder.addDisallowedApplication(excludedApplication); + Log.i(TAG, "Builder 4"); for (final String includedApplication : config.getInterface().getIncludedApplications()) builder.addAllowedApplication(includedApplication); + Log.i(TAG, "Builder 5"); + if (leases != null) { + for (final Dhcp.Lease lease: leases) { + InetNetwork addr = lease.getAddress(); + builder.addAddress(addr.getAddress(), addr.getMask()); + } + } + + Log.i(TAG, "Builder 6"); for (final InetNetwork addr : config.getInterface().getAddresses()) builder.addAddress(addr.getAddress(), addr.getMask()); + Log.i(TAG, "Builder 7"); for (final InetAddress addr : config.getInterface().getDnsServers()) builder.addDnsServer(addr.getHostAddress()); + Log.i(TAG, "Builder 8"); for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains()) builder.addSearchDomain(dnsSearchDomain); + Log.i(TAG, "Builder 9"); boolean sawDefaultRoute = false; for (final Peer peer : config.getPeers()) { for (final InetNetwork addr : peer.getAllowedIps()) { @@ -305,20 +626,115 @@ public final class GoBackend implements Backend { } } + Log.i(TAG, "Builder 10"); // "Kill-switch" semantics if (!(sawDefaultRoute && config.getPeers().size() == 1)) { builder.allowFamily(OsConstants.AF_INET); builder.allowFamily(OsConstants.AF_INET6); } + Log.i(TAG, "Builder 11"); builder.setMtu(config.getInterface().getMtu().orElse(1280)); + Log.i(TAG, "Builder 12"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false); + Log.i(TAG, "Builder 13"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) service.setUnderlyingNetworks(null); + Log.i(TAG, "Builder 14"); + Optional<HttpProxy> proxy = config.getInterface().getHttpProxy(); + if (proxy.isPresent()) { + ProxyInfo pi = proxy.get().getProxyInfo(); + Uri pacFileUrl = pi.getPacFileUrl(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (pacFileUrl != null && pacFileUrl != Uri.EMPTY) { + String pacFile = downloadPacFile(activeNetwork, pacFileUrl); + int listenPort = startHttpProxy(pacFile); + ProxyInfo localPi = ProxyInfo.buildDirectProxy("localhost", listenPort); + builder.setHttpProxy(localPi); + } else { + builder.setHttpProxy(pi); + } + } + } + + Log.i(TAG, "Builder 15"); builder.setBlocking(true); + return builder; + } + + private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) + throws Exception { + Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state); + + if (state == State.UP) { + if (config == null) + throw new BackendException(Reason.TUNNEL_MISSING_CONFIG); + + if (VpnService.prepare(context) != null) + throw new BackendException(Reason.VPN_NOT_AUTHORIZED); + + final VpnService service; + if (!vpnService.isDone()) { + Log.d(TAG, "Requesting to start VpnService"); + context.startService(new Intent(context, VpnService.class)); + } + + try { + service = vpnService.get(2, TimeUnit.SECONDS); + } catch (final TimeoutException e) { + final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN); + be.initCause(e); + throw be; + } + service.setOwner(this); + + if (currentTunnelHandle != -1) { + Log.w(TAG, "Tunnel already up"); + return; + } + + + activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + Log.w(TAG, "Null activeNetwork"); + activeNetwork = null; + } + else if (!connectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + Log.w(TAG, "VPN network is active, null activeNetwork"); + activeNetwork = null; + } + final Resolver resolver = new Resolver(activeNetwork, connectivityManager.getLinkProperties(activeNetwork)); + dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) { + // Pre-resolve IPs so they're cached when building the userspace string + for (final Peer peer : config.getPeers()) { + final InetEndpoint ep = peer.getEndpoint().orElse(null); + if (ep == null) + continue; + // FIXME + tunnel.onEndpointChange(peer.getPublicKey(), ep); + Log.i(TAG, "onEndpointChange " + peer.getPublicKey() + ", " + ep); + if (ep.getResolved(resolver, true).orElse(null) == null) { + if (i < DNS_RESOLUTION_RETRIES - 1) { + Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again"); + Thread.sleep(1000); + continue dnsRetry; + } else + throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost()); + } + } + break; + } + + // Build config + final String goConfig = config.toWgUserspaceString(resolver); + + // Create the vpn tunnel with android API + final VpnService.Builder builder = getBuilder(tunnel.getName(), config, service, null); + try (final ParcelFileDescriptor tun = builder.establish()) { if (tun == null) throw new BackendException(Reason.TUN_CREATION_ERROR); @@ -333,6 +749,46 @@ public final class GoBackend implements Backend { service.protect(wgGetSocketV4(currentTunnelHandle)); service.protect(wgGetSocketV6(currentTunnelHandle)); + + obtainDhcpLease = true; + + thread = new HandlerThread("GoBackend HandlerThread"); + thread.start(); + handler = new Handler(thread.getLooper()) { + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DHCP_EXPIRE: + Log.w(TAG, "DHCP expire: " + ((activeNetwork != null) ? activeNetwork : "disabled")); + try { + if (activeNetwork != null) { + Log.w(TAG, "DHCP before"); + Dhcp(service); // Renew addresses + Log.w(TAG, "DHCP after"); + } else { + Log.w(TAG, "DHCP delay obtain lease"); + obtainDhcpLease = true; + } + } catch (Exception ex) { + Log.e(TAG, "DHCP failed: " + ex); + } + break; + case MSG_CAPABILITIES_CHANGED: + Log.w(TAG, "Msg: capabilities changed: " + activeNetworkCapabilities); + capabilitiesChanged(activeNetworkCapabilities); + break; + default: + Log.w(TAG, "Unknown message: " + msg.what); + break; + } + } + }; + + NetworkRequest req = new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build(); + connectivityManager.requestNetwork(req, myNetworkCallback); + + NetworkRequest vpnReq = new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_VPN).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build(); + vpnNetworkCallback = new VpnNetworkCallback(service); + connectivityManager.requestNetwork(vpnReq, vpnNetworkCallback); } else { if (currentTunnelHandle == -1) { Log.w(TAG, "Tunnel already down"); @@ -342,6 +798,24 @@ public final class GoBackend implements Backend { currentTunnel = null; currentTunnelHandle = -1; currentConfig = null; + + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + am.cancel(alarmListener); + if (thread != null) { + handler = null; + thread.quit(); + thread = null; + } + if (bgp != null) { + bgp.stopServer(); + bgp = null; + } + stopHttpProxy(); + if (vpnNetworkCallback != null) + connectivityManager.unregisterNetworkCallback(vpnNetworkCallback); + vpnNetworkCallback = null; + connectivityManager.unregisterNetworkCallback(myNetworkCallback); + activeNetwork = null; wgTurnOff(handleToClose); try { vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf(); @@ -407,10 +881,29 @@ public final class GoBackend implements Backend { @Override public void onDestroy() { if (owner != null) { + if (owner.bgp != null) { + owner.bgp.stopServer(); + owner.bgp = null; + } + AlarmManager am = (AlarmManager)owner.context.getSystemService(Context.ALARM_SERVICE); + am.cancel(owner.alarmListener); + if (owner.thread != null) { + owner.handler = null; + owner.thread.quit(); + owner.thread = null; + } + + owner.stopHttpProxy(); final Tunnel tunnel = owner.currentTunnel; if (tunnel != null) { - if (owner.currentTunnelHandle != -1) + if (owner.currentTunnelHandle != -1) { + if (owner.vpnNetworkCallback != null) + owner.connectivityManager.unregisterNetworkCallback(owner.vpnNetworkCallback); + owner.vpnNetworkCallback = null; + owner.connectivityManager.unregisterNetworkCallback(owner.myNetworkCallback); + owner.activeNetwork = null; wgTurnOff(owner.currentTunnelHandle); + } owner.currentTunnel = null; owner.currentTunnelHandle = -1; owner.currentConfig = null; @@ -436,4 +929,88 @@ public final class GoBackend implements Backend { this.owner = owner; } } + + private class VpnNetworkCallback extends ConnectivityManager.NetworkCallback { + private VpnService service; + public VpnNetworkCallback(VpnService service) { + this.service = service; + } + @Override + public void onAvailable(Network network) { + Log.w(TAG, "VPN onAvailable: " + network); + if (obtainDhcpLease && activeNetwork != null) { + Log.w(TAG, "Obtaindhcplease"); + try { + Log.w(TAG, "Before Dhcp"); + Dhcp(service); + Log.w(TAG, "After Dhcp"); + } catch (Exception ex) { + Log.e(TAG, "DHCP failed: " + ex); + } + } + } + } + + private class MyNetworkCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + activeNetwork = network; + Log.w(TAG, "onAvailable: " + activeNetwork); + } + + @Override + public void onLosing(Network network, int maxMsToLive) { + Log.w(TAG, "onLosing: " + network + " maxMsToLive: " + maxMsToLive); + // TODO release DHCP? + } + + @Override + public void onLost(Network network) { + Log.w(TAG, "onLost: " + network + " (" + activeNetwork + ")"); + if (network.equals(activeNetwork)) { + activeNetwork = null; + } + } + + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { + Log.w(TAG, "onLinkPropertiesChanged: " + network + " is default:" + (network.equals(activeNetwork))); + if (network.equals(activeNetwork) && currentConfig != null && currentTunnelHandle > -1) { + final Resolver resolver = new Resolver(network, linkProperties); + final String goConfig = currentConfig.toWgEndpointsUserspaceString(resolver); + Log.w(TAG, "is default network, config:" + goConfig); + + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + TunnelHandle tunnel = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build(); + IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(tunnel).setConfig(goConfig).build(); + IpcSetResponse resp = stub.ipcSet(request); + + for (final Peer peer : currentConfig.getPeers()) { + final InetEndpoint ep = peer.getEndpoint().orElse(null); + if (ep == null) + continue; + currentTunnel.onEndpointChange(peer.getPublicKey(), ep); + } + } + } + + @Override + public void onCapabilitiesChanged (Network network, NetworkCapabilities networkCapabilities) { + boolean hasCapNotMetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + Log.w(TAG, "onCapabilitiesChanged: " + network + " is not metered:" + (hasCapNotMetered)); + if (network.equals(activeNetwork) && handler != null) { + activeNetworkCapabilities = networkCapabilities; + handler.sendEmptyMessage(MSG_CAPABILITIES_CHANGED); + } + } + } + + private AlarmManager.OnAlarmListener alarmListener = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + if (handler != null) { + handler.sendEmptyMessage(MSG_DHCP_EXPIRE); + } + } + }; } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java index 9fc92c53..08b84949 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java @@ -21,31 +21,7 @@ import androidx.annotation.Nullable; */ @NonNullForAll public class Statistics { - - // TODO: switch to Java Record class once R8 supports desugaring those. - public final class PeerStats { - public final long rxBytes, txBytes, latestHandshakeEpochMillis; - - PeerStats(final long rxBytes, final long txBytes, final long latestHandshakeEpochMillis) { - this.rxBytes = rxBytes; - this.txBytes = txBytes; - this.latestHandshakeEpochMillis = latestHandshakeEpochMillis; - } - - @Override public boolean equals(final Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - final PeerStats stats = (PeerStats) o; - return rxBytes == stats.rxBytes && txBytes == stats.txBytes && latestHandshakeEpochMillis == stats.latestHandshakeEpochMillis; - } - - @Override public int hashCode() { - return Objects.hash(rxBytes, txBytes, latestHandshakeEpochMillis); - } - } - + public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { } private final Map<Key, PeerStats> stats = new HashMap<>(); private long lastTouched = SystemClock.elapsedRealtime(); @@ -85,7 +61,7 @@ public class Statistics { */ @Nullable public PeerStats peer(final Key peer) { - return this.stats.get(peer); + return stats.get(peer); } /** 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/android/util/RootShell.java b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java index e123733b..67bff568 100644 --- a/tunnel/src/main/java/com/wireguard/android/util/RootShell.java +++ b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java @@ -46,8 +46,8 @@ public class RootShell { final String packageName = context.getPackageName(); if (packageName.contains("'")) throw new RuntimeException("Impossibly invalid package name contains a single quote"); - preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE package_name='%s'\" >/dev/null 2>&1; id -u\n", - packageName, localBinaryDir, localTemporaryDir, packageName); + preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n", + packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid()); } private static boolean isExecutableInPath(final String name) { diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java index db022e14..0d41cc05 100644 --- a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java +++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java @@ -8,6 +8,8 @@ package com.wireguard.config; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; +import java.net.MalformedURLException; + import androidx.annotation.Nullable; @NonNullForAll @@ -44,6 +46,12 @@ public class BadConfigException extends Exception { } public BadConfigException(final Section section, final Location location, + @Nullable final CharSequence text, + final MalformedURLException cause) { + this(section, location, Reason.INVALID_VALUE, text, cause); + } + + public BadConfigException(final Section section, final Location location, final ParseException cause) { this(section, location, Reason.INVALID_VALUE, cause.getText(), cause); } @@ -73,6 +81,7 @@ public class BadConfigException extends Exception { ENDPOINT("Endpoint"), EXCLUDED_APPLICATIONS("ExcludedApplications"), INCLUDED_APPLICATIONS("IncludedApplications"), + HTTP_PROXY("HttpProxy"), LISTEN_PORT("ListenPort"), MTU("MTU"), PERSISTENT_KEEPALIVE("PersistentKeepalive"), diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java index ee9cebce..12ddb242 100644 --- a/tunnel/src/main/java/com/wireguard/config/Config.java +++ b/tunnel/src/main/java/com/wireguard/config/Config.java @@ -9,6 +9,7 @@ import com.wireguard.config.BadConfigException.Location; import com.wireguard.config.BadConfigException.Reason; import com.wireguard.config.BadConfigException.Section; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.io.BufferedReader; import java.io.IOException; @@ -173,12 +174,19 @@ public final class Config { * * @return the {@code Config} represented as a series of "key=value" lines */ - public String toWgUserspaceString() { + public String toWgUserspaceString(Resolver resolver) { final StringBuilder sb = new StringBuilder(); sb.append(interfaze.toWgUserspaceString()); sb.append("replace_peers=true\n"); for (final Peer peer : peers) - sb.append(peer.toWgUserspaceString()); + sb.append(peer.toWgUserspaceString(resolver)); + return sb.toString(); + } + + public String toWgEndpointsUserspaceString(Resolver resolver) { + final StringBuilder sb = new StringBuilder(); + for (final Peer peer : peers) + sb.append(peer.toWgEndpointsUserspaceString(resolver)); return sb.toString(); } diff --git a/tunnel/src/main/java/com/wireguard/config/HttpProxy.java b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java new file mode 100644 index 00000000..0a9d19f6 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2023 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.config; + +import com.wireguard.config.BadConfigException.Location; +import com.wireguard.config.BadConfigException.Section; +import com.wireguard.util.NonNullForAll; + +import java.net.MalformedURLException; +import java.net.URL; + +import android.net.ProxyInfo; +import android.net.Uri; + +@NonNullForAll +public final class HttpProxy { + public static final int DEFAULT_PROXY_PORT = 8080; + + private ProxyInfo pi; + + protected HttpProxy(ProxyInfo pi) { + this.pi = pi; + } + + public ProxyInfo getProxyInfo() { + return pi; + } + + public String getHost() { + return pi.getHost(); + } + + public Uri getPacFileUrl() { + return pi.getPacFileUrl(); + } + + public int getPort() { + return pi.getPort(); + } + + public static HttpProxy parse(final String httpProxy) throws BadConfigException { + try { + if (httpProxy.startsWith("pac:")) { + return new HttpProxy(ProxyInfo.buildPacProxy(Uri.parse(httpProxy.substring(4)))); + } else { + final String urlStr; + if (!httpProxy.contains("://")) { + urlStr = "http://" + httpProxy; + } else { + urlStr = httpProxy; + } + URL url = new URL(urlStr); + return new HttpProxy(ProxyInfo.buildDirectProxy(url.getHost(), url.getPort() <= 0 ? DEFAULT_PROXY_PORT : url.getPort())); + } + } catch (final MalformedURLException e) { + throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, httpProxy, e); + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (pi.getPacFileUrl() != null && pi.getPacFileUrl() != Uri.EMPTY) + sb.append("pac:").append(pi.getPacFileUrl()); + else { + sb.append("http://").append(pi.getHost()).append(':'); + if (pi.getPort() <= 0) + sb.append(DEFAULT_PROXY_PORT); + else + sb.append(pi.getPort()); + } + + return sb.toString(); + } +} diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java index d1db432b..f9db5779 100644 --- a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java +++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java @@ -6,6 +6,7 @@ package com.wireguard.config; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.net.Inet4Address; import java.net.InetAddress; @@ -64,6 +65,10 @@ public final class InetEndpoint { } } + public static InetEndpoint fromAddress(final InetAddress address, final int port) { + return new InetEndpoint(address.getHostAddress(), true, port); + } + @Override public boolean equals(final Object obj) { if (!(obj instanceof InetEndpoint)) @@ -80,6 +85,14 @@ public final class InetEndpoint { return port; } + public Optional<InetEndpoint> getResolved() { + if (isResolved) { + return Optional.of(this); + } else { + return Optional.ofNullable(resolved); + } + } + /** * Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS * to a numeric address. If the host is already numeric, the existing instance may be returned. @@ -87,24 +100,22 @@ public final class InetEndpoint { * * @return the resolved endpoint, or {@link Optional#empty()} */ - public Optional<InetEndpoint> getResolved() { - if (isResolved) + public Optional<InetEndpoint> getResolved(Resolver resolver) { + return getResolved(resolver, false); + } + + public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) { + if (!force && isResolved) return Optional.of(this); synchronized (lock) { //TODO(zx2c4): Implement a real timeout mechanism using DNS TTL - if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) { + if (force || Duration.between(lastResolution, Instant.now()).toMinutes() > 1) { try { - // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues. - final InetAddress[] candidates = InetAddress.getAllByName(host); - InetAddress address = candidates[0]; - for (final InetAddress candidate : candidates) { - if (candidate instanceof Inet4Address) { - address = candidate; - break; - } - } - resolved = new InetEndpoint(address.getHostAddress(), true, port); + InetAddress address = resolver.resolve(host); + InetEndpoint resolvedNow = new InetEndpoint(address.getHostAddress(), true, port); lastResolution = Instant.now(); + + resolved = resolvedNow; } catch (final UnknownHostException e) { resolved = null; } diff --git a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java index 4a918044..02ccd946 100644 --- a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java +++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java @@ -20,7 +20,7 @@ public final class InetNetwork { private final InetAddress address; private final int mask; - private InetNetwork(final InetAddress address, final int mask) { + public InetNetwork(final InetAddress address, final int mask) { this.address = address; this.mask = mask; } diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java index bebca2e5..e47e0be3 100644 --- a/tunnel/src/main/java/com/wireguard/config/Interface.java +++ b/tunnel/src/main/java/com/wireguard/config/Interface.java @@ -46,6 +46,7 @@ public final class Interface { private final KeyPair keyPair; private final Optional<Integer> listenPort; private final Optional<Integer> mtu; + private final Optional<HttpProxy> httpProxy; private Interface(final Builder builder) { // Defensively copy to ensure immutability even if the Builder is reused. @@ -57,6 +58,7 @@ public final class Interface { keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key"); listenPort = builder.listenPort; mtu = builder.mtu; + httpProxy = builder.httpProxy; } /** @@ -92,6 +94,9 @@ public final class Interface { case "mtu": builder.parseMtu(attribute.getValue()); break; + case "httpproxy": + builder.parseHttpProxy(attribute.getValue()); + break; case "privatekey": builder.parsePrivateKey(attribute.getValue()); break; @@ -115,7 +120,8 @@ public final class Interface { && includedApplications.equals(other.includedApplications) && keyPair.equals(other.keyPair) && listenPort.equals(other.listenPort) - && mtu.equals(other.mtu); + && mtu.equals(other.mtu) + && httpProxy.equals(other.httpProxy); } /** @@ -195,6 +201,15 @@ public final class Interface { return mtu; } + /** + * Returns the HTTP proxy used for the WireGuard interface. + * + * @return the HTTP proxy, or {@code Optional.empty()} if none is configured + */ + public Optional<HttpProxy> getHttpProxy() { + return httpProxy; + } + @Override public int hashCode() { int hash = 1; @@ -205,6 +220,7 @@ public final class Interface { hash = 31 * hash + keyPair.hashCode(); hash = 31 * hash + listenPort.hashCode(); hash = 31 * hash + mtu.hashCode(); + hash = 31 * hash + httpProxy.hashCode(); return hash; } @@ -244,6 +260,7 @@ public final class Interface { sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n'); listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n')); mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n')); + httpProxy.ifPresent(p -> sb.append("HttpProxy = ").append(p).append('\n')); sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n'); return sb.toString(); } @@ -279,6 +296,8 @@ public final class Interface { private Optional<Integer> listenPort = Optional.empty(); // Defaults to not present. private Optional<Integer> mtu = Optional.empty(); + // Defaults to not present. + private Optional<HttpProxy> httpProxy = Optional.empty(); public Builder addAddress(final InetNetwork address) { addresses.add(address); @@ -391,6 +410,10 @@ public final class Interface { } } + public Builder parseHttpProxy(final String httpProxy) throws BadConfigException { + return setHttpProxy(HttpProxy.parse(httpProxy)); + } + public Builder parsePrivateKey(final String privateKey) throws BadConfigException { try { return setKeyPair(new KeyPair(Key.fromBase64(privateKey))); @@ -419,5 +442,13 @@ public final class Interface { this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu); return this; } + + public Builder setHttpProxy(final HttpProxy httpProxy) throws BadConfigException { + if (httpProxy == null) + throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, + Reason.INVALID_VALUE, String.valueOf(httpProxy)); + this.httpProxy = httpProxy == null ? Optional.empty() : Optional.of(httpProxy); + return this; + } } } diff --git a/tunnel/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java index 8a0fd763..1d20a961 100644 --- a/tunnel/src/main/java/com/wireguard/config/Peer.java +++ b/tunnel/src/main/java/com/wireguard/config/Peer.java @@ -11,6 +11,7 @@ import com.wireguard.config.BadConfigException.Section; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; import com.wireguard.util.NonNullForAll; +import com.wireguard.util.Resolver; import java.util.Collection; import java.util.Collections; @@ -190,18 +191,26 @@ public final class Peer { * * @return the {@code Peer} represented as a series of "key=value" lines */ - public String toWgUserspaceString() { + public String toWgUserspaceString(Resolver resolver) { final StringBuilder sb = new StringBuilder(); // The order here is important: public_key signifies the beginning of a new peer. sb.append("public_key=").append(publicKey.toHex()).append('\n'); for (final InetNetwork allowedIp : allowedIps) sb.append("allowed_ip=").append(allowedIp).append('\n'); - endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n')); + endpoint.flatMap(ep -> ep.getResolved(resolver)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n')); persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n')); preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n')); return sb.toString(); } + public String toWgEndpointsUserspaceString(Resolver resolver) { + final StringBuilder sb = new StringBuilder(); + // The order here is important: public_key signifies the beginning of a new peer. + sb.append("public_key=").append(publicKey.toHex()).append('\n'); + endpoint.flatMap(ep -> ep.getResolved(resolver, true)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n')); + return sb.toString(); + } + @SuppressWarnings("UnusedReturnValue") public static final class Builder { // See wg(8) diff --git a/tunnel/src/main/java/com/wireguard/util/Resolver.java b/tunnel/src/main/java/com/wireguard/util/Resolver.java new file mode 100644 index 00000000..24f5ab88 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java @@ -0,0 +1,140 @@ +/* + * Copyright © 2023 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.util; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +import android.net.IpPrefix; +import android.net.LinkProperties; +import android.net.Network; +import android.net.TrafficStats; +import android.util.Log; + +import androidx.annotation.Nullable; + +@NonNullForAll +public class Resolver { + private static final String TAG = "WireGuard/Resolver"; + private static final int STATS_TAG = 3; // FIXME + @Nullable private final Network network; + @Nullable private final LinkProperties linkProps; + @Nullable private IpPrefix nat64Prefix; + + public Resolver(Network network, LinkProperties linkProps) { + this.network = network; + this.linkProps = linkProps; + if (linkProps != null) { + this.nat64Prefix = linkProps.getNat64Prefix(); + } + } + + public static boolean isULA(Inet6Address addr) { + byte[] raw = addr.getAddress(); + return ((raw[0] & 0xfe) == 0xfc); + } + + boolean isWithinNAT64Prefix(Inet6Address address) { + if (nat64Prefix == null) + return false; + + int prefixLength = nat64Prefix.getPrefixLength(); + byte[] rawAddr = address.getAddress(); + byte[] rawPrefix = nat64Prefix.getRawAddress(); + + for (int i=0; i < prefixLength/8; i++) { + if (rawAddr[i] != rawPrefix[i]) + return false; + } + + return true; + } + + boolean isPreferredIPv6(Inet6Address local, Inet6Address remote) { + if (linkProps == null) { + // Prefer IPv4 if there are not link properties that can + // be tested. + return false; + } + + // * Prefer IPv4 if local or remote address is ULA + // * Prefer IPv4 if remote IPv6 is within NAT64 prefix. + // * Otherwise prefer IPv6 + boolean isLocalULA = isULA(local); + boolean isRemoteULA = isULA(remote); + + if (isLocalULA || isRemoteULA) { + return false; + } + + if (isWithinNAT64Prefix(remote)) { + return false; + } + + return true; + } + + public InetAddress resolve(String host) throws UnknownHostException { + TrafficStats.setThreadStatsTag(STATS_TAG); + final InetAddress[] candidates = network != null ? network.getAllByName(host) : InetAddress.getAllByName(host); + InetAddress address = candidates[0]; + for (final InetAddress candidate : candidates) { + DatagramSocket sock; + + try { + sock = new DatagramSocket(); + TrafficStats.tagDatagramSocket(sock); + if (network != null) { + network.bindSocket(sock); + } + } catch (SocketException e) { + // Return first candidate as fallback + Log.w(TAG, "DatagramSocket failed, fallback to: \"" + address); + return address; + } catch (IOException e) { + // Return first candidate as fallback + Log.w(TAG, "BindSocket failed, fallback to: \"" + address); + return address; + } + + sock.connect(candidate, 51820); + + if (sock.getLocalAddress().isAnyLocalAddress()) { + // Connect didn't find a local address. + Log.w(TAG, "No local address"); + sock.close(); + continue; + } + + Log.w(TAG, "Local address: " + sock.getLocalAddress()); + + if (candidate instanceof Inet4Address) { + // Accept IPv4 as preferred address. + address = candidate; + sock.close(); + break; + } + + Inet6Address local = (Inet6Address)sock.getLocalAddress(); + InetSocketAddress remoteSockAddr = (InetSocketAddress)sock.getRemoteSocketAddress(); + Inet6Address remote = (Inet6Address)remoteSockAddr.getAddress(); + sock.close(); + + if (isPreferredIPv6(local, remote)) { + address = candidate; + break; + } + } + Log.w(TAG, "Resolved \"" + host + "\" to: " + address); + return address; + } +} diff --git a/tunnel/src/main/proto/libwg.proto b/tunnel/src/main/proto/libwg.proto new file mode 100644 index 00000000..bf471599 --- /dev/null +++ b/tunnel/src/main/proto/libwg.proto @@ -0,0 +1,139 @@ +syntax = "proto3"; + +import "google/protobuf/duration.proto"; + +option java_multiple_files = true; +option java_package = 'com.wireguard.android.backend.gen'; +option java_outer_classname = "LibwgProto"; +option java_generic_services = true; +option go_package = 'golang.zx2c4.com/wireguard/android/gen'; + +package api; + +service Libwg { + rpc StopGrpc(StopGrpcRequest) returns (StopGrpcResponse); + rpc Version(VersionRequest) returns (VersionResponse); + rpc StartHttpProxy(StartHttpProxyRequest) returns (StartHttpProxyResponse); + rpc StopHttpProxy(StopHttpProxyRequest) returns (StopHttpProxyResponse); + rpc Reverse(stream ReverseRequest) returns (stream ReverseResponse); + rpc IpcSet(IpcSetRequest) returns (IpcSetResponse); + rpc Dhcp(DhcpRequest) returns (DhcpResponse); + rpc CapabilitiesChanged(CapabilitiesChangedRequest) returns (CapabilitiesChangedResponse); +} + +message TunnelHandle { int32 handle = 1; } + +message Error { + enum Code { + NO_ERROR = 0; + UNSPECIFIED = 1; + INVALID_PROTOCOL_BUFFER = 2; + INVALID_RESPONSE = 3; + } + Code code = 1; + string message = 2; +} + +message InetAddress { + bytes address = 1; +} + +message InetSocketAddress { + InetAddress address = 1; + uint32 port = 2; +} + +message StopGrpcRequest { +} + +message StopGrpcResponse { +} + +message VersionRequest { +} + +message VersionResponse { + string version = 1; +} + +message StartHttpProxyRequest { + oneof pacFile { + string pacFileUrl = 1; + string pacFileContent = 2; + } +} + +message StartHttpProxyResponse { + uint32 listen_port = 1; + Error error = 2; +} + +message StopHttpProxyRequest { +} + +message StopHttpProxyResponse { + Error error = 1; +} + +message ReverseRequest { + oneof response { + GetConnectionOwnerUidResponse uid = 1; + } +} + +message ReverseResponse { + oneof request { + GetConnectionOwnerUidRequest uid = 1; + } +} + +message GetConnectionOwnerUidRequest { + // ConnectivityManager.getConnectionOwnerUid(int protocol, + // InetSocketAddress local, InetSocketAddress remote) + int32 protocol = 1; + InetSocketAddress local = 2; + InetSocketAddress remote = 3; +} + +message GetConnectionOwnerUidResponse { + int32 uid = 1; + string package = 2; // context.getPackageManager().getNameForUid() +} + +message IpcSetRequest { + TunnelHandle tunnel = 1; + string config = 2; +} + +message IpcSetResponse { + Error error = 1; +} + +message Lease { + InetAddress address = 1; + google.protobuf.Duration preferred_lifetime = 2; + google.protobuf.Duration valid_lifetime = 3; +} + +message DhcpRequest { + InetAddress relay = 1; + InetAddress source = 2; +} + +message DhcpResponse { + Error error = 1; + repeated Lease leases = 2; +} + +message CapabilitiesChangedRequest { + enum Capability { + NONE = 0; + NOT_METERED = 11; + }; + + repeated Capability capabilities = 1; +} + +message CapabilitiesChangedResponse { + Error error = 1; +} diff --git a/tunnel/tools/libwg-go/.gitignore b/tunnel/tools/libwg-go/.gitignore index d1638636..d69ee823 100644 --- a/tunnel/tools/libwg-go/.gitignore +++ b/tunnel/tools/libwg-go/.gitignore @@ -1 +1,2 @@ -build/
\ No newline at end of file +build/ +/gen/ diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile index f1936e1d..ed718258 100644 --- a/tunnel/tools/libwg-go/Makefile +++ b/tunnel/tools/libwg-go/Makefile @@ -19,13 +19,21 @@ 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_VERSION := 1.21.3 GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)) GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz -GO_HASH_darwin-amd64 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76269c3565 -GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95 -GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca +GO_HASH_darwin-amd64 := 27014fc69e301d7588a169ca239b3cc609f0aa1abf38528bf0d20d3b259211eb +GO_HASH_darwin-arm64 := 65302a7a9f7a4834932b3a7a14cb8be51beddda757b567a2f9e0cbd0d7b5a6ab +GO_HASH_linux-amd64 := 1241381b2843fae5a9707eec1f8fb2ef94d827990582c7c7c32f5bdfbfd420c8 + +PROTOC_VERSION := 3.24.4 +PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go +PROTOC := $(GRADLE_USER_HOME)/caches/protoc-bin-$(PROTOC_VERSION)/protoc +PROTODIR = $(CURDIR)/../../src/main/proto +PROTO_INCLUDEDIR = $(CURDIR)/../../build/extracted-include-protos/debug +PBDIR = $(GOPATH)/pkg/mod default: $(DESTDIR)/libwg-go.so @@ -45,8 +53,25 @@ $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_T patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \ touch "$@"' +$(PROTOC_GEN_GO): export GOARCH := +$(PROTOC_GEN_GO): export GOOS := +$(PROTOC_GEN_GO): export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH) +$(PROTOC_GEN_GO): $(BUILDDIR)/go-$(GO_VERSION)/.prepared Makefile + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + +gen/%.pb.go: export PATH := $(GOPATH)/bin:$(PATH) +gen/%.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO) + test -d gen || mkdir gen + $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go_out=paths=source_relative:./gen $< + +gen/%_grpc.pb.go: export PATH := $(GOPATH)/bin:$(PATH) +gen/%_grpc.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO) + test -d gen || mkdir gen + $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go-grpc_out=./gen --go-grpc_opt=paths=source_relative $< + $(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH) -$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod +$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod api-android.go dhcp.go http-proxy.go service.go gen/libwg.pb.go gen/libwg_grpc.pb.go jni.c go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared .DELETE_ON_ERROR: diff --git a/tunnel/tools/libwg-go/api-android.go b/tunnel/tools/libwg-go/api-android.go index d47c5d76..5d9e8b96 100644 --- a/tunnel/tools/libwg-go/api-android.go +++ b/tunnel/tools/libwg-go/api-android.go @@ -48,10 +48,17 @@ func (l AndroidLogger) Printf(format string, args ...interface{}) { type TunnelHandle struct { device *device.Device uapi net.Listener + logger *device.Logger + tun *tun.NativeTun } var tunnelHandles map[int32]TunnelHandle +func GetTunnel(handle int32) (tunnelHandle TunnelHandle, ok bool) { + tunnelHandle, ok = tunnelHandles[handle] + return +} + func init() { tunnelHandles = make(map[int32]TunnelHandle) signals := make(chan os.Signal) @@ -142,10 +149,29 @@ func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 { device.Close() return -1 } - tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi} + tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi, tun: tun} return i } +//export wgSetFd +func wgSetFd(tunnelHandle int32, tunFd int32) { + tag := cstring(fmt.Sprintf("WireGuard/GoBackend/%x", tunnelHandle)) + logger := &device.Logger{ + Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf, + Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf, + } + + handle, ok := tunnelHandles[tunnelHandle] + if !ok { + unix.Close(int(tunFd)) + logger.Errorf("Tunnel not found") + return + } + + handle.tun.SetFd(int(tunFd)) + logger.Verbosef("wgSetFd: %v", tunFd) +} + //export wgTurnOff func wgTurnOff(tunnelHandle int32) { handle, ok := tunnelHandles[tunnelHandle] @@ -208,20 +234,53 @@ func wgGetConfig(tunnelHandle int32) *C.char { //export wgVersion func wgVersion() *C.char { + return C.CString(Version()) +} + +func Version() string { info, ok := debug.ReadBuildInfo() if !ok { - return C.CString("unknown") + return "unknown" } for _, dep := range info.Deps { if dep.Path == "golang.zx2c4.com/wireguard" { parts := strings.Split(dep.Version, "-") if len(parts) == 3 && len(parts[2]) == 12 { - return C.CString(parts[2][:7]) + return parts[2][:7] } - return C.CString(dep.Version) + return dep.Version } } - return C.CString("unknown") + return "unknown" +} + +//export wgStartGrpc +func wgStartGrpc(sock_path string) C.int{ + tag := cstring("WireGuard/GoBackend/gRPC") + logger := &device.Logger{ + Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf, + Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf, + } + + res, errmsg := StartGrpc(sock_path, logger) + if res < 0 { + logger.Verbosef("wgStartGrpc: %v (%v)", errmsg, res) + } + + return C.int(res) +} + +//export wgSetConfig +func wgSetConfig(tunnelHandle int32, settings string) int32 { + handle, ok := tunnelHandles[tunnelHandle] + if !ok { + return -1 + } + err := handle.device.IpcSet(settings) + if err != nil { + return -1 + } + return 0 } func main() {} diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go new file mode 100644 index 00000000..4af5b0cb --- /dev/null +++ b/tunnel/tools/libwg-go/dhcp.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/dhcpv6/nclient6" + "github.com/insomniacslk/dhcp/iana" + + gen "golang.zx2c4.com/wireguard/android/gen" + "google.golang.org/protobuf/types/known/durationpb" +) + +const ( + ENABLE_PD = true +) + +type dhcp struct { + fqdn string + hwAddr net.HardwareAddr + conn *net.UDPConn + serverAddr net.UDPAddr + client *nclient6.Client + linkAddr net.IP + peerAddr net.IP +} + +func newClientIDOpt(duid dhcpv6.DUID) dhcpv4.Option { + iaid := []byte{0, 0, 0, 3} + ident := []byte{255} // Type IAID+DUID + ident = append(ident, iaid...) // IAID + ident = append(ident, duid.ToBytes()...) // DUID + return dhcpv4.OptClientIdentifier(ident) +} + +func getDuid(hwAddr net.HardwareAddr) dhcpv6.DUID { + duid := &dhcpv6.DUIDLLT{ + HWType: iana.HWTypeEthernet, + Time: uint32(time.Now().Unix()), + LinkLayerAddr: hwAddr, + } + + return duid +} + +func (d *dhcp) encapSendAndRead(ctx context.Context, msg *dhcpv6.Message, match nclient6.Matcher) (*dhcpv6.Message, error) { + packet, err := dhcpv6.EncapsulateRelay(msg, dhcpv6.MessageTypeRelayForward, d.linkAddr, d.peerAddr) + if err != nil { + return nil, err + } + + relay, err := d.client.SendAndReadRelay(ctx, &d.serverAddr, packet, match) + if err != nil { + return nil, err + } + + inner, err := relay.GetInnerMessage() + if err != nil { + return nil, err + } + + return inner, nil +} + +// isRelayMessageType returns a matcher that checks for the message type. +func isRelayMessageType(t dhcpv6.MessageType, tt ...dhcpv6.MessageType) nclient6.Matcher { + return func(p dhcpv6.DHCPv6) bool { + inner, err := p.GetInnerMessage() + if err != nil { + return false + } + if inner.Type() == t { + return true + } + for _, mt := range tt { + if inner.Type() == mt { + return true + } + } + return false + } +} + +// func New() *dhcp { +// } + +func RunDhcp(ctx context.Context, laddr, raddr netip.Addr) ([]*gen.Lease, error) { + d := &dhcp{} + + d.linkAddr = net.ParseIP("fe80::101") + d.peerAddr = net.ParseIP("::1") + + // TODO generate hostname and hwAddr from public key + hostName := "foobar" + d.fqdn = hostName + ".m7n.se" + d.hwAddr = []byte{41, 42, 43, 44, 45, 46} + + + src := net.UDPAddr{IP: laddr.AsSlice(), + Port: 0, // Use non-restrict UDP source port + } + + + d.serverAddr = net.UDPAddr{IP: raddr.AsSlice(), + Port: 547, + } + + err := d.Start(&src) + if err != nil { + return nil, err + } + + defer d.Close() + + reply, err := d.ObtainLease(ctx) // Use reply + if err != nil { + return nil, err + } + + return getAddressesFromReply(reply), nil +} + +func getAddressesFromReply(reply *dhcpv6.Message) []*gen.Lease{ + var leases []*gen.Lease = make([]*gen.Lease, 0, 4) + + if opt := reply.GetOneOption(dhcpv6.OptionIANA); opt != nil { + iana := opt.(*dhcpv6.OptIANA) + ianaOpts := iana.Options.Get(dhcpv6.OptionIAAddr) + + for _, opt := range ianaOpts { + addrOpt := opt.(*dhcpv6.OptIAAddress) + lease := &gen.Lease{ + Address: &gen.InetAddress{ + Address: addrOpt.IPv6Addr, + }, + PreferredLifetime: durationpb.New(addrOpt.PreferredLifetime), + ValidLifetime: durationpb.New(addrOpt.ValidLifetime), + } + leases = append(leases, lease) + } + } + + return leases +} + +func (d *dhcp) Start(localAddr *net.UDPAddr) error { + conn, err := net.ListenUDP("udp6", localAddr) + if err != nil { + return err + } + + d.client, err = nclient6.NewWithConn(conn, d.hwAddr, nclient6.WithDebugLogger()) + return err +} + +func (d *dhcp) Close() error { + err := d.client.Close() + + d.client = nil + return err +} + +func (d *dhcp) ObtainLease(ctx context.Context) (*dhcpv6.Message, error){ + duidOpt := dhcpv6.WithClientID(getDuid(d.hwAddr)) + fqdnOpt := dhcpv6.WithFQDN(0x1, d.fqdn) + + solicit, err := dhcpv6.NewSolicit(d.hwAddr, duidOpt, fqdnOpt, dhcpv6.WithRapidCommit) + if err != nil { + return nil, err + } + + msg, err := d.encapSendAndRead(ctx, solicit, isRelayMessageType(dhcpv6.MessageTypeReply, dhcpv6.MessageTypeAdvertise)) + if err != nil { + return nil, err + } + + if msg.Type() == dhcpv6.MessageTypeReply { + // We got RapidCommitted. + return msg, nil + } + + // We didn't get RapidCommitted. Request regular lease. + req, err := dhcpv6.NewRequestFromAdvertise(msg, fqdnOpt) + if err != nil { + return nil, err + } + + return d.encapSendAndRead(ctx, req, nil) +} diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod index 9318ebcf..86a09293 100644 --- a/tunnel/tools/libwg-go/go.mod +++ b/tunnel/tools/libwg-go/go.mod @@ -3,12 +3,27 @@ module golang.zx2c4.com/wireguard/android go 1.20 require ( - golang.org/x/sys v0.6.0 - golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 + github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d + github.com/insomniacslk/dhcp v0.0.0-00010101000000-000000000000 + golang.org/x/net v0.17.0 + golang.org/x/sys v0.13.0 + golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb + google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.27.1 + gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440 ) require ( - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect ) + +replace github.com/insomniacslk/dhcp => /src/insomniacslk-dhcp + +replace golang.zx2c4.com/wireguard => /src/wireguard-go diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum index 3b49b497..70c365ff 100644 --- a/tunnel/tools/libwg-go/go.sum +++ b/tunnel/tools/libwg-go/go.sum @@ -1,10 +1,167 @@ -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d h1:ikfPAvbWWkCagwNfwfWsIaRyKHDF1I6T4lh7PewKV9A= +github.com/elazarl/goproxy v0.0.0-20231017160920-1fe6677f404d/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU= -golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440 h1:SxFAMd+8zfpL/Rk4pgdb8leeZDiL3M/gCWCbBvmLkoE= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tunnel/tools/libwg-go/http-proxy.go b/tunnel/tools/libwg-go/http-proxy.go new file mode 100644 index 00000000..11ab0716 --- /dev/null +++ b/tunnel/tools/libwg-go/http-proxy.go @@ -0,0 +1,736 @@ +package main + +import ( + "fmt" + "io" + "net" + "net/http" + "net/netip" + "net/url" + "strings" + "sync" + + "github.com/elazarl/goproxy" + + "golang.zx2c4.com/wireguard/device" + + net_proxy "golang.org/x/net/proxy" + + "gopkg.in/olebedev/go-duktape.v3" +) + +const ( + // Imported from Firefox' ProxyAutoConfig.cpp + // https://searchfox.org/mozilla-central/source/netwerk/base/ProxyAutoConfig.cpp + // This Source Code Form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at http://mozilla.org/MPL/2.0/. + ASCII_PAC_UTILS = "function dnsDomainIs(host, domain) {\n" + + " return (host.length >= domain.length &&\n" + + " host.substring(host.length - domain.length) == domain);\n" + + "}\n" + + "" + + "function dnsDomainLevels(host) {\n" + + " return host.split('.').length - 1;\n" + + "}\n" + + "" + + "function isValidIpAddress(ipchars) {\n" + + " var matches = " + + "/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(ipchars);\n" + + " if (matches == null) {\n" + + " return false;\n" + + " } else if (matches[1] > 255 || matches[2] > 255 || \n" + + " matches[3] > 255 || matches[4] > 255) {\n" + + " return false;\n" + + " }\n" + + " return true;\n" + + "}\n" + + "" + + "function convert_addr(ipchars) {\n" + + " var bytes = ipchars.split('.');\n" + + " var result = ((bytes[0] & 0xff) << 24) |\n" + + " ((bytes[1] & 0xff) << 16) |\n" + + " ((bytes[2] & 0xff) << 8) |\n" + + " (bytes[3] & 0xff);\n" + + " return result;\n" + + "}\n" + + "" + + "function isInNet(ipaddr, pattern, maskstr) {\n" + + " if (!isValidIpAddress(pattern) || !isValidIpAddress(maskstr)) {\n" + + " return false;\n" + + " }\n" + + " if (!isValidIpAddress(ipaddr)) {\n" + + " ipaddr = dnsResolve(ipaddr);\n" + + " if (ipaddr == null) {\n" + + " return false;\n" + + " }\n" + + " }\n" + + " var host = convert_addr(ipaddr);\n" + + " var pat = convert_addr(pattern);\n" + + " var mask = convert_addr(maskstr);\n" + + " return ((host & mask) == (pat & mask));\n" + + " \n" + + "}\n" + + "" + + "function isPlainHostName(host) {\n" + + " return (host.search('(\\\\.)|:') == -1);\n" + + "}\n" + + "" + + "function isResolvable(host) {\n" + + " var ip = dnsResolve(host);\n" + + " return (ip != null);\n" + + "}\n" + + "" + + "function localHostOrDomainIs(host, hostdom) {\n" + + " return (host == hostdom) ||\n" + + " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" + + "}\n" + + "" + + "function shExpMatch(url, pattern) {\n" + + " pattern = pattern.replace(/\\./g, '\\\\.');\n" + + " pattern = pattern.replace(/\\*/g, '.*');\n" + + " pattern = pattern.replace(/\\?/g, '.');\n" + + " var newRe = new RegExp('^'+pattern+'$');\n" + + " return newRe.test(url);\n" + + "}\n" + + "" + + "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" + + "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, " + + "AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" + + "" + + "function weekdayRange() {\n" + + " function getDay(weekday) {\n" + + " if (weekday in wdays) {\n" + + " return wdays[weekday];\n" + + " }\n" + + " return -1;\n" + + " }\n" + + " var date = new Date();\n" + + " var argc = arguments.length;\n" + + " var wday;\n" + + " if (argc < 1)\n" + + " return false;\n" + + " if (arguments[argc - 1] == 'GMT') {\n" + + " argc--;\n" + + " wday = date.getUTCDay();\n" + + " } else {\n" + + " wday = date.getDay();\n" + + " }\n" + + " var wd1 = getDay(arguments[0]);\n" + + " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" + + " return (wd1 == -1 || wd2 == -1) ? false\n" + + " : (wd1 <= wd2) ? (wd1 <= wday && wday " + + "<= wd2)\n" + + " : (wd2 >= wday || wday " + + ">= wd1);\n" + + "}\n" + + "" + + "function dateRange() {\n" + + " function getMonth(name) {\n" + + " if (name in months) {\n" + + " return months[name];\n" + + " }\n" + + " return -1;\n" + + " }\n" + + " var date = new Date();\n" + + " var argc = arguments.length;\n" + + " if (argc < 1) {\n" + + " return false;\n" + + " }\n" + + " var isGMT = (arguments[argc - 1] == 'GMT');\n" + + "\n" + + " if (isGMT) {\n" + + " argc--;\n" + + " }\n" + + " // function will work even without explict handling of this case\n" + + " if (argc == 1) {\n" + + " var tmp = parseInt(arguments[0]);\n" + + " if (isNaN(tmp)) {\n" + + " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" + + " getMonth(arguments[0]));\n" + + " } else if (tmp < 32) {\n" + + " return ((isGMT ? date.getUTCDate() : date.getDate()) == " + + "tmp);\n" + + " } else { \n" + + " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) " + + "==\n" + + " tmp);\n" + + " }\n" + + " }\n" + + " var year = date.getFullYear();\n" + + " var date1, date2;\n" + + " date1 = new Date(year, 0, 1, 0, 0, 0);\n" + + " date2 = new Date(year, 11, 31, 23, 59, 59);\n" + + " var adjustMonth = false;\n" + + " for (var i = 0; i < (argc >> 1); i++) {\n" + + " var tmp = parseInt(arguments[i]);\n" + + " if (isNaN(tmp)) {\n" + + " var mon = getMonth(arguments[i]);\n" + + " date1.setMonth(mon);\n" + + " } else if (tmp < 32) {\n" + + " adjustMonth = (argc <= 2);\n" + + " date1.setDate(tmp);\n" + + " } else {\n" + + " date1.setFullYear(tmp);\n" + + " }\n" + + " }\n" + + " for (var i = (argc >> 1); i < argc; i++) {\n" + + " var tmp = parseInt(arguments[i]);\n" + + " if (isNaN(tmp)) {\n" + + " var mon = getMonth(arguments[i]);\n" + + " date2.setMonth(mon);\n" + + " } else if (tmp < 32) {\n" + + " date2.setDate(tmp);\n" + + " } else {\n" + + " date2.setFullYear(tmp);\n" + + " }\n" + + " }\n" + + " if (adjustMonth) {\n" + + " date1.setMonth(date.getMonth());\n" + + " date2.setMonth(date.getMonth());\n" + + " }\n" + + " if (isGMT) {\n" + + " var tmp = date;\n" + + " tmp.setFullYear(date.getUTCFullYear());\n" + + " tmp.setMonth(date.getUTCMonth());\n" + + " tmp.setDate(date.getUTCDate());\n" + + " tmp.setHours(date.getUTCHours());\n" + + " tmp.setMinutes(date.getUTCMinutes());\n" + + " tmp.setSeconds(date.getUTCSeconds());\n" + + " date = tmp;\n" + + " }\n" + + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + + " : (date2 >= date) || (date >= date1);\n" + + "}\n" + + "" + + "function timeRange() {\n" + + " var argc = arguments.length;\n" + + " var date = new Date();\n" + + " var isGMT= false;\n" + + "" + + " if (argc < 1) {\n" + + " return false;\n" + + " }\n" + + " if (arguments[argc - 1] == 'GMT') {\n" + + " isGMT = true;\n" + + " argc--;\n" + + " }\n" + + "\n" + + " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" + + " var date1, date2;\n" + + " date1 = new Date();\n" + + " date2 = new Date();\n" + + "\n" + + " if (argc == 1) {\n" + + " return (hour == arguments[0]);\n" + + " } else if (argc == 2) {\n" + + " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" + + " } else {\n" + + " switch (argc) {\n" + + " case 6:\n" + + " date1.setSeconds(arguments[2]);\n" + + " date2.setSeconds(arguments[5]);\n" + + " case 4:\n" + + " var middle = argc >> 1;\n" + + " date1.setHours(arguments[0]);\n" + + " date1.setMinutes(arguments[1]);\n" + + " date2.setHours(arguments[middle]);\n" + + " date2.setMinutes(arguments[middle + 1]);\n" + + " if (middle == 2) {\n" + + " date2.setSeconds(59);\n" + + " }\n" + + " break;\n" + + " default:\n" + + " throw 'timeRange: bad number of arguments'\n" + + " }\n" + + " }\n" + + "\n" + + " if (isGMT) {\n" + + " date.setFullYear(date.getUTCFullYear());\n" + + " date.setMonth(date.getUTCMonth());\n" + + " date.setDate(date.getUTCDate());\n" + + " date.setHours(date.getUTCHours());\n" + + " date.setMinutes(date.getUTCMinutes());\n" + + " date.setSeconds(date.getUTCSeconds());\n" + + " }\n" + + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + + " : (date2 >= date) || (date >= date1);\n" + + "\n" + + "}\n" +) + +type HttpProxy struct { + listener net.Listener + tlsListener net.Listener + logger *device.Logger + addrPort netip.AddrPort + tlsAddrPort netip.AddrPort + ctx *duktape.Context + defaultProxy *goproxy.ProxyHttpServer + uidRequest chan UidRequest + handlers []*HttpHandler +} + +func NewHttpProxy(uidRequest chan UidRequest, logger *device.Logger) *HttpProxy { + logger.Verbosef("NewHttpProxy") + return &HttpProxy{ + listener: nil, + logger: logger, + uidRequest: uidRequest, + handlers: make([]*HttpHandler, 0, 2), + } +} + +var ( + ASCII_PAC_UTILS_NAMES = []string{"dnsDomainIs", "dnsDomainLevels", "isValidIpAddress", "convert_addr", "isInNet", "isPlainHostName", "isResolvable", "localHostOrDomainIs", "shExpMatch", "weekdayRange", "dateRange", "timeRange"} +) + +func (p *HttpProxy) GetAddrPort() netip.AddrPort { + return p.addrPort +} + +type Logger struct { + logger *device.Logger +} + +func (l *Logger) Printf(format string, v ...interface{}) { + l.logger.Verbosef(format, v...) +} + +func (p *HttpProxy) newGoProxy(cat, proxyUrl string) (*goproxy.ProxyHttpServer, error) { + proxy := goproxy.NewProxyHttpServer() + proxy.Logger = &Logger{logger: p.logger} + proxy.Verbose = true + if cat == "SOCKS5" { + socksDialer, err := net_proxy.SOCKS5("tcp", proxyUrl, nil, nil) + if err != nil { + return nil, err + } + proxy.Tr.Dial = socksDialer.Dial + } + proxy.NonproxyHandler = http.HandlerFunc(func (w http.ResponseWriter, req *http.Request) { + if req.Host == "" { + fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") + return + } + req.URL.Scheme = "http" + req.URL.Host = req.Host + proxy.ServeHTTP(w, req) + }) + + if cat == "PROXY" { + proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) { + return url.Parse(proxyUrl) + } + proxy.ConnectDial = proxy.NewConnectDialToProxy(proxyUrl) + } + + return proxy, nil +} + +func FindProxyForURL(ctx *duktape.Context, url, host string, logger *device.Logger) (res string, err error) { + if !ctx.GetGlobalString("FindProxyForURL") { + ctx.Pop() + return "", fmt.Errorf("FindProxyForURL not found") + } + + ctx.PushString(url) + ctx.PushString(host) + + res = "DIRECT" + logger.Verbosef("Before pcall") + r := ctx.Pcall(2) + logger.Verbosef("After pcall") + if r == 0 { + res = ctx.GetString(-1) + } else if ctx.IsError(-1) { + ctx.GetPropString(-1, "stack") + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } else { + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } + ctx.Pop() + return +} + + +func FindProxyForPkg(ctx *duktape.Context, pkg string, logger *device.Logger) (res string, err error) { + if !ctx.GetGlobalString("FindProxyForPkg") { + ctx.Pop() + return "", fmt.Errorf("FindProxyForPkg not found") + } + + ctx.PushString(pkg) + + res = "DIRECT" + logger.Verbosef("Before pcall") + r := ctx.Pcall(1) + logger.Verbosef("After pcall") + if r == 0 { + res = ctx.GetString(-1) + } else if ctx.IsError(-1) { + ctx.GetPropString(-1, "stack") + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } else { + err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) + } + ctx.Pop() + return +} + +func newPacBodyCtx(body string, logger *device.Logger) *duktape.Context { + ctx := duktape.New() + ctx.PushGlobalGoFunction("dnsResolve", + func(ctx *duktape.Context) int { + // Check stack + host := ctx.GetString(-1) + ctx.Pop() + ips, err := net.LookupIP(host) + if err != nil { + return 0 + } + + for _, ip := range(ips) { + ipStr := string(ip) + if strings.Contains(ipStr, ".") { + // Found IPv4 + ctx.PushString(ipStr) + return 1 + } + } + // No IPv4 + return 0 + }) + + ctx.PevalString(ASCII_PAC_UTILS) + logger.Verbosef("ASCII_PAC_UTILS result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) + ctx.Pop() + + ctx.PevalString(string(body)) + logger.Verbosef("result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) + ctx.Pop() + FindProxyForURL(ctx, "http://www.jabra.se/", "www.jabra.se", logger) + return ctx +} + +func (p *HttpProxy) newPacFileCtx(pacFileUrl *url.URL) (*duktape.Context, error) { + var pacFileBody = "" + if pacFileUrl == nil { + return nil, nil + } + + resp, err := http.Get(pacFileUrl.String()) + p.logger.Verbosef("pacFile: %v, %v", resp, err) + + if err == nil { + defer resp.Body.Close() + ct, ok := resp.Header["Content-Type"] + if ok && len(ct) == 1 && ct[0] == "application/x-ns-proxy-autoconfig" { + body, err := io.ReadAll(resp.Body) + if err == nil { + pacFileBody = string(body) + return newPacBodyCtx(pacFileBody, p.logger), nil + } + } + } + + return nil, err +} + +func (p *HttpProxy) SetPacFileUrl(pacFileUrl *url.URL) error { + ctx, err := p.newPacFileCtx(pacFileUrl) + if err != nil { + return err + } + + for _, handler := range(p.handlers) { + handler.setPacCtx(ctx) + } + + if p.ctx != nil { + p.ctx.Destroy() + } + p.ctx = ctx + p.logger.Verbosef("SetNotMetered(false) from SetPacFileUrl") + p.SetNotMetered(false) + return nil +} + +func (p *HttpProxy) SetPacFileContent(pacFile string) error { + ctx := newPacBodyCtx(pacFile, p.logger) + + for _, handler := range(p.handlers) { + handler.setPacCtx(ctx) + } + + if p.ctx != nil { + p.ctx.Destroy() + } + p.ctx = ctx + p.logger.Verbosef("SetNotMetered(false) from SetPacFileContent") + p.SetNotMetered(false) + return nil +} + +func (p *HttpProxy) SetNotMetered(notMetered bool) error { + p.logger.Verbosef("SetNotMetered: %v", notMetered) + + if p.ctx == nil { + return fmt.Errorf("No ductape context") + } + + p.ctx.PushGlobalObject() + p.ctx.PushBoolean(notMetered) + p.ctx.PutPropString(-2, "notMetered") + p.ctx.Pop() + + return nil +} + +func (p *HttpProxy) Start() (listen_port uint16, err error) { + p.logger.Verbosef("HttpProxy.Start()") + listen_port = 0 + + + proxyMap := make(map[string]*goproxy.ProxyHttpServer) + + p.defaultProxy, err = p.newGoProxy("", "") + if err != nil { + return + } + proxyMap[""] = p.defaultProxy + + listen_port, handler, err := p.startRegularProxy(proxyMap) + if err != nil { + return + } + p.handlers = append(p.handlers, handler) + + return +} + +func (p *HttpProxy) startRegularProxy(proxyMap map[string]*goproxy.ProxyHttpServer) (listen_port uint16, handler *HttpHandler, err error) { + p.listener, err = net.Listen("tcp", "[::]:") + if err != nil { + return + } + + p.addrPort, err = netip.ParseAddrPort(p.listener.Addr().String()) + if err != nil { + return + } + + listen_port = p.addrPort.Port() + + handler = NewHttpHandler(p, p.defaultProxy, proxyMap, p.logger) + + go http.Serve(NewListener(p.listener, handler, p.logger), handler) + return +} + +func (p *HttpProxy) Stop() { + if p.listener != nil { + p.logger.Verbosef("Close: %v", p.listener) + p.listener.Close() +// p.listener = nil + } + if p.tlsListener != nil { + p.logger.Verbosef("Close: %v", p.tlsListener) + p.tlsListener.Close() +// p.tlsListener = nil + } + if p.ctx != nil { + p.ctx.DestroyHeap() + p.ctx = nil + } +} + +type HttpHandler struct { + p *HttpProxy + defaultProxy *goproxy.ProxyHttpServer + logger *device.Logger + remoteAddrPkgMapMutex sync.RWMutex + remoteAddrPkgMap map[string]*goproxy.ProxyHttpServer + uidRequest chan UidRequest + ctx *duktape.Context + proxyMap map[string]*goproxy.ProxyHttpServer // FIXME include type in map key +} + +func NewHttpHandler(p *HttpProxy, defaultProxy *goproxy.ProxyHttpServer, proxyMap map[string]*goproxy.ProxyHttpServer, logger *device.Logger) *HttpHandler{ + h := &HttpHandler{ + p: p, + defaultProxy: defaultProxy, + logger: logger, + remoteAddrPkgMapMutex: sync.RWMutex{}, + remoteAddrPkgMap: make(map[string]*goproxy.ProxyHttpServer), + uidRequest: p.uidRequest, + proxyMap: proxyMap, + } + + return h +} + +// TODO fix multi-threading +func (h *HttpHandler) setPacCtx(ctx *duktape.Context) { + h.ctx = ctx +} + +func (h *HttpHandler) addConnToProxyMap(c net.Conn) error { + h.logger.Verbosef("Accept: %v -> %v", c.RemoteAddr().String(), c.LocalAddr().String()) + local, err := netip.ParseAddrPort(c.RemoteAddr().String()) + if err != nil { + return fmt.Errorf("Bad local address (%v): %v", c.RemoteAddr(), err) + } + // Remove IPv6 zone, NOOP on IPv4 + local = netip.AddrPortFrom(local.Addr().WithZone(""), local.Port()) + + remote, err := netip.ParseAddrPort(c.LocalAddr().String()) + if err != nil { + return fmt.Errorf("Bad remote address (%v): %v", c.LocalAddr(), err) + } + + h.logger.Verbosef("uidRequest: %v -> %v", local, remote) + addrPortPair := AddrPortPair{local: local, remote: remote} + retCh := make(chan string) + h.uidRequest <- UidRequest{Data: addrPortPair, RetCh: retCh} + + select { + case pkg := <-retCh: + h.logger.Verbosef("uidResponse: '%v'", pkg) + + // TODO add NotMetered as + proxy, ok := h.proxyMap[pkg] + if !ok { + proxy, err = h.findProxyForPkg(pkg) + if err != nil { + return err + } + } + + if proxy != nil { + h.remoteAddrPkgMapMutex.Lock() + h.remoteAddrPkgMap[c.RemoteAddr().String()] = proxy + h.remoteAddrPkgMapMutex.Unlock() + } + } + + return nil +} + +func (h *HttpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + h.remoteAddrPkgMapMutex.Lock() + proxy, ok := h.remoteAddrPkgMap[req.RemoteAddr] + if ok && proxy != nil { + delete(h.remoteAddrPkgMap, req.RemoteAddr) + h.remoteAddrPkgMapMutex.Unlock() + + proxyStr := "nil" + proxyUrl, err := proxy.Tr.Proxy(req) + if err == nil && proxyUrl != nil { + proxyStr = proxyUrl.String() + } + h.logger.Verbosef("ServeHTTP remote:%s proxy:%v", req.RemoteAddr, proxyStr) + + proxy.ServeHTTP(rw, req) + } else { + h.remoteAddrPkgMapMutex.Unlock() + h.defaultProxy.ServeHTTP(rw, req) + } +} + +func (h *HttpHandler) findProxyForPkg(pkg string) (*goproxy.ProxyHttpServer, error) { + if h.ctx == nil { + return nil, fmt.Errorf("Null context") + } + + find := func() (cat string, res string, err error) { + h.logger.Verbosef("Call FindProxyForPkg %v %v", pkg) + res, err = FindProxyForPkg(h.ctx, pkg, h.logger) + h.logger.Verbosef("FindProxyForPkg res %v %v", res, err) + if err != nil { + h.logger.Verbosef("FindProxyForPkg result is: %v stack:%v", res, h.ctx.GetTop()) + return "", "", err + } else if res == "" { + return "DIRECT", "", nil + } else { + values := strings.Split(strings.Trim(res, " "), ";") + for _, v := range values { + value := strings.Trim(v, " ") + parts := strings.SplitN(value, " ", 2) + if parts[0] == "PROXY" || parts[0] == "HTTP" { + return "PROXY", "http://" + parts[1], nil + } else if parts[0] == "HTTPS" { + return "PROXY", "https://" + parts[1], nil + } else if parts[0] == "DIRECT" { + return parts[0], "", nil + } else if parts[0] == "SOCKS" || parts[0] == "SOCKS5" { + return "SOCKS5", parts[1], nil + } + } + } + return "", "", fmt.Errorf("No result") + } + cat, res, err := find() + if err != nil { + return nil, err + } + + var proxy *goproxy.ProxyHttpServer + if cat == "DIRECT" { + proxy = h.defaultProxy + } else { + proxy, err = h.p.newGoProxy(cat, res) + if err != nil { + return nil, err + } + } + h.proxyMap[pkg] = proxy + + return proxy, nil +} + +type AddrPortPair struct { + local netip.AddrPort + remote netip.AddrPort +} + +// Listener +type Listener struct { + l net.Listener + handler *HttpHandler + logger *device.Logger +} + +func NewListener(listener net.Listener, handler *HttpHandler, logger *device.Logger) *Listener{ + l := &Listener{ + l: listener, + handler: handler, + logger: logger, + } + + return l +} + +func (l *Listener) Accept() (net.Conn, error) { + c, err := l.l.Accept() + if err != nil { + l.logger.Verbosef("Accept failed: %v", err) + return c, err + } + + if err := l.handler.addConnToProxyMap(c); err != nil { + err = fmt.Errorf("Reject connection (%v): %v", c, err) + c.Close() + return nil, err + } + + return c, nil +} + +func (l *Listener) Close() error { + return l.l.Close() +} + +func (l *Listener) Addr() net.Addr { + return l.l.Addr() +} diff --git a/tunnel/tools/libwg-go/jni.c b/tunnel/tools/libwg-go/jni.c index 7ad94d35..443848e8 100644 --- a/tunnel/tools/libwg-go/jni.c +++ b/tunnel/tools/libwg-go/jni.c @@ -14,6 +14,9 @@ extern int wgGetSocketV4(int handle); extern int wgGetSocketV6(int handle); extern char *wgGetConfig(int handle); extern char *wgVersion(); +extern int wgSetConfig(int handle, struct go_string settings); +extern int wgStartGrpc(); +extern int wgSetFd(int handle, int tun_fd); JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings) { @@ -69,3 +72,32 @@ JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion free(version); return ret; } + +JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetConfig(JNIEnv *env, jclass c, jint handle, jstring settings) +{ + const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0); + size_t settings_len = (*env)->GetStringUTFLength(env, settings); + int ret = wgSetConfig(handle, (struct go_string){ + .str = settings_str, + .n = settings_len + }); + (*env)->ReleaseStringUTFChars(env, settings, settings_str); + return ret; +} + +JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgStartGrpc(JNIEnv *env, jclass c, jstring sockname) +{ + const char *sockname_str = (*env)->GetStringUTFChars(env, sockname, 0); + size_t sockname_len = (*env)->GetStringUTFLength(env, sockname); + jint res = wgStartGrpc((struct go_string){ + .str = sockname_str, + .n = sockname_len + }); + (*env)->ReleaseStringUTFChars(env, sockname, sockname_str); + return res; +} + +JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetFd(JNIEnv *env, jclass c, jint handle, jint tun_fd) +{ + wgSetFd(handle, tun_fd); +} diff --git a/tunnel/tools/libwg-go/service.go b/tunnel/tools/libwg-go/service.go new file mode 100644 index 00000000..41834fe0 --- /dev/null +++ b/tunnel/tools/libwg-go/service.go @@ -0,0 +1,342 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + "net/netip" + "net/url" + "os" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + gen "golang.zx2c4.com/wireguard/android/gen" + "golang.zx2c4.com/wireguard/device" +) + +const ( + IPPROTO_TCP = 6 +) + +type UidRequest struct { + Data AddrPortPair + RetCh chan string +} + +type LibwgServiceImpl struct { + gen.UnimplementedLibwgServer + logger *device.Logger + httpProxy *HttpProxy + uidRequest chan UidRequest + stopReverse chan bool +} + +var service *LibwgServiceImpl +var server *grpc.Server + +func NewLibwgService(logger *device.Logger) gen.LibwgServer { + return &LibwgServiceImpl{ + logger: logger, + uidRequest: make(chan UidRequest), + stopReverse: make(chan bool), + } +} + +func StartGrpc(sock_path string, logger *device.Logger) (int, string) { + if server != nil { + return -1, "Already started" + } + + if _, err := os.Stat(sock_path); err == nil { + if err := os.RemoveAll(sock_path); err != nil { + return -1, fmt.Sprintf("Cleanup failed: %v %v", sock_path, err) + } + } + + listener, err := net.Listen("unix", sock_path) + if err != nil { + return -1, fmt.Sprintf("Listen failed: %v %v", sock_path, err) + } + + server = grpc.NewServer() + service = NewLibwgService(logger).(*LibwgServiceImpl) + + gen.RegisterLibwgServer(server, service) + + go func() { + server.Serve(listener) + }() + + logger.Verbosef("gRPC started") + + return 0, "" +} + +func (e *LibwgServiceImpl) Version(ctx context.Context, req *gen.VersionRequest) (*gen.VersionResponse, error) { + + r := &gen.VersionResponse{ + Version: Version(), + } + + return r, nil +} + +func (e *LibwgServiceImpl) StopGrpc(ctx context.Context, req *gen.StopGrpcRequest) (*gen.StopGrpcResponse, error) { + if server != nil { + server.Stop() + server = nil + service = nil + } + + r := &gen.StopGrpcResponse{ + } + + return r, nil +} + +func buildStartHttpProxyError(message string) (*gen.StartHttpProxyResponse, error) { + r := &gen.StartHttpProxyResponse{ + Error: &gen.Error{ + Message: message, + }, + } + return r, nil +} + +func (e *LibwgServiceImpl) StartHttpProxy(ctx context.Context, req *gen.StartHttpProxyRequest) (*gen.StartHttpProxyResponse, error) { + var listenPort uint16 + if e.httpProxy == nil { + e.httpProxy = NewHttpProxy(e.uidRequest, e.logger) + var err error + listenPort, err = e.httpProxy.Start() + + if err != nil { + e.httpProxy = nil + return buildStartHttpProxyError(fmt.Sprintf("Http proxy start failed: %v", err)) + } + } else { + listenPort = e.httpProxy.GetAddrPort().Port() + } + + content := req.GetPacFileContent() + if content != "" { + err := e.httpProxy.SetPacFileContent(content) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileContent: %v (%s)", content)) + } + } else { + pacFileUrl, err := url.Parse(req.GetPacFileUrl()) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, req.GetPacFileUrl())) + } + err = e.httpProxy.SetPacFileUrl(pacFileUrl) + if err != nil { + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, pacFileUrl)) + } + } + + r := &gen.StartHttpProxyResponse{ + ListenPort: uint32(listenPort), + } + return r, nil +} + +func (e *LibwgServiceImpl) StopHttpProxy(ctx context.Context, req *gen.StopHttpProxyRequest) (*gen.StopHttpProxyResponse, error) { + if e.httpProxy == nil { + r := &gen.StopHttpProxyResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Http proxy not running"), + }, + } + return r, nil + } + + e.httpProxy.Stop() + e.httpProxy = nil + e.stopReverse <- true + r := &gen.StopHttpProxyResponse{} + return r, nil +} + +func (e *LibwgServiceImpl) Reverse(stream gen.Libwg_ReverseServer) error { + e.logger.Verbosef("Reverse enter loop") + for e.httpProxy != nil { + var err error + + // err := contextError(stream.Context()) + err = stream.Context().Err() + if err != nil { + e.logger.Verbosef("Reverse: context: %v", err) + return err + } + + select { + case <-e.stopReverse: + e.logger.Verbosef("Reverse: stop") + break + case uidReq := <-e.uidRequest: + addrPortPair := uidReq.Data + local := addrPortPair.local + remote := addrPortPair.remote + r := &gen.ReverseResponse{ + Request: &gen.ReverseResponse_Uid{ + Uid: &gen.GetConnectionOwnerUidRequest{ + Protocol: IPPROTO_TCP, + Local: &gen.InetSocketAddress{ + Address: &gen.InetAddress{ + Address: local.Addr().AsSlice(), + }, + Port: uint32(local.Port()), + }, + Remote: &gen.InetSocketAddress{ + Address: &gen.InetAddress{ + Address: remote.Addr().AsSlice(), + }, + Port: uint32(remote.Port()), + }, + }, + }, + } + + stream.Send(r) + + req, err := stream.Recv() + if err == io.EOF { + e.logger.Verbosef("no more data") + uidReq.RetCh <- "" + break + } + if err != nil { + err = status.Errorf(codes.Unknown, "cannot receive stream request: %v", err) + e.logger.Verbosef("Reverse: %v", err) + uidReq.RetCh <- "" + return err + } + + e.logger.Verbosef("Reverse: received, wait: %v", req) + uidReq.RetCh <- req.GetUid().GetPackage() + } + } + + + e.logger.Verbosef("Reverse returns") + return nil +} + +func (e *LibwgServiceImpl) IpcSet(ctx context.Context, req *gen.IpcSetRequest) (*gen.IpcSetResponse, error) { + tunnel, ok := GetTunnel(req.GetTunnel().GetHandle()) + if !ok { + r := &gen.IpcSetResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Invalid tunnel"), + }, + } + return r, nil + } + + err := tunnel.device.IpcSet(req.GetConfig()) + if err != nil { + r := &gen.IpcSetResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("IpcSet failed: %v", err), + }, + } + return r, nil + } + + r := &gen.IpcSetResponse{ + } + + return r, nil +} + +func (e *LibwgServiceImpl) Dhcp(ctx context.Context, req *gen.DhcpRequest) (*gen.DhcpResponse, error) { + var relayAddr netip.Addr + var sourceAddr netip.Addr + + source := req.GetSource() + if source != nil { + sourceAddr, _ = netip.AddrFromSlice(source.GetAddress()) + } + + if !sourceAddr.IsValid() || !sourceAddr.Is6() { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("DHCPv6 source address missing"), + }, + } + return r, nil + } + + relay := req.GetRelay() + if relay != nil { + relayAddr, _ = netip.AddrFromSlice(relay.GetAddress()) + } else { + // Construct relay address from source prefix + relayRaw := source.GetAddress()[:8] + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 0) + relayRaw = append(relayRaw, 1) + relayAddr, _ = netip.AddrFromSlice(relayRaw) + } + + if !relayAddr.IsValid() || !relayAddr.Is6() { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("DHCPv6 relay address calculation failed"), + }, + } + return r, nil + } + + e.logger.Verbosef("RunDhcp %v %v", sourceAddr, relayAddr) + + leases, err := RunDhcp(ctx, sourceAddr, relayAddr) + if err != nil { + r := &gen.DhcpResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("RunDhcp failed: %v", err), + }, + } + return r, nil + } + + r := &gen.DhcpResponse{ + Leases: leases, + } + return r, nil +} + +func (e *LibwgServiceImpl) CapabilitiesChanged(ctx context.Context, req *gen.CapabilitiesChangedRequest) (*gen.CapabilitiesChangedResponse, error) { + if e.httpProxy == nil { + r := &gen.CapabilitiesChangedResponse{ + Error: &gen.Error{ + Message: fmt.Sprintf("Http Proxy not running"), + }, + } + return r, nil + } + + notMetered := false + + for _, cap := range req.Capabilities { + if cap == gen.CapabilitiesChangedRequest_NOT_METERED { + notMetered = true + } + } + + e.logger.Verbosef("Set not metered %v", notMetered) + e.httpProxy.SetNotMetered(notMetered) + + r := &gen.CapabilitiesChangedResponse{ + } + return r, nil +} diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 58a6a687..fff3acd9 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -1,4 +1,5 @@ @file:Suppress("UnstableApiUsage") + import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -9,10 +10,11 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.ajoberstar.grgit) } android { - compileSdk = 33 + compileSdk = 34 buildFeatures { buildConfig = true dataBinding = true @@ -22,11 +24,13 @@ android { defaultConfig { applicationId = appID minSdk = 21 - targetSdk = 33 + targetSdk = 34 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 +54,7 @@ android { resources { excludes += "DebugProbesKt.bin" excludes += "kotlin-tooling-metadata.json" + excludes += "META-INF/*.version" } } } @@ -60,9 +65,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..902c6530 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -3,11 +3,16 @@ 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" /> <uses-permission + android:name="android.permission.SYSTEM_ALERT_WINDOW" + android:minSdkVersion="34" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" tools:ignore="ScopedStorage" /> @@ -45,10 +50,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 +69,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 +94,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 +107,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 +126,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 +137,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" /> @@ -137,6 +148,10 @@ <meta-data android:name="android.service.quicksettings.ACTIVE_TILE" android:value="false" /> + + <meta-data + android:name="android.service.quicksettings.TOGGLEABLE_TILE" + android:value="true" /> </service> <meta-data @@ -149,5 +164,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..9be2cc2b 100644 --- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt +++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt @@ -4,12 +4,15 @@ */ package com.wireguard.android +import android.app.PendingIntent import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.Icon +import android.net.Uri import android.os.Build import android.os.IBinder +import android.provider.Settings import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log @@ -49,32 +52,42 @@ class QuickTileService : TileService() { } override fun onClick() { - if (tunnel != null) { - unlockAndRun { - val tile = qsTile - if (tile != null) { - tile.icon = if (tile.icon == iconOn) iconOff else iconOn - tile.updateTile() + when (val tunnel = tunnel) { + null -> { + val intent = Intent(this, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)) + } else { + @Suppress("DEPRECATION") + startActivityAndCollapse(intent) } - applicationScope.launch { - try { - tunnel!!.setStateAsync(Tunnel.State.TOGGLE) - updateTile() - } catch (_: Throwable) { - val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java) - toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(toggleIntent) + } + else -> { + unlockAndRun { + applicationScope.launch { + try { + tunnel.setStateAsync(Tunnel.State.TOGGLE) + updateTile() + } catch (_: Throwable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !Settings.canDrawOverlays(this@QuickTileService)) { + val permissionIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) + permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivityAndCollapse(PendingIntent.getActivity(this@QuickTileService, 0, permissionIntent, PendingIntent.FLAG_IMMUTABLE)) + return@launch + } + val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java) + toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(toggleIntent) + } } } } - } else { - val intent = Intent(this, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivityAndCollapse(intent) } } override fun onCreate() { + isAdded = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { iconOn = Icon.createWithResource(this, R.drawable.ic_tile) iconOff = iconOn @@ -84,55 +97,64 @@ class QuickTileService : TileService() { icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */ icon.setSlashed(false) var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) - ?: return var c = Canvas(b) icon.setBounds(0, 0, c.width, c.height) icon.draw(c) iconOn = Icon.createWithBitmap(b) icon.setSlashed(true) b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) - ?: return c = Canvas(b) icon.setBounds(0, 0, c.width, c.height) icon.draw(c) iconOff = Icon.createWithBitmap(b) } + override fun onDestroy() { + super.onDestroy() + isAdded = false + } + override fun onStartListening() { Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback) - if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback) + tunnel?.addOnPropertyChangedCallback(onStateChangedCallback) updateTile() } override fun onStopListening() { - if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback) + tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback) 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 if (newTunnel != tunnel) { - if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback) + tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback) tunnel = newTunnel - if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback) + tunnel?.addOnPropertyChangedCallback(onStateChangedCallback) } // Update the tile contents. - val label: String - val state: Int - val tile = qsTile - if (tunnel != null) { - label = tunnel!!.name - state = if (tunnel!!.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE - } else { - label = getString(R.string.app_name) - state = Tile.STATE_INACTIVE - } - if (tile == null) return - tile.label = label - if (tile.state != state) { - tile.icon = if (state == Tile.STATE_ACTIVE) iconOn else iconOff - tile.state = state + val tile = qsTile ?: return + + when (val tunnel = tunnel) { + null -> { + tile.label = getString(R.string.app_name) + tile.state = Tile.STATE_INACTIVE + tile.icon = iconOff + } + else -> { + tile.label = tunnel.name + tile.state = if (tunnel.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + tile.icon = if (tunnel.state == Tunnel.State.UP) iconOn else iconOff + } } tile.updateTile() } @@ -143,19 +165,23 @@ class QuickTileService : TileService() { sender.removeOnPropertyChangedCallback(this) return } - if (propertyId != 0 && propertyId != BR.state) return + if (propertyId != 0 && propertyId != BR.state) + return updateTile() } } private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { - if (propertyId != 0 && propertyId != BR.lastUsedTunnel) return + if (propertyId != 0 && propertyId != BR.lastUsedTunnel) + return updateTile() } } 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..69e33bc9 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) { @@ -215,7 +223,7 @@ class LogViewerActivity : AppCompatActivity() { } else { if (bufferedLogLines.isNotEmpty()) { bufferedLogLines.last().msg += "\n$line" - } else if (!logLines.isEmpty) { + } else if (!logLines.isEmpty()) { logLines[logLines.size() - 1].msg += "\n$line" priorModified = true } @@ -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..6b2040e1 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.wireguard.android.BR import com.wireguard.android.R +import com.wireguard.android.backend.Dhcp; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler import com.wireguard.android.widget.ToggleSwitch import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener @@ -47,9 +48,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 +76,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 +98,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 +130,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 @@ -160,6 +171,12 @@ object BindingAdapters { } @JvmStatic + @BindingAdapter("android:text") + fun setDhcpLeaseSetText(view: TextView, dhcp: Dhcp?) { + view.text = if (dhcp?.leases != null) Attribute.join(dhcp.leases.map { it }) else "" + } + + @JvmStatic fun tryParseInt(s: String?): Int { if (s == null) return 0 diff --git a/ui/src/main/java/com/wireguard/android/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..55c84df5 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 } @@ -136,6 +161,104 @@ class ObservableTunnel internal constructor( return statistics } + @get:Bindable + var dhcp: Dhcp? = null + private set + + override fun onDhcpChange(newDhcp: Dhcp) { + onDhcpChanged(newDhcp) + } + + fun onDhcpChanged(dhcp: Dhcp?): Dhcp? { + this.dhcp = dhcp + notifyPropertyChanged(BR.dhcp) + return dhcp + } + + // Remove dynamic peers, and reset static peers + fun onPeersReset() { + Log.i(TAG, "ObservableTunnel onPeersReset") + var toRemove: MutableList<PeerDetail> = ArrayList() + + configDetail?.peers?.forEach { + if (it.peer == null) { + toRemove.add(it) + } else { + it.endpoint = Optional.empty() + } + } + + toRemove.forEach { + Log.i(TAG, "ObservableTunnel remove " + it) + configDetail?.peers?.remove(it) + } + } + + override fun onEndpointChange(publicKey: Key, newEndpoint: InetEndpoint?) { + Application.getCoroutineScope().launch { + onEndpointChanged(publicKey, newEndpoint) + } + } + + private fun onEndpointChanged(publicKey: Key, newEndpoint: InetEndpoint?) { + + Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint) + var 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..30da3b07 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,16 @@ 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.dialog.MaterialAlertDialogBuilder 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 +24,20 @@ 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 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) @@ -42,11 +50,10 @@ object SnackbarUpdateShower { 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) + (snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus) ) return - activity.lifecycleScope.launch { + fragment.lifecycleScope.launch { delay(5.seconds) snackbar?.show() } @@ -88,17 +95,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 +105,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 +141,33 @@ 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 -> { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.updater_corrupt_title) + .setMessage(R.string.updater_corrupt_message) + .setPositiveButton(R.string.updater_corrupt_navigate) { _, _ -> + 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() + } + }.setCancelable(false).setOnDismissListener { + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_HOME) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + System.exit(0) + }.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..87adad96 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 @@ -19,6 +21,7 @@ import com.wireguard.android.Application import com.wireguard.android.BuildConfig import com.wireguard.android.activity.MainActivity import com.wireguard.android.util.UserKnobs +import com.wireguard.android.util.applicationScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -44,22 +47,35 @@ 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 const val LATEST_FILE = "latest.sig" + private const val RELEASE_PUBLIC_KEY_BASE64 = "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp" + private val CURRENT_VERSION by lazy { Version(BuildConfig.VERSION_NAME) } 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() { fun update() { - Application.getCoroutineScope().launch { + applicationScope.launch { UserKnobs.setUpdaterNewerVersionConsented(version) } } @@ -83,7 +99,7 @@ object Updater { } fun markAsDone() { - Application.getCoroutineScope().launch { + applicationScope.launch { if (installerActive()) return@launch delay(7.seconds) @@ -101,6 +117,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 +152,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 +224,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 +243,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 +260,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. */ @@ -300,13 +315,18 @@ object Updater { session.close() } + private var updating = false private suspend fun downloadAndUpdateWrapErrors() { + if (updating) + return + updating = true try { downloadAndUpdate() } catch (e: Throwable) { Log.e(TAG, "Update failure", e) emitProgress(Progress.Failure(e)) } + updating = false } private class InstallReceiver : BroadcastReceiver() { @@ -316,25 +336,17 @@ 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 - )!! - Application.getCoroutineScope().launch { + val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!! + applicationScope.launch { emitProgress(Progress.NeedsUserIntervention(userIntervention, id)) } } PackageInstaller.STATUS_SUCCESS -> { - Application.getCoroutineScope().launch { + applicationScope.launch { emitProgress(Progress.Complete) } context.applicationContext.unregisterReceiver(this) @@ -346,10 +358,8 @@ object Updater { context.applicationContext.packageManager.packageInstaller.abandonSession(id) } catch (_: SecurityException) { } - val message = - intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - ?: "Installation error $status" - Application.getCoroutineScope().launch { + val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status" + applicationScope.launch { val e = Exception(message) Log.e(TAG, "Update failure", e) emitProgress(Progress.Failure(e)) @@ -361,13 +371,36 @@ object Updater { } fun monitorForUpdates() { - if (BuildConfig.IS_GOOGLE_PLAY) + if (BuildConfig.DEBUG) + return + + 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) + ) { + if (installer(context).isNotEmpty()) { + 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,41 +420,28 @@ 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()) + }.launchIn(applicationScope) UserKnobs.updaterNewerVersionConsented.onEach { ver -> if (ver != null && Version(ver) > CURRENT_VERSION) updaterScope.launch { downloadAndUpdateWrapErrors() } - }.launchIn(Application.getCoroutineScope()) + }.launchIn(applicationScope) } 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 +451,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/ClipboardUtils.kt b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt index 6a0d54ba..c9a7f59d 100644 --- a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt +++ b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt @@ -6,6 +6,7 @@ package com.wireguard.android.util import android.content.ClipData import android.content.ClipboardManager +import android.os.Build import android.view.View import android.widget.TextView import androidx.core.content.getSystemService @@ -29,6 +30,8 @@ object ClipboardUtils { } val service = view.context.getSystemService<ClipboardManager>() ?: return service.setPrimaryClip(ClipData.newPlainText(data.second, data.first)) - Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show() + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show() + } } } 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..81a548f9 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt @@ -4,6 +4,8 @@ */ package com.wireguard.android.viewmodel +import android.net.Uri +import android.os.Build import android.os.Parcel import android.os.Parcelable import androidx.databinding.BaseObservable @@ -18,6 +20,12 @@ import com.wireguard.crypto.Key import com.wireguard.crypto.KeyFormatException import com.wireguard.crypto.KeyPair +object Constants { + const val HTTP_PROXY_NONE = "None" + const val HTTP_PROXY_MANUAL = "Manual" + const val HTTP_PROXY_PAC = "Proxy Auto-Config" +} + class InterfaceProxy : BaseObservable, Parcelable { @get:Bindable val excludedApplications: ObservableList<String> = ObservableArrayList() @@ -54,6 +62,44 @@ class InterfaceProxy : BaseObservable, Parcelable { } @get:Bindable + var httpProxyMenu: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyMenu) + notifyPropertyChanged(BR.httpProxyManualVisibility) + notifyPropertyChanged(BR.httpProxyPacVisibility) + } + + @get:Bindable + var httpProxyManualVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_MANUAL) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable + var httpProxyHostname: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyHostname) + } + + @get:Bindable + var httpProxyPort: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPort) + } + + @get:Bindable + var httpProxyPac: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPac) + } + + @get:Bindable + var httpProxyPacVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_PAC) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable var privateKey: String = "" set(value) { field = value @@ -76,6 +122,10 @@ class InterfaceProxy : BaseObservable, Parcelable { parcel.readStringList(includedApplications) listenPort = parcel.readString() ?: "" mtu = parcel.readString() ?: "" + httpProxyMenu = parcel.readString() ?: "" + httpProxyHostname = parcel.readString() ?: "" + httpProxyPort = parcel.readString() ?: "" + httpProxyPac = parcel.readString() ?: "" privateKey = parcel.readString() ?: "" } @@ -87,6 +137,10 @@ class InterfaceProxy : BaseObservable, Parcelable { includedApplications.addAll(other.includedApplications) listenPort = other.listenPort.map { it.toString() }.orElse("") mtu = other.mtu.map { it.toString() }.orElse("") + httpProxyHostname = other.httpProxy.map { if (it.getHost().startsWith('[') && it.getHost().endsWith(']')) it.getHost().substring(1, it.getHost().length-1) else it.getHost() }.orElse("") + httpProxyPort = other.httpProxy.map { if (it.getPort() <= 0) "8080" else it.getPort().toString() }.orElse("") + httpProxyPac = other.httpProxy.map { it.getPacFileUrl().toString() }.orElse("") + httpProxyMenu = other.httpProxy.map { if (it.getPacFileUrl() != Uri.EMPTY) Constants.HTTP_PROXY_PAC else if (it.getHost() != "") Constants.HTTP_PROXY_MANUAL else Constants.HTTP_PROXY_NONE }.orElse(Constants.HTTP_PROXY_NONE) val keyPair = other.keyPair privateKey = keyPair.privateKey.toBase64() } @@ -111,6 +165,20 @@ class InterfaceProxy : BaseObservable, Parcelable { if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications) if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort) if (mtu.isNotEmpty()) builder.parseMtu(mtu) + if (Constants.HTTP_PROXY_MANUAL.equals(httpProxyMenu) && httpProxyHostname.isNotEmpty()) { + var httpProxy: String + if (httpProxyHostname.contains(":")) { + httpProxy = "[" + httpProxyHostname + "]" + } else { + httpProxy = httpProxyHostname + } + if (httpProxyPort.isNotEmpty()) { + httpProxy += ":" + httpProxyPort; + } + builder.parseHttpProxy(httpProxy) + } else if (Constants.HTTP_PROXY_PAC.equals(httpProxyMenu) && httpProxyPac.isNotEmpty()) { + builder.parseHttpProxy("pac:" + httpProxyPac) + } if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey) return builder.build() } @@ -122,6 +190,10 @@ class InterfaceProxy : BaseObservable, Parcelable { dest.writeStringList(includedApplications) dest.writeString(listenPort) dest.writeString(mtu) + dest.writeString(httpProxyMenu) + dest.writeString(httpProxyHostname) + dest.writeString(httpProxyPort) + dest.writeString(httpProxyPac) dest.writeString(privateKey) } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt new file mode 100644 index 00000000..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..9b17a06c 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.backend.Tunnel.State" /> <import type="com.wireguard.android.util.ClipboardUtils" /> @@ -19,7 +21,7 @@ <variable name="config" - type="com.wireguard.config.Config" /> + type="com.wireguard.android.viewmodel.ConfigDetail" /> </data> <ScrollView @@ -116,7 +118,7 @@ android:nextFocusForward="@id/addresses_text" android:onClick="@{ClipboardUtils::copyTextView}" android:singleLine="true" - android:text="@{config.interface.keyPair.publicKey.toBase64}" + android:text="@{config.config.interface.keyPair.publicKey.toBase64}" android:textAppearance="?attr/textAppearanceBodyLarge" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" @@ -129,7 +131,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/addresses_text" android:text="@string/addresses" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -139,14 +141,41 @@ android:layout_height="wrap_content" android:contentDescription="@string/addresses" android:nextFocusUp="@id/public_key_text" + android:nextFocusDown="@id/dynamic_addresses_text" + android:nextFocusForward="@id/dynamic_addresses_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.addresses}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/addresses_label" + tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> + + <TextView + android:id="@+id/dynamic_addresses_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/dynamic_addresses_text" + android:text="@string/dynamic_addresses" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + + <TextView + android:id="@+id/dynamic_addresses_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/dynamic_addresses" + android:nextFocusUp="@id/addresses_text" android:nextFocusDown="@id/dns_servers_text" android:nextFocusForward="@id/dns_servers_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.addresses}" + android:text="@{tunnel.dhcp}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/addresses_label" + app:layout_constraintTop_toBottomOf="@+id/dynamic_addresses_label" tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> <TextView @@ -156,22 +185,22 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_servers_text" android:text="@string/dns_servers" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + app:layout_constraintTop_toBottomOf="@id/dynamic_addresses_text" /> <TextView android:id="@+id/dns_servers_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/dns_servers" - android:nextFocusUp="@id/addresses_text" + android:nextFocusUp="@id/dynamic_addresses_text" android:nextFocusDown="@id/dns_search_domains_text" android:nextFocusForward="@id/dns_search_domains_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsServers}" + android:text="@{config.config.interface.dnsServers}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" tools:text="8.8.8.8, 8.8.4.4" /> @@ -183,7 +212,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_search_domain_text" android:text="@string/dns_search_domains" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> @@ -196,9 +225,9 @@ android:nextFocusDown="@id/listen_port_text" android:nextFocusForward="@id/listen_port_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsSearchDomains}" + android:text="@{config.config.interface.dnsSearchDomains}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label" tools:text="zx2c4.com" /> @@ -210,7 +239,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/listen_port_text" android:text="@string/listen_port" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -223,12 +252,12 @@ android:contentDescription="@string/listen_port" android:nextFocusRight="@id/mtu_text" android:nextFocusUp="@id/dns_search_domains_text" - android:nextFocusDown="@id/applications_text" + android:nextFocusDown="@id/http_proxy_text" android:nextFocusForward="@id/mtu_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.listenPort}" + android:text="@{config.config.interface.listenPort}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -242,7 +271,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/mtu_text" android:text="@string/mtu" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintLeft_toRightOf="@id/listen_port_label" @@ -256,11 +285,11 @@ android:contentDescription="@string/mtu" android:nextFocusLeft="@id/listen_port_text" android:nextFocusUp="@id/dns_servers_text" - android:nextFocusForward="@id/applications_text" + android:nextFocusForward="@id/http_proxy_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.mtu}" + android:text="@{config.config.interface.mtu}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toEndOf="@id/listen_port_label" @@ -276,15 +305,42 @@ app:constraint_referenced_ids="listen_port_text,mtu_text" /> <TextView + android:id="@+id/http_proxy_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/http_proxy_text" + android:text="@string/http_proxy" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + + <TextView + android:id="@+id/http_proxy_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:contentDescription="@string/http_proxy" + android:nextFocusUp="@id/listen_port_text" + android:nextFocusDown="@id/applications_text" + android:nextFocusForward="@id/applications_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.httpProxy}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintTop_toBottomOf="@id/http_proxy_label" + app:layout_constraintStart_toStartOf="parent" + tools:text="http://example.com:8888" /> + + <TextView android:id="@+id/applications_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:labelFor="@+id/applications_text" android:text="@string/applications" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" /> <TextView android:id="@+id/applications_text" @@ -295,9 +351,9 @@ android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}" + android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/applications_label" tools:text="8 excluded" /> @@ -311,6 +367,7 @@ android:layout_marginTop="8dp" android:divider="@null" android:orientation="vertical" + app:fragment="@{fragment}" app:items="@{config.peers}" app:layout="@{@layout/tunnel_detail_peer}" app:layout_constraintStart_toStartOf="parent" @@ -318,4 +375,4 @@ tools:ignore="UselessLeaf" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> -</layout>
\ No newline at end of file +</layout> diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index 25081cea..89bb85ec 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -9,7 +9,7 @@ <variable name="item" - type="com.wireguard.config.Peer" /> + type="com.wireguard.android.viewmodel.PeerDetail" /> </data> <com.google.android.material.card.MaterialCardView @@ -64,7 +64,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/pre_shared_key_text" android:text="@string/pre_shared_key" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -81,7 +81,7 @@ android:singleLine="true" android:text="@string/pre_shared_key_enabled" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label" tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" /> diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml index 0350486b..42222399 100644 --- a/ui/src/main/res/layout/tunnel_editor_fragment.xml +++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.util.ClipboardUtils" /> <import type="com.wireguard.android.widget.KeyInputFilter" /> @@ -210,7 +212,7 @@ android:imeOptions="actionNext" android:inputType="textNoSuggestions|textVisiblePassword" android:nextFocusUp="@id/addresses_label_text" - android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" android:nextFocusForward="@id/mtu_text" android:text="@={config.interface.dnsServers}" /> </com.google.android.material.textfield.TextInputLayout> @@ -235,19 +237,112 @@ android:imeOptions="actionDone" android:inputType="number" android:nextFocusUp="@id/listen_port_text" - android:nextFocusDown="@id/set_excluded_applications" - android:nextFocusForward="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" + android:nextFocusForward="@id/http_proxy_hostname_text" android:text="@={config.interface.mtu}" android:textAlignment="center" /> </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_menu" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dns_servers_label_layout"> + + <AutoCompleteTextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="none" + android:text="@={config.interface.httpProxyMenu}" + /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_hostname_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_hostname" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:layout_constraintEnd_toStartOf="@id/http_proxy_port_label_layout" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintHorizontal_weight="0.7" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_hostname_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/http_proxy_port_text" + android:text="@={config.interface.httpProxyHostname}" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_port_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_port" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_weight="0.3" + app:layout_constraintStart_toEndOf="@id/http_proxy_hostname_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_port_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/http_proxy_pac_label_layout" + android:nextFocusForward="@id/http_proxy_pac_label_layout" + android:text="@={config.interface.httpProxyPort}" + android:textAlignment="center" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_pac_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_pac" + android:visibility="@{config.interface.httpProxyPacVisibility}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_hostname_label_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_pac_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/http_proxy_hostname_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/set_excluded_applications" + android:text="@={config.interface.httpProxyPac}" /> + </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.button.MaterialButton android:id="@+id/set_excluded_applications" style="@style/Widget.Material3.Button.TextButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" - android:nextFocusUp="@id/dns_servers_text" + android:nextFocusUp="@id/http_proxy_hostname_text" android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}" @@ -256,7 +351,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/mtu_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_pac_label_layout" app:rippleColor="?attr/colorSecondary" tools:text="4 excluded applications" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/ui/src/main/res/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-ar-rSA/strings.xml b/ui/src/main/res/values-ar-rSA/strings.xml index 32bf386f..73f6111a 100644 --- a/ui/src/main/res/values-ar-rSA/strings.xml +++ b/ui/src/main/res/values-ar-rSA/strings.xml @@ -1,5 +1,13 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + <plurals name="delete_error"> + <item quantity="zero">غير قادر على حذف %d النفق: %s</item> + <item quantity="one">غير قادر على حذف %d النفق: %s</item> + <item quantity="two">غير قادر على حذف %d النفق: %s</item> + <item quantity="few">غير قادر على حذف %d أنفاق: %s</item> + <item quantity="many">غير قادر على حذف %d أنفاق: %s</item> + <item quantity="other">غير قادر على حذف %d أنفاق: %s</item> + </plurals> <plurals name="delete_success"> <item quantity="zero">تم حذف %d من الأنفاق بنجاح</item> <item quantity="one">تم حذف %d من الأنفاق بنجاح</item> @@ -24,6 +32,30 @@ <item quantity="many">تم استيراد %1$d من %2$d من الأنفاق</item> <item quantity="other">تم استيراد %1$d من %2$d من الأنفاق</item> </plurals> + <plurals name="import_total_success"> + <item quantity="zero">تم استيراد %d نفق</item> + <item quantity="one">تم استيراد %d نفق</item> + <item quantity="two">تم استيراد نفقين</item> + <item quantity="few">تم استيراد %d أنفاق</item> + <item quantity="many">تم استيراد %d أنفاق</item> + <item quantity="other">تم استيراد %d أنفاق</item> + </plurals> + <plurals name="set_excluded_applications"> + <item quantity="zero">%d تطبيقات مستبعدة</item> + <item quantity="one">%d تطبيق مستبعد</item> + <item quantity="two">تطبيقين مستبعدين</item> + <item quantity="few">%d تطبيقات مستبعدة</item> + <item quantity="many">%d تطبيقات مستبعدة</item> + <item quantity="other">%d تطبيقات مستبعدة</item> + </plurals> + <plurals name="set_included_applications"> + <item quantity="zero">%d تطبيق مضمن</item> + <item quantity="one">%d تطبيق مضمن</item> + <item quantity="two">تطبيقين مضمننين</item> + <item quantity="few">%d تطبيقات مضمنة</item> + <item quantity="many">%d تطبيقات مضمنة</item> + <item quantity="other">%d تطبيقات مضمنة</item> + </plurals> <plurals name="n_excluded_applications"> <item quantity="zero">تم استبعاد %d</item> <item quantity="one">تم استبعاد %d</item> @@ -51,6 +83,14 @@ <item quantity="many">تضمين %d من التطبيقات</item> <item quantity="other">تضمين %d من التطبيقات</item> </plurals> + <plurals name="exclude_n_applications"> + <item quantity="zero">استبعاد %d تطبيقات</item> + <item quantity="one">استبعاد %d تطبيقات</item> + <item quantity="two">استبعاد طبيقين</item> + <item quantity="few">استبعاد %d تطبيقات</item> + <item quantity="many">استبعاد %d تطبيقات</item> + <item quantity="other">استبعاد %d تطبيقات</item> + </plurals> <plurals name="persistent_keepalive_seconds_unit"> <item quantity="zero">كل %d ثانية</item> <item quantity="one">كل ثانية</item> @@ -59,6 +99,14 @@ <item quantity="many">كل %d ثوانٍ</item> <item quantity="other">كل %d ثوانٍ</item> </plurals> + <plurals name="persistent_keepalive_seconds_suffix"> + <item quantity="zero">لا ثانية</item> + <item quantity="one">ثانية</item> + <item quantity="two">ثنيتين</item> + <item quantity="few">ثواني</item> + <item quantity="many">ثواني</item> + <item quantity="other">ثوان</item> + </plurals> <string name="use_all_applications">استخدام جميع التطبيقات</string> <string name="add_peer">إضافة ند</string> <string name="addresses">العناوين</string> @@ -113,6 +161,7 @@ <string name="tv_add_tunnel_get_started">أضف نفقاً لتبدأ</string> <string name="donate_title">♥️ تبرع لمشروع وايرجارد</string> <string name="donate_summary">كل مساهمة تساعد</string> + <string name="donate_google_play_disappointment">شكرًا لك على دعم مشروع\n\nWireGuard! للأسف، نظرًا لسياسات Google، لا يُسمح لنا بالارتباط بجزء من صفحة الويب الخاصة بالمشروع حيث يمكنك التبرع. نأمل أن تتمكن من معرفة ذلك!\n\nشكرًا مرة أخرى على مساهمتك.</string> <string name="disable_config_export_title">تعطيل تصدير التكوين</string> <string name="disable_config_export_description">تعطيل تصدير الإعدادات يجعل المفاتيح الخاصة غير متاحة</string> <string name="dns_servers">خوادم DNS</string> @@ -142,6 +191,8 @@ <string name="key_length_explanation_base64">: مفاتيح وايرجارد في الأساس 64 (base64) يجب أن تكون 44 حرفاً (32 بايت)</string> <string name="key_length_explanation_binary">: مفاتيح وايرجارد يجب أن تكون 32 بايت</string> <string name="key_length_explanation_hex">: مفاتيح وايرجارد في نظام عد ستة عشري (hexadecimal) يجب أن تكون 64 حرفاً (32 بايت)</string> + <string name="latest_handshake">أحدث مصافحة</string> + <string name="latest_handshake_ago">منذ %s</string> <string name="listen_port">منفذ الاستماع</string> <string name="log_export_error">غير قادر على تصدير السجل: %s</string> <string name="log_export_subject">ملف سجل اندرويد وايرجارد</string> @@ -177,6 +228,7 @@ <string name="parse_error_integer">رقم</string> <string name="parse_error_reason">لا يمكن تحليل %1$s \"%2$s\"</string> <string name="peer">ند</string> + <string name="permission_description">التحكم في أنفاق WireGuard، وتمكين الأنفاق وتعطيلها حسب الرغبة، مما قد يؤدي إلى تضليل حركة مرور الإنترنت</string> <string name="permission_label">التحكم في أنفاق وايرجارد</string> <string name="persistent_keepalive">الحفاظ المستمر</string> <string name="pre_shared_key">مفتاح مسبق التشارك (Pre-shared key)</string> @@ -184,6 +236,10 @@ <string name="private_key">مفتاح خاص</string> <string name="public_key">مفتاح عام</string> <string name="qr_code_hint">نصيحة: إنشاء بواسطة `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">إضافة بلاطة إلى لوحة الإعدادات السريعة</string> + <string name="quick_settings_tile_add_summary">بلاطة الاختصار تستبدل أحدث نفق</string> + <string name="quick_settings_tile_add_failure">تعذر إضافة لوحة الاختصار: خطأ %d</string> + <string name="quick_settings_tile_action">تبديل النفق</string> <string name="restore_on_boot_summary_off">لن تجلب الأنفاق المفعلة عند التمهيد</string> <string name="restore_on_boot_summary_on">سوف تجلب الأنفاق المفعلة عند التمهيد</string> <string name="restore_on_boot_title">الإستعادة عند تشغيل الجهاز</string> @@ -211,6 +267,7 @@ <string name="transfer_gibibytes">%.2f جيبي بايت</string> <string name="transfer_kibibytes">%.2f كيبيبايت</string> <string name="transfer_mibibytes">%.2f مبيبايت</string> + <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string> <string name="transfer_tibibytes">%.2f تيبي بايت</string> <string name="tun_create_error">غير قادر على إنشاء جهاز tun</string> <string name="tunnel_config_error">غير قادر على تكوين النفق (wg-quick أعاد %d)</string> @@ -224,8 +281,19 @@ <string name="tunnel_dns_failure">غير قادر على حل اسم مضيف نظام أسماء النطاقات (DNS hostname): \"%s\"</string> <string name="tunnel_rename_error">غير قادر على إعادة تسمية النفق: %s</string> <string name="tunnel_rename_success">تمت إعادة تسمية النفق بنجاح إلى \"%s\"</string> + <string name="type_name_go_userspace">انتقل مساحة المستخدمين</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="updater_corrupt_title">التطبيق تالف</string> + <string name="updater_corrupt_message">هذا التطبيق تالف. يرجى إعادة تنزيل APK من موقع الويب المرتبط أدناه. بعد ذلك، قم بإلغاء تثبيت هذا التطبيق، وأعِد تثبيته من ملف APK الذي تم تنزيله.</string> + <string name="updater_corrupt_navigate">فتح الموقع</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-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml index 98f8de6e..bf786c07 100644 --- a/ui/src/main/res/values-da-rDK/strings.xml +++ b/ui/src/main/res/values-da-rDK/strings.xml @@ -20,6 +20,14 @@ <item quantity="one">Importeret %d tunnel</item> <item quantity="other">Importeret %d tunneler</item> </plurals> + <plurals name="set_excluded_applications"> + <item quantity="one">%d Ekskluderet Applikation</item> + <item quantity="other">%d Ekskluderet Applikationer</item> + </plurals> + <plurals name="set_included_applications"> + <item quantity="one">%d Inkluderet Applikation</item> + <item quantity="other">%d Inkluderet Applikationer</item> + </plurals> <plurals name="n_excluded_applications"> <item quantity="one">%d ekskluderet</item> <item quantity="other">%d ekskluderet</item> @@ -51,6 +59,9 @@ <string name="add_peer">Tilføj modpart</string> <string name="addresses">Adresser</string> <string name="applications">Applikationer</string> + <string name="allow_remote_control_intents_summary_off">Eksterne apps kan ikke slå tunneler til/fra (Anbefales)</string> + <string name="allow_remote_control_intents_summary_on">Eksterne apps må slå tunneler til/fra (Avanceret)</string> + <string name="allow_remote_control_intents_title">Tillad fjernstyring fra eksterne apps</string> <string name="allowed_ips">Tilladte IP-adresser</string> <string name="bad_config_context">%1$s\'s %2$s</string> <string name="bad_config_context_top_level">%s</string> @@ -80,11 +91,14 @@ <string name="create_from_qr_code">Scan fra QR-kode</string> <string name="create_tunnel">Opret Tunnel</string> <string name="copied_to_clipboard">%s kopieret til udklipsholder</string> + <string name="dark_theme_summary_off">Bruger lige nu lyst (dag) tema</string> + <string name="dark_theme_summary_on">Bruger lige nu mørkt (nat) tema</string> <string name="dark_theme_title">Brug mørkt tema</string> <string name="delete">Slet</string> <string name="tv_delete">Vælg tunnel du vil slette</string> <string name="tv_select_a_storage_drive">Vælg et lagerdrev</string> <string name="tv_add_tunnel_get_started">Tilføj en tunnel for at komme i gang</string> + <string name="donate_title">♥ Donér til WireGuard projektet</string> <string name="disable_config_export_title">Deaktivér eksportering af konfiguration</string> <string name="dns_servers">DNS-servere</string> <string name="dns_search_domains">DNS-søgedomæner</string> @@ -107,6 +121,7 @@ <string name="interface_title">Grænseflade</string> <string name="key_contents_error">Ugyldige tegn i nøgle</string> <string name="key_length_error">Forkert længde på nøgle</string> + <string name="latest_handshake_ago">%s siden</string> <string name="listen_port">Lytteport</string> <string name="log_export_error">Kunne ikke eksportere log: %s</string> <string name="log_export_subject">WireGuard Android Log-fil</string> @@ -162,6 +177,11 @@ <string name="tunnel_rename_success">Tunnel blev succesfuldt omdøbt til \"%s\"</string> <string name="type_name_kernel_module">Kerne modul</string> <string name="unknown_error">Ukendt fejl</string> + <string name="updater_avalable">En applikationsopdatering er tilgængelig. Opdatér venligst nu.</string> + <string name="updater_action">Hent & Opdatér</string> + <string name="updater_download_progress">Henter opdatering: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Henter opdatering: %s</string> + <string name="updater_installing">Installerer opdatering…</string> <string name="version_summary_unknown">Ukendt %s version</string> <string name="version_title">WireGuard for Android v%s</string> <string name="zip_export_error">Ikke i stand til at eksportere tunneler: %s</string> diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml index 696fde62..372abcad 100644 --- a/ui/src/main/res/values-de/strings.xml +++ b/ui/src/main/res/values-de/strings.xml @@ -99,8 +99,8 @@ <string name="create_temp_dir_error">Lokales temporäres Verzeichnis kann nicht erstellt werden</string> <string name="create_tunnel">Tunnel erstellen</string> <string name="copied_to_clipboard">%s in die Zwischenanlage kopiert</string> - <string name="dark_theme_summary_off">Verwende helles Design (Tag)</string> - <string name="dark_theme_summary_on">Verwende dunkles Design (Nacht)</string> + <string name="dark_theme_summary_off">Helles (Tag) Design in Verwendung</string> + <string name="dark_theme_summary_on">Dunkles (Nacht) Design in Verwendung</string> <string name="dark_theme_title">Dunkles Design verwenden</string> <string name="delete">Entfernen</string> <string name="tv_delete">Wählen Sie den zu löschenden Tunnel aus</string> @@ -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> @@ -181,6 +184,10 @@ <string name="private_key">Privater Schlüssel</string> <string name="public_key">Öffentlicher Schlüssel</string> <string name="qr_code_hint">Tipp: Mit `qrencode -t ansiutf8 < tunnel.conf` generieren.</string> + <string name="quick_settings_tile_add_title">Kachel zu Schnelleinstellungen hinzufügen</string> + <string name="quick_settings_tile_add_summary">Die Verknüpfung schaltet den letzten Tunnel um</string> + <string name="quick_settings_tile_add_failure">Verknüpfung kann nicht hinzugefügt werden: Fehler %d</string> + <string name="quick_settings_tile_action">Tunnel umschalten</string> <string name="restore_on_boot_summary_off">Aktivierte Tunnel beim Systemstart nicht automatisch starten</string> <string name="restore_on_boot_summary_on">Aktivierte Tunnel beim Systemstart automatisch wieder starten</string> <string name="restore_on_boot_title">Beim Neustart wiederherstellen</string> @@ -195,14 +202,14 @@ <string name="toggle_error">Fehler beim Umschalten des WireGuard-Tunnels: %s</string> <string name="tools_installer_already">wg und wg-quick sind bereits installiert</string> <string name="tools_installer_failure">Kommandozeilenwerkzeuge konnten nicht installiert werden (kein Root?)</string> - <string name="tools_installer_initial">Optionale Tools für Skripte installieren</string> + <string name="tools_installer_initial">Optionale Werkzeuge für Skripte installieren</string> <string name="tools_installer_initial_magisk">Optionale Werkzeuge für das Skripten als Magisk-Modul installieren</string> <string name="tools_installer_initial_system">Optionale Werkzeuge für Skripte auf der Systempartition installieren</string> <string name="tools_installer_success_magisk">wg und wg-quick als Magisk-Modul installiert (Neustart erforderlich)</string> <string name="tools_installer_success_system">wg und wg-quick auf der Systempartition installiert</string> <string name="tools_installer_title">Kommandozeilenwerkzeuge installieren</string> <string name="tools_installer_working">Installiere wg und wg-quick</string> - <string name="tools_unavailable_error">Erforderliche Tools nicht verfügbar</string> + <string name="tools_unavailable_error">Erforderliche Werkzeuge nicht verfügbar</string> <string name="transfer">Transfer</string> <string name="transfer_bytes">%d B</string> <string name="transfer_gibibytes">%.2f GiB</string> @@ -216,6 +223,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 +232,16 @@ <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 & 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="updater_corrupt_title">Anwendung beschädigt</string> + <string name="updater_corrupt_message">Diese Anwendung ist beschädigt. Bitte laden Sie die APK erneut von der unten verlinkten Website herunter. Deinstallieren Sie danach diese Anwendung und installieren Sie sie mit der heruntergeladenen APK neu.</string> + <string name="updater_corrupt_navigate">Webseite öffnen</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..9beaafd9 100644 --- a/ui/src/main/res/values-et-rEE/strings.xml +++ b/ui/src/main/res/values-et-rEE/strings.xml @@ -185,6 +185,7 @@ Aitäh veelkord sinu panuse eest.</string> <string name="private_key">Privaatvõti</string> <string name="public_key">Avalik võti</string> <string name="qr_code_hint">Vihje: tekita käsuga `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_action">Lülita tunnel</string> <string name="restore_on_boot_summary_off">Ei lülita seadme käivitumisel lubatud tunneleid sisse</string> <string name="restore_on_boot_summary_on">Lülitab seadme käivitumisel lubatud tunnelid sisse</string> <string name="restore_on_boot_title">Taasta seadme käivitumisel</string> @@ -229,6 +230,16 @@ 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="updater_corrupt_title">Rakendus rikutud</string> + <string name="updater_corrupt_message">See rakendus on rikutud. Palun laadi APK uuesti allpool lingitud veebilehelt. Pärast seda desinstalli rakendus ja installi allalaaditud APK uuesti.</string> + <string name="updater_corrupt_navigate">Ava veebileht</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-fa-rIR/strings.xml b/ui/src/main/res/values-fa-rIR/strings.xml index 41712608..e9000214 100644 --- a/ui/src/main/res/values-fa-rIR/strings.xml +++ b/ui/src/main/res/values-fa-rIR/strings.xml @@ -99,7 +99,7 @@ <string name="create_output_dir_error">نمیتوان دایرکتوری خروجی را ایجاد کرد</string> <string name="create_temp_dir_error">نمیتوان دایرکتوری موقت محلی را ساخت</string> <string name="create_tunnel">ساختن تونل</string> - <string name="copied_to_clipboard">متن در کلیپبورد کپی شد</string> + <string name="copied_to_clipboard">%s در کلیپبورد کپی شد</string> <string name="dark_theme_summary_off">اکنون از پوسته روشن(روز) استفاده میشود</string> <string name="dark_theme_summary_on">اکنون از پوسته تاریک(شب) استفاده میشود</string> <string name="dark_theme_title">استفاده از پوسته تاریک</string> diff --git a/ui/src/main/res/values-fr/strings.xml b/ui/src/main/res/values-fr/strings.xml index 225e0770..d4d4b95f 100644 --- a/ui/src/main/res/values-fr/strings.xml +++ b/ui/src/main/res/values-fr/strings.xml @@ -6,7 +6,7 @@ </plurals> <plurals name="delete_success"> <item quantity="one">Suppression réussie du tunnel %d</item> - <item quantity="other">Supprimé avec succès %d tunnels</item> + <item quantity="other">%d tunnels supprimés avec succès</item> </plurals> <plurals name="delete_title"> <item quantity="one">%d tunnel sélectionné</item> @@ -85,16 +85,16 @@ <string name="config_delete_error">Impossible de supprimer le fichier de configuration %s</string> <string name="config_exists_error">La configuration de « %s » existe déjà</string> <string name="config_file_exists_error">Le fichier de configuration « %s » existe déjà</string> - <string name="config_not_found_error">Fichier de configuration «%s» introuvable</string> - <string name="config_rename_error">Impossible de renommer le fichier de configuration «%s»</string> - <string name="config_save_error">Impossible d’enregistrer la configuration pour «%1$s» : %2$s</string> - <string name="config_save_success">Configuration enregistrée avec succès pour “%s”</string> + <string name="config_not_found_error">Fichier de configuration « %s » introuvable</string> + <string name="config_rename_error">Impossible de renommer le fichier de configuration « %s »</string> + <string name="config_save_error">Impossible d’enregistrer la configuration pour « %1$s » : %2$s</string> + <string name="config_save_success">Configuration enregistrée avec succès pour « %s »</string> <string name="create_activity_title">Créer un tunnel WireGuard</string> <string name="create_bin_dir_error">Impossible de créer le répertoire binaire local</string> <string name="create_downloads_file_error">Impossible de créer le fichier dans le répertoire des téléchargements</string> <string name="create_empty">Créer à partir de zéro</string> <string name="create_from_file">Importer depuis un fichier ou une archive</string> - <string name="create_from_qr_code">Créer avec un scan de QR code</string> + <string name="create_from_qr_code">Importer depuis un QR code</string> <string name="create_output_dir_error">Impossible de créer le répertoire de sortie</string> <string name="create_temp_dir_error">Impossible de créer le répertoire temporaire local</string> <string name="create_tunnel">Créer un tunnel</string> @@ -109,6 +109,7 @@ <string name="tv_add_tunnel_get_started">Ajouter un tunnel pour commencer</string> <string name="donate_title">♥ Faire un don au projet WireGuard</string> <string name="donate_summary">Chaque contribution aide</string> + <string name="donate_google_play_disappointment">Merci de votre soutien au projet WireGuard !\n\nMalheureusement, en raisons des politiques de Google, nous ne pouvons pas vous rediriger vers la page vous permettant de faire un don. Heureusement, vous pouvez le trouver par vous-même !\n\nMerci encore pour votre soutien.</string> <string name="disable_config_export_title">Désactiver l\'export de configuration</string> <string name="disable_config_export_description">La désactivation de l\'export de configuration rend les clés privées moins accessibles</string> <string name="dns_servers">Serveurs DNS</string> @@ -118,6 +119,7 @@ <string name="error_down">Erreur lors de la désactivation du tunnel : %s</string> <string name="error_fetching_apps">Erreur lors de la récupération de la liste d\'applications : %s</string> <string name="error_root">Veuillez obtenir l\'accès root et essayez à nouveau</string> + <string name="error_prepare">Erreur lors de la préparation du tunnel : %s</string> <string name="error_up">Erreur lors de la mise en place du tunnel : %s</string> <string name="exclude_private_ips">Exclure les IPs privées</string> <string name="generate_new_private_key">Générer une nouvelle clé privée</string> @@ -137,6 +139,8 @@ <string name="key_length_explanation_base64">: Les clés base64 WireGuard doivent comporter 44 caractères (32 octets)</string> <string name="key_length_explanation_binary">: Les clés WireGuard doivent comporter 32 octets</string> <string name="key_length_explanation_hex">: Les clés hexadécimales WireGuard doivent comporter 64 caractères (32 octets)</string> + <string name="latest_handshake">Dernière liaison</string> + <string name="latest_handshake_ago">Il y a %s</string> <string name="listen_port">Port d\'écoute</string> <string name="log_export_error">Impossible d\'exporter le journal : %s</string> <string name="log_export_subject">Fichier journal d\'Android WireGuard</string> @@ -180,6 +184,10 @@ <string name="private_key">Clé privée</string> <string name="public_key">Clé publique</string> <string name="qr_code_hint">Astuce : générez avec \"qrencode -t ansiutf8 < tunnel.conf\".</string> + <string name="quick_settings_tile_add_title">Ajouter une bascule au volet des paramètres</string> + <string name="quick_settings_tile_add_summary">Cette bascule active le dernier tunnel utilisé</string> + <string name="quick_settings_tile_add_failure">Impossible d\'ajouter la bascule : erreur %d</string> + <string name="quick_settings_tile_action">Activer le tunnel</string> <string name="restore_on_boot_summary_off">N\'affichera pas les tunnels activés au démarrage</string> <string name="restore_on_boot_summary_on">Les tunnels activés seront affichés au démarrage</string> <string name="restore_on_boot_title">Restaurer au démarrage</string> @@ -224,6 +232,16 @@ <string name="type_name_go_userspace">Implémentation Go en espace utilisateur</string> <string name="type_name_kernel_module">Module noyau</string> <string name="unknown_error">Erreur inconnue</string> + <string name="updater_avalable">Une mise à jour est disponible. Veuillez mettre l\'application à jour.</string> + <string name="updater_action">Télécharger & Mettre à jour</string> + <string name="updater_rechecking">Récupération des métadonnées de la mise à jour…</string> + <string name="updater_download_progress">Téléchargement de la mise à jour : %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Téléchargement de la mise à jour : %s</string> + <string name="updater_installing">Installation de la mise à jour…</string> + <string name="updater_failure">Erreur lors de la mise à jour : %s. Nous réessaierons dans un instant…</string> + <string name="updater_corrupt_title">Application corrompue</string> + <string name="updater_corrupt_message">Cette application est corrompue. Veuillez réinstaller le fichier APK depuis le site ci-dessous. Ensuite, désinstallez cette application puis réinstallez-la à l\'aide du fichier APK téléchargé.</string> + <string name="updater_corrupt_navigate">Accéder au site internet</string> <string name="version_summary">%1$s backend %2$s</string> <string name="version_summary_checking">Vérification de la version %s du backend</string> <string name="version_summary_unknown">Version %s inconnue</string> diff --git a/ui/src/main/res/values-hu-rHU/strings.xml b/ui/src/main/res/values-hu-rHU/strings.xml new file mode 100644 index 00000000..bdc19fe3 --- /dev/null +++ b/ui/src/main/res/values-hu-rHU/strings.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="all_applications">Minden alkalmazás</string> + <string name="exclude_from_tunnel">Kizárás</string> + <string name="addresses">Címek</string> + <string name="applications">Alkalmazások</string> + <string name="allowed_ips">Engedélyezett IP-k</string> + <string name="bad_config_reason_invalid_key">Érvénytelen kulcs</string> + <string name="bad_config_reason_invalid_value">Helytelen érték</string> + <string name="bad_config_reason_syntax_error">Szintaktikai hiba</string> + <string name="bad_config_reason_unknown_section">Ismeretlen szekció</string> + <string name="bad_config_reason_value_out_of_range">Az érték a megengedett tartományon kívül van</string> + <string name="bad_extension_error">A fájl .conf vagy .zip legyen</string> + <string name="config_not_found_error">Konfigurációs állomány \"%s\" nem található meg</string> + <string name="dns_servers">DNS szerverek</string> + <string name="dns_search_domains">Domain keresés</string> + <string name="edit">Szerkesztés</string> + <string name="endpoint">Végpont</string> + <string name="log_saver_activity_label">Log mentése</string> + <string name="log_viewer_pref_title">Alkalmazás log megtekintése</string> + <string name="name">Név</string> + <string name="parse_error_inet_address">IP cím</string> + <string name="parse_error_inet_network">IP hálózat</string> + <string name="parse_error_integer">szám</string> + <string name="pre_shared_key_enabled">engedélyezve</string> + <string name="public_key">Nyilvános kulcs</string> + <string name="quick_settings_tile_action">Csatorna átkapcsolása</string> + <string name="save">Mentés</string> + <string name="select_all">Összes kijelölése</string> + <string name="settings">Beállítások</string> + <string name="toggle_all">Összes átkapcsolása</string> + <string name="tunnel_name">Csatorna neve</string> + <string name="type_name_kernel_module">Kernel modul</string> + <string name="unknown_error">Ismeretlen hiba</string> + <string name="updater_corrupt_navigate">Weboldal megnyitása</string> +</resources> diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml index e441133f..8fba2592 100644 --- a/ui/src/main/res/values-it/strings.xml +++ b/ui/src/main/res/values-it/strings.xml @@ -59,9 +59,9 @@ <string name="add_peer">Aggiungi peer</string> <string name="addresses">Indirizzi</string> <string name="applications">Applicazioni</string> - <string name="allow_remote_control_intents_summary_off">Le applicazioni esterne non possono attivare tunnel (consigliato)</string> - <string name="allow_remote_control_intents_summary_on">Le applicazioni esterne possono attivare tunnel (avanzato)</string> - <string name="allow_remote_control_intents_title">Consenti applicazioni di controllo remoto</string> + <string name="allow_remote_control_intents_summary_off">Le app esterne non possono attivare tunnel (consigliato)</string> + <string name="allow_remote_control_intents_summary_on">Le app esterne possono attivare tunnel (avanzato)</string> + <string name="allow_remote_control_intents_title">Consenti app di controllo remoto</string> <string name="allowed_ips">IP consentiti</string> <string name="bad_config_context">%2$s di %1$s</string> <string name="bad_config_context_top_level">%s</string> @@ -94,7 +94,7 @@ <string name="create_downloads_file_error">Impossibile creare il file nella cartella di download</string> <string name="create_empty">Crea da zero</string> <string name="create_from_file">Importa da file o archivio</string> - <string name="create_from_qr_code">Scansione da codice QR</string> + <string name="create_from_qr_code">Scansiona da codice QR</string> <string name="create_output_dir_error">Impossibile creare la cartella di output</string> <string name="create_temp_dir_error">Impossibile creare la cartella locale temporanea</string> <string name="create_tunnel">Crea tunnel</string> @@ -107,6 +107,9 @@ <string name="tv_select_a_storage_drive">Seleziona un\'unità di archiviazione</string> <string name="tv_no_file_picker">Installa un\'utilità di gestione file per sfogliare i file</string> <string name="tv_add_tunnel_get_started">Aggiungi un tunnel per iniziare</string> + <string name="donate_title">♥ Dona al progetto WireGuard</string> + <string name="donate_summary">Ogni contributo aiuta</string> + <string name="donate_google_play_disappointment">Grazie per il sostegno al progetto WireGuard!\n\nPurtroppo, a causa delle politiche di Google, non siamo autorizzati a linkare la pagina del progetto dove puoi fare una donazione. Speriamo che la troverai!\n\nGrazie ancora per il tuo contributo.</string> <string name="disable_config_export_title">Disattiva esportazione config</string> <string name="disable_config_export_description">Disabilitare l\'esportazione della configurazione rende le chiavi private meno accessibili</string> <string name="dns_servers">Server DNS</string> @@ -116,13 +119,14 @@ <string name="error_down">Errore di disattivazione del tunnel: %s</string> <string name="error_fetching_apps">Errore di recupero dell\'elenco applicazioni: %s</string> <string name="error_root">Accedi come root e riprova</string> + <string name="error_prepare">Errore di preparazione del tunnel: %s</string> <string name="error_up">Errore di attivazione del tunnel: %s</string> <string name="exclude_private_ips">Escludi IP privati</string> <string name="generate_new_private_key">Genera nuova chiave privata</string> <string name="generic_error">Errore “%s” sconosciuto</string> <string name="hint_automatic">(auto)</string> - <string name="hint_generated">(generato)</string> - <string name="hint_optional">(facoltativo)</string> + <string name="hint_generated">(generata)</string> + <string name="hint_optional">(facoltativa)</string> <string name="hint_optional_discouraged">(facoltativo, non consigliato)</string> <string name="hint_random">(casuale)</string> <string name="illegal_filename_error">Nome file “%s” non valido</string> @@ -135,6 +139,8 @@ <string name="key_length_explanation_base64">: le chiavi base64 di WireGuard devono essere di 44 caratteri (32 byte)</string> <string name="key_length_explanation_binary">: le chiavi di WireGuard devono essere di 32 byte</string> <string name="key_length_explanation_hex">: le chiavi esadecimali di WireGuard devono essere di 64 caratteri (32 byte)</string> + <string name="latest_handshake">Ultima negoziazione</string> + <string name="latest_handshake_ago">%s fa</string> <string name="listen_port">Porta in ascolto</string> <string name="log_export_error">Impossibile esportare il log: %s</string> <string name="log_export_subject">File di log WireGuard Android</string> @@ -178,6 +184,10 @@ <string name="private_key">Chiave privata</string> <string name="public_key">Chiave pubblica</string> <string name="qr_code_hint">Suggerimento: genera con `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">Aggiungi riquadro ale impostazioni rapide</string> + <string name="quick_settings_tile_add_summary">La scorciatoia attiva/disattiva il tunnel più recente</string> + <string name="quick_settings_tile_add_failure">Impossibile aggiungere la scorciatoia: errore %d</string> + <string name="quick_settings_tile_action">Attiva/disattiva tunnel</string> <string name="restore_on_boot_summary_off">Non attiverà i tunnel configurati all\'avvio</string> <string name="restore_on_boot_summary_on">Attiverà i tunnel configurati all\'avvio</string> <string name="restore_on_boot_title">Ripristina all\'avvio</string> @@ -213,6 +223,7 @@ <string name="tunnel_create_success">Tunnel “%s” creato correttamente</string> <string name="tunnel_error_already_exists">Il tunnel “%s” esiste già</string> <string name="tunnel_error_invalid_name">Nome non valido</string> + <string name="tunnel_list_placeholder">Aggiungi un tunnel usando il pulsante sotto</string> <string name="tunnel_name">Nome tunnel</string> <string name="tunnel_on_error">Impossibile attivare il tunnel (wgTurnOn ha risposto %d)</string> <string name="tunnel_dns_failure">Impossibile risolve il nome di domino: \"%s\"</string> @@ -221,6 +232,16 @@ <string name="type_name_go_userspace">Spazio utente Go</string> <string name="type_name_kernel_module">Modulo kernel</string> <string name="unknown_error">Errore sconosciuto</string> + <string name="updater_avalable">È disponibile un aggiornamento dell\'app. Si prega di aggiornare ora.</string> + <string name="updater_action">Scarica e aggiorna</string> + <string name="updater_rechecking">Recupero metadati aggiornamento…</string> + <string name="updater_download_progress">Scaricamento aggiornamento: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Scaricamento aggiornamento: %s</string> + <string name="updater_installing">Installazione aggiornamento…</string> + <string name="updater_failure">Aggiornamento fallito: %s. Riprovo momentaneamente…</string> + <string name="updater_corrupt_title">Applicazione danneggiata</string> + <string name="updater_corrupt_message">Questa applicazione è danneggiata. Riscarica l\'APK dal sito collegato qui sotto. Dopo, disinstalla questa applicazione e reinstallala dall\'APK scaricato.</string> + <string name="updater_corrupt_navigate">Apri sito web</string> <string name="version_summary">Backend %1$s %2$s</string> <string name="version_summary_checking">Controllo versione backend %s</string> <string name="version_summary_unknown">Versione %s sconosciuta</string> diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml index c320d476..26e99af3 100644 --- a/ui/src/main/res/values-ja/strings.xml +++ b/ui/src/main/res/values-ja/strings.xml @@ -96,6 +96,7 @@ <string name="tv_add_tunnel_get_started">トンネルを追加して開始する</string> <string name="donate_title">♥ WireGuard プロジェクトに寄付する</string> <string name="donate_summary">すべての貢献が役立ちます</string> + <string name="donate_google_play_disappointment">WireGuard プロジェクトを支援していただきありがとうございます!\n\n残念ながら、Google のポリシーの影響で寄付のページへのリンクを記載することができません。見つけていただけることを願っています。\n\nもう一度、あなたの貢献に深く感謝します。</string> <string name="disable_config_export_title">設定のエクスポートを無効にする</string> <string name="disable_config_export_description">設定のエクスポートを無効にすると、秘密鍵にアクセスされにくくなります</string> <string name="dns_servers">DNS サーバ</string> @@ -170,6 +171,10 @@ <string name="private_key">秘密鍵</string> <string name="public_key">公開鍵</string> <string name="qr_code_hint">Tip: `qrencode -t ansiutf8 < tunnel.conf` で生成できます</string> + <string name="quick_settings_tile_add_title">クイック設定パネルを追加</string> + <string name="quick_settings_tile_add_summary">ショートカットタイルを使用すると、最新のトンネルに切り替わります</string> + <string name="quick_settings_tile_add_failure">ショートカットタイルを追加できません: エラー %d</string> + <string name="quick_settings_tile_action">トンネルを切り替え</string> <string name="restore_on_boot_summary_off">起動時にトンネルを有効化しない</string> <string name="restore_on_boot_summary_on">起動時に、前回有効だったトンネルを有効化する</string> <string name="restore_on_boot_title">起動時に復元</string> @@ -214,6 +219,16 @@ <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="updater_corrupt_title">アプリケーションが破損しています</string> + <string name="updater_corrupt_message">このアプリケーションは破損しています。下記のリンク先のウェブサイトから APK を再ダウンロードしてください。その後、このアプリケーションをアンインストールし、ダウンロードした APK を再インストールしてください。</string> + <string name="updater_corrupt_navigate">ウェブサイトを開く</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-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-nl-rNL/strings.xml b/ui/src/main/res/values-nl-rNL/strings.xml index dc2a9f93..50317fe0 100644 --- a/ui/src/main/res/values-nl-rNL/strings.xml +++ b/ui/src/main/res/values-nl-rNL/strings.xml @@ -4,6 +4,10 @@ <item quantity="one">Kan %d tunnel niet verwijderen: %s</item> <item quantity="other">Kan %d tunnels niet verwijderen: %s</item> </plurals> + <plurals name="delete_success"> + <item quantity="one">%d tunnel succesvol verwijderd</item> + <item quantity="other">%d tunnels succesvol verwijderd</item> + </plurals> <plurals name="delete_title"> <item quantity="one">%d tunnel geselecteerd</item> <item quantity="other">%d tunnels geselecteerd</item> @@ -12,4 +16,225 @@ <item quantity="one">%1$d van %2$d tunnels geïmporteerd</item> <item quantity="other">%1$d van de %2$d tunnels geïmporteerd</item> </plurals> + <plurals name="import_total_success"> + <item quantity="one">%d tunnel geïmporteerd</item> + <item quantity="other">%d tunnels geïmporteerd</item> + </plurals> + <plurals name="set_excluded_applications"> + <item quantity="one">%d uitgesloten applicatie(s)</item> + <item quantity="other">%d uitgesloten applicaties</item> + </plurals> + <plurals name="set_included_applications"> + <item quantity="one">%d inbegrepen applicatie</item> + <item quantity="other">%d inbegrepen applicaties</item> + </plurals> + <plurals name="n_excluded_applications"> + <item quantity="one">%d uitgesloten</item> + <item quantity="other">%d uitgesloten</item> + </plurals> + <plurals name="n_included_applications"> + <item quantity="one">%d inbegrepen</item> + <item quantity="other">%d inbegrepen</item> + </plurals> + <string name="all_applications">Alle applicaties</string> + <string name="exclude_from_tunnel">Uitsluiten</string> + <string name="include_in_tunnel">Alleen opnemen</string> + <plurals name="include_n_applications"> + <item quantity="one">Neem %d app op</item> + <item quantity="other">Voeg %d apps toe</item> + </plurals> + <plurals name="exclude_n_applications"> + <item quantity="one">%d app uitsluiten</item> + <item quantity="other">%d apps uitsluiten</item> + </plurals> + <plurals name="persistent_keepalive_seconds_unit"> + <item quantity="one">iedere seconde</item> + <item quantity="other">iedere %d seconden</item> + </plurals> + <plurals name="persistent_keepalive_seconds_suffix"> + <item quantity="one">seconde</item> + <item quantity="other">seconden</item> + </plurals> + <string name="use_all_applications">Gebruik alle applicaties</string> + <string name="add_peer">Peer toevoegen</string> + <string name="addresses">Adressen</string> + <string name="applications">Applicaties</string> + <string name="allow_remote_control_intents_summary_off">Externe apps kunnen mogelijk geen tunnels in-/uitschakelen (aanbevolen)</string> + <string name="allow_remote_control_intents_summary_on">Externe apps kunnen tunnels in-/uitschakelen (geavanceerd)</string> + <string name="allow_remote_control_intents_title">Controle door externe besturingsapps toestaan</string> + <string name="allowed_ips">Toegestane IP-adressen</string> + <string name="bad_config_context">%1$s\'s %2$s</string> + <string name="bad_config_context_top_level">%s</string> + <string name="bad_config_error">%1$s in %2$s</string> + <string name="bad_config_explanation_pka">: moet positief zijn en niet meer dan 65535</string> + <string name="bad_config_explanation_positive_number">: Moet positief zijn</string> + <string name="bad_config_explanation_udp_port">: Moet een geldig UDP poortnummer zijn</string> + <string name="bad_config_reason_invalid_key">Ongeldige sleutel</string> + <string name="bad_config_reason_invalid_number">Ongeldig nummer</string> + <string name="bad_config_reason_invalid_value">Ongeldige waarde</string> + <string name="bad_config_reason_missing_attribute">Attribuut ontbreekt</string> + <string name="bad_config_reason_missing_section">Ontbrekende sectie</string> + <string name="bad_config_reason_syntax_error">Syntaxfout</string> + <string name="bad_config_reason_unknown_attribute">Onbekend attribuut</string> + <string name="bad_config_reason_unknown_section">Onbekende sectie</string> + <string name="bad_config_reason_value_out_of_range">Waarde buiten bereik</string> + <string name="bad_extension_error">Bestand moet .conf of .zip zijn</string> + <string name="error_no_qr_found">QR-code niet gevonden in afbeelding</string> + <string name="error_qr_checksum">QR-code checksum verificatie mislukt</string> + <string name="cancel">Annuleren</string> + <string name="config_delete_error">Kan configuratiebestand %s niet verwijderen</string> + <string name="config_exists_error">Configuratie voor \"%s\" bestaat al</string> + <string name="config_file_exists_error">Configuratiebestand \"%s\" bestaat al</string> + <string name="config_not_found_error">Configuratiebestand \"%s\" niet gevonden</string> + <string name="config_rename_error">Kan configuratiebestand \"%s\" \" niet hernoemen</string> + <string name="config_save_error">Kan de configuratie voor \"%1$s\" niet opslaan: %2$s</string> + <string name="config_save_success">Configuratie succesvol opgeslagen voor \"%s\"</string> + <string name="create_activity_title">WireGuard tunnel aanmaken</string> + <string name="create_bin_dir_error">Kan geen lokale \'bin\' map aanmaken</string> + <string name="create_downloads_file_error">Kan bestand niet maken in downloadmap</string> + <string name="create_empty">Begin met lege configuratie</string> + <string name="create_from_file">Importeren uit bestand of archief</string> + <string name="create_from_qr_code">Scan van QR code</string> + <string name="create_output_dir_error">Kan de output map niet aanmaken</string> + <string name="create_temp_dir_error">Kan geen tijdelijke map aanmaken</string> + <string name="create_tunnel">Maak nieuwe tunnel</string> + <string name="copied_to_clipboard">%s gekopieerd naar klembord</string> + <string name="dark_theme_summary_off">Momenteel wordt licht (dag) thema gebruikt</string> + <string name="dark_theme_summary_on">Momenteel wordt donker (nacht) thema gebruikt</string> + <string name="dark_theme_title">Gebruik donker thema</string> + <string name="delete">Verwijder</string> + <string name="tv_delete">Selecteer tunnel om te verwijderen</string> + <string name="tv_select_a_storage_drive">Selecteer een opslaglocatie</string> + <string name="tv_no_file_picker">Installeer een bestandsbeheer applicatie</string> + <string name="tv_add_tunnel_get_started">Voeg een tunnel toe om te beginnen</string> + <string name="donate_title">♥️ Doneer aan het WireGuard Project</string> + <string name="donate_summary">Elke bijdrage helpt</string> + <string name="donate_google_play_disappointment">Bedankt voor het steunen van het WireGuard Project!\n\nHelaas, als gevolg van Google beleid, We mogen niet linken naar de webpagina van het project waar u een donatie kunt doen. Hopelijk kunt u deze zelf wel vinden!\n\nNogmaals bedankt voor uw bijdrage.</string> + <string name="disable_config_export_title">Config export uitschakelen</string> + <string name="disable_config_export_description">Het uitschakelen van configuratie export maakt privésleutels minder toegankelijk</string> + <string name="dns_servers">DNS-servers</string> + <string name="dns_search_domains">DNS-zoekdomeinen</string> + <string name="edit">Bewerken</string> + <string name="endpoint">Eindpunt</string> + <string name="error_down">Fout bij stoppen tunnel: %s</string> + <string name="error_fetching_apps">Fout bij ophalen van apps-lijst: %s</string> + <string name="error_root">Verkrijg root toegang en probeer het opnieuw</string> + <string name="error_prepare">Fout bij voorbereiden tunnel: %s</string> + <string name="error_up">Fout bij het starten van tunnel: %s</string> + <string name="exclude_private_ips">Privé-IP\'s uitsluiten</string> + <string name="generate_new_private_key">Nieuwe privésleutel genereren</string> + <string name="generic_error">Onbekend fout: \"%s\"</string> + <string name="hint_automatic">(auto)</string> + <string name="hint_generated">(gegenereerd)</string> + <string name="hint_optional">(optioneel)</string> + <string name="hint_optional_discouraged">(optioneel, niet aanbevolen)</string> + <string name="hint_random">(willekeurig)</string> + <string name="illegal_filename_error">Ongeldige bestandsnaam \"%s\" \"</string> + <string name="import_error">Kan tunnel niet importeren: %s</string> + <string name="import_from_qr_code">Importeer Tunnel uit QR Code</string> + <string name="import_success">Geïmporteerd \"%s\"</string> + <string name="interface_title">Interface</string> + <string name="key_contents_error">Slechte tekens in de veld</string> + <string name="key_length_error">Onjuiste sleutellengte</string> + <string name="key_length_explanation_base64">: WireGuard base64 sleutels moeten 44 tekens zijn (32 bytes)</string> + <string name="key_length_explanation_binary">: WireGuard sleutels moeten 32 bytes zijn</string> + <string name="key_length_explanation_hex">: WireGuard hex sleutels moeten 64 tekens zijn (32 bytes)</string> + <string name="latest_handshake">Recentste uitwisseling</string> + <string name="latest_handshake_ago">%s geleden</string> + <string name="listen_port">Luister op poort</string> + <string name="log_export_error">Kan logboek niet exporteren: %s</string> + <string name="log_export_subject">WireGuard Android logbestand</string> + <string name="log_export_success">Opgeslagen in \"%s\"</string> + <string name="log_export_title">Exporteer logboek naar bestand</string> + <string name="log_saver_activity_label">Logboek opslaan</string> + <string name="log_viewer_pref_summary">Logboeken kunnen helpen bij het debuggen</string> + <string name="log_viewer_pref_title">Bekijk applicatielogboek</string> + <string name="log_viewer_title">Log</string> + <string name="logcat_error">Kan logcat niet uitvoeren: </string> + <string name="module_enabler_disabled_summary">De experimentele kernel module kan de prestaties verbeteren</string> + <string name="module_enabler_disabled_title">Kernel module backend inschakelen</string> + <string name="module_enabler_enabled_summary">De langzamere userspace backend kan de stabiliteit verbeteren</string> + <string name="module_enabler_enabled_title">Uitschakelen kernel module backend</string> + <string name="module_installer_error">Er ging iets mis. Probeer het nog eens</string> + <string name="module_installer_initial">De experimentele kernel module kan de prestaties verbeteren</string> + <string name="module_installer_not_found">Er zijn geen modules beschikbaar voor uw apparaat</string> + <string name="module_installer_title">Download en installeer kernel module</string> + <string name="module_installer_working">Downloaden en installeren…</string> + <string name="module_version_error">Niet in staat om kernel module versie te bepalen</string> + <string name="mtu">MTU</string> + <string name="multiple_tunnels_summary_off">Het inschakelen van één tunnel zal anderen uitzetten</string> + <string name="multiple_tunnels_summary_on">Meerdere tunnels kunnen tegelijkertijd actief zijn</string> + <string name="multiple_tunnels_title">Meerdere gelijktijdige tunnels toestaan</string> + <string name="name">Naam</string> + <string name="no_config_error">Probeer een tunnel zonder configuratie te starten</string> + <string name="no_configs_error">Geen configuraties gevonden</string> + <string name="no_tunnels_error">Geen tunnels gedefinieerd</string> + <string name="parse_error_generic">string</string> + <string name="parse_error_inet_address">IP-adres</string> + <string name="parse_error_inet_endpoint">eindpunt</string> + <string name="parse_error_inet_network">IP netwerk</string> + <string name="parse_error_integer">nummer</string> + <string name="parse_error_reason">Kan %1$s%2$s niet parsen</string> + <string name="peer">Peer</string> + <string name="permission_description">beheer WireGuard tunnels, zet tunnels naar keuze aan en uit, en misleid mogelijk het Internetverkeer</string> + <string name="permission_label">WireGuard tunnels beheren</string> + <string name="persistent_keepalive">Voortdurende verbindingstest</string> + <string name="pre_shared_key">Gedeelde sleutel</string> + <string name="pre_shared_key_enabled">ingeschakeld</string> + <string name="private_key">Privésleutel</string> + <string name="public_key">Publieke sleutel</string> + <string name="qr_code_hint">Tip: genereer met `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">Voeg tegel toe aan snelle instellingen</string> + <string name="quick_settings_tile_add_summary">De sneltoets schakelt de meest recente tunnel aan</string> + <string name="quick_settings_tile_add_failure">Kan geen sneltoets toevoegen: fout %d</string> + <string name="quick_settings_tile_action">tunnel in-/uitschakelen</string> + <string name="restore_on_boot_summary_off">Zal ingeschakelde tunnels niet aanzetten bij opstarten</string> + <string name="restore_on_boot_summary_on">Zal ingeschakelde tunnels aanzetten bij opstarten</string> + <string name="restore_on_boot_title">Tunnel starten bij herstart</string> + <string name="save">Opslaan</string> + <string name="select_all">Selecteer alles</string> + <string name="settings">Instellingen</string> + <string name="shell_exit_status_read_error">Shell kan de exitstatus niet lezen</string> + <string name="shell_marker_count_error">Shell verwachtte 4 markeringen, ontving er %d</string> + <string name="shell_start_error">Shell kon niet starten: %d</string> + <string name="success_application_will_restart">Succes. De toepassing zal nu herstarten…</string> + <string name="toggle_all">Alles wisselen</string> + <string name="toggle_error">Fout bij omschakelen Wireguard tunnel: %s</string> + <string name="tools_installer_already">wg and wg-quick zijn al geïnstalleerd</string> + <string name="tools_installer_failure">Kan de command-line tools niet installeren (geen root?)</string> + <string name="tools_installer_initial">Optionele tools voor scripts installeren</string> + <string name="tools_installer_title">Installeer command line tools</string> + <string name="tools_installer_working">Installeren van wg en wg-quick</string> + <string name="transfer_bytes">%d B</string> + <string name="transfer_gibibytes">%.2f GiB</string> + <string name="transfer_kibibytes">%.2f KiB</string> + <string name="transfer_mibibytes">%.2f MiB</string> + <string name="transfer_tibibytes">%.2f TiB</string> + <string name="tun_create_error">Kan tun apparaat niet aanmaken</string> + <string name="tunnel_error_already_exists">Tunnel \"%s\" bestaat al</string> + <string name="tunnel_error_invalid_name">Ongeldige naam</string> + <string name="tunnel_list_placeholder">Voeg een tunnel toe met de knop hieronder</string> + <string name="tunnel_name">Tunnelnaam</string> + <string name="tunnel_rename_error">Kan tunnel niet hernoemen: %s</string> + <string name="tunnel_rename_success">Tunnel succesvol hernoemd naar \"%s\"</string> + <string name="type_name_go_userspace">Go userspace</string> + <string name="type_name_kernel_module">Kernel module</string> + <string name="unknown_error">Onbekende fout</string> + <string name="updater_avalable">Er is een nieuwe versie beschikbaar. Werk het programma s.v.p. bij.</string> + <string name="updater_action">Download & installeer updates</string> + <string name="updater_rechecking">Update metadata downloaden…</string> + <string name="updater_download_progress">Ophalen nieuwe versie: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Updates downloaden: %s</string> + <string name="updater_installing">Update wordt geïnstalleerd…</string> + <string name="updater_failure">Bijwerken mislukt: %s. Zal het zo opnieuw proberen…</string> + <string name="updater_corrupt_title">Toepassing beschadigd</string> + <string name="updater_corrupt_message">De toepassing is beschadigd. Download de APK opnieuw van de onderstaande website. De-installeer daarna het programma, en herinstalleer het met de gedownloade APK.</string> + <string name="updater_corrupt_navigate">Open website</string> + <string name="version_title">WireGuard voor Android v%s</string> + <string name="zip_export_summary">Zip-bestand wordt opgeslagen in de downloadmap</string> + <string name="zip_export_title">Exporteer tunnels naar zip-bestand</string> + <string name="biometric_prompt_zip_exporter_title">Authenticeer om de tunnel configuratie te exporteren</string> + <string name="biometric_prompt_private_key_title">Authenticeer om de persoonlijke sleutel te bekijken</string> + <string name="biometric_auth_error">Authenticatiefout</string> + <string name="biometric_auth_error_reason">Authenticatiefout: %s</string> </resources> diff --git a/ui/src/main/res/values-pt-rBR/strings.xml b/ui/src/main/res/values-pt-rBR/strings.xml index 456bedbf..eb8ab339 100644 --- a/ui/src/main/res/values-pt-rBR/strings.xml +++ b/ui/src/main/res/values-pt-rBR/strings.xml @@ -27,9 +27,13 @@ <string name="all_applications">Todos as aplicativos</string> <string name="exclude_from_tunnel">Retirar</string> <string name="include_in_tunnel">Incluir somente</string> + <plurals name="persistent_keepalive_seconds_suffix"> + <item quantity="one">segundo</item> + <item quantity="other">segundos</item> + </plurals> <string name="use_all_applications">Usar todas aplicações</string> <string name="add_peer">Adicionar Par</string> - <string name="addresses">Endereço:</string> + <string name="addresses">Endereço</string> <string name="applications">Aplicativo</string> <string name="allow_remote_control_intents_summary_off">Aplicativos externos podem não alternar túneis (recomendado)</string> <string name="allow_remote_control_intents_summary_on">Aplicativos externos podem alternar túneis (avançado)</string> @@ -79,6 +83,9 @@ <string name="tv_select_a_storage_drive">Selecione uma unidade de armazenamento</string> <string name="tv_no_file_picker">Por favor, instale um utilitário de gerenciamento de arquivos para procurar arquivos</string> <string name="tv_add_tunnel_get_started">Adicione um túnel para começar</string> + <string name="donate_title">♥️ Doar para o projeto WireGuard</string> + <string name="donate_summary">Todas as contribuições ajudam</string> + <string name="donate_google_play_disappointment">Obrigado por apoiar o Projeto WireGuard!\n\nInfelizmente, devido às políticas do Google, não temos permissão para vincular a parte da página do projeto onde você pode fazer uma doação. Esperamos que você consiga descobrir isso!\n\nObrigado novamente pela sua contribuição.</string> <string name="disable_config_export_title">Desativar exportação de configuração</string> <string name="disable_config_export_description">Desativar a exportação de configuração torna as chaves privadas menos acessíveis</string> <string name="dns_servers">Servidores DNS</string> @@ -88,6 +95,7 @@ <string name="error_down">Erro ao derrubar o túnel: %s</string> <string name="error_fetching_apps">Erro ao obter lista de apps: %s</string> <string name="error_root">Por favor, obtenha acesso root e tente novamente</string> + <string name="error_prepare">Erro ao preparar o túnel: %s</string> <string name="error_up">Erro ao criar túnel: %s</string> <string name="exclude_private_ips">Excluir IPs privados</string> <string name="generate_new_private_key">Gerar uma nova chave privada</string> @@ -107,6 +115,8 @@ <string name="key_length_explanation_base64">: Chaves base64 do WireGuard devem ter 44 caracteres (32 bytes)</string> <string name="key_length_explanation_binary">: Chaves do WireGuard devem ter 32 bytes</string> <string name="key_length_explanation_hex">: Chaves hex do WireGuard devem ter 64 caracteres (32 bytes)</string> + <string name="latest_handshake">Último handshake</string> + <string name="latest_handshake_ago">%s atrás</string> <string name="listen_port">Porta de escuta</string> <string name="log_export_error">Não foi possível exportar o log: %s</string> <string name="log_export_subject">Arquivo de log do WireGuard Android</string> @@ -148,6 +158,9 @@ <string name="private_key">Chave Privada</string> <string name="public_key">Chave pública</string> <string name="qr_code_hint">Dica: gerar com `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">Adicionar botão ao painel de configurações rápidas</string> + <string name="quick_settings_tile_add_summary">A tecla de atalho alterna o túnel mais recente</string> + <string name="quick_settings_tile_add_failure">Não foi possível adicionar o atalho de botão: erro %d</string> <string name="restore_on_boot_title">Restaurar na inicialização</string> <string name="save">Salvar</string> <string name="select_all">Selecionar tudo</string> @@ -161,6 +174,7 @@ <string name="tools_installer_failure">Não foi possível instalar ferramentas de linha de comando (sem root?)</string> <string name="tools_installer_initial">Instalar ferramentas opcionais para scripting</string> <string name="tools_installer_initial_magisk">Instalar ferramentas opcionais para scripting como o módulo Magisk</string> + <string name="tools_installer_initial_system">Instalar ferramentas opcionais para escrever na partição do sistema</string> <string name="tools_installer_success_magisk">wg e wg-quick instalado como um módulo Magisk (é necessário reiniciar)</string> <string name="tools_installer_success_system">wg e wg-quick instalados na partição do sistema</string> <string name="tools_installer_title">Instalar ferramentas de linha de comando</string> @@ -178,8 +192,27 @@ <string name="tunnel_create_success">Túnel criado com sucesso “%s”</string> <string name="tunnel_error_already_exists">Túnel “%s” já existe</string> <string name="tunnel_error_invalid_name">Nome inválido</string> + <string name="tunnel_list_placeholder">Adicionar um túnel usando o botão abaixo</string> + <string name="tunnel_name">Nome do túnel</string> + <string name="tunnel_on_error">Não foi possível ativar o túnel (wgTurnOn retornou %d)</string> + <string name="tunnel_dns_failure">Não foi possível resolver host DNS: \"%s\"</string> + <string name="tunnel_rename_error">Não foi possível renomear o túnel: %s</string> + <string name="tunnel_rename_success">Renomeado com sucesso o túnel para “%s”</string> <string name="type_name_kernel_module">Modo de Kernel</string> <string name="unknown_error">Erro desconhecido</string> + <string name="updater_avalable">Uma atualização do aplicativo está disponível. Pôr favor atualize agora.</string> + <string name="updater_action">Baixar & Atualizar</string> + <string name="updater_rechecking">Obtendo metadados de atualização…</string> + <string name="updater_download_progress">Baixando a atualização: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Baixando atualização: %s</string> + <string name="updater_installing">Instalando atualização…</string> + <string name="updater_failure">Falha na atualização: %s. Tentaremos novamente em breve…</string> + <string name="updater_corrupt_title">Aplicativo corrompido</string> + <string name="updater_corrupt_message">Este aplicativo está corrompido. Por favor, baixe novamente o APK do site vinculado abaixo. Depois disso, desinstale o aplicativo e reinstale-o a partir do APK baixado.</string> + <string name="updater_corrupt_navigate">Abrir site</string> + <string name="version_summary_checking">Verificando a versão do backend %s</string> + <string name="version_summary_unknown">Versão %s desconhecida</string> + <string name="version_title">WireGuard para Android v%s</string> <string name="vpn_not_authorized_error">Serviço de VPN não autorizado pelo usuário</string> <string name="vpn_start_error">Não foi possível iniciar o serviço de VPN do Android</string> <string name="zip_export_error">Não foi possível exportar túneis: %s</string> diff --git a/ui/src/main/res/values-ro-rRO/strings.xml b/ui/src/main/res/values-ro-rRO/strings.xml index 18144658..16ee62f0 100644 --- a/ui/src/main/res/values-ro-rRO/strings.xml +++ b/ui/src/main/res/values-ro-rRO/strings.xml @@ -120,6 +120,9 @@ <string name="tv_select_a_storage_drive">Selectează o unitate de stocare</string> <string name="tv_no_file_picker">Instalează un serviciu de administrare a fișierelor pentru a căuta fișiere</string> <string name="tv_add_tunnel_get_started">Adaugă un tunel pentru a începe</string> + <string name="donate_title">♥ Donează pentru proiectul WireGuard</string> + <string name="donate_summary">Fiecare contribuţie ajută</string> + <string name="donate_google_play_disappointment">Vă mulțumim pentru sprijinul acordat Proiectului WireGuard!\n\nDin păcate, din cauza politicilor Google, nu avem voie să punem un link către pagina web a proiectului unde poți face o donație. Sperăm că vă puteți descurca!\n\nMulțumim din nou pentru contribuție.</string> <string name="disable_config_export_title">Dezactivează exportarea configurației</string> <string name="disable_config_export_description">Dezactivarea exportării configurației face mai puțin accesibile cheile private</string> <string name="dns_servers">Servere DNS</string> @@ -129,6 +132,7 @@ <string name="error_down">Eroare la oprirea tunelului: %s</string> <string name="error_fetching_apps">Eroare la preluarea listei de aplicații: %s</string> <string name="error_root">Obține acces root și încearcă din nou</string> + <string name="error_prepare">Eroare la pregătirea tunelului: %s</string> <string name="error_up">Eroare la pornirea tunelului: %s</string> <string name="exclude_private_ips">Excludere IP-uri private</string> <string name="generate_new_private_key">Generare cheie privată nouă</string> @@ -148,6 +152,8 @@ <string name="key_length_explanation_base64">: Cheile base64 ale WireGuard trebuie să aibă 44 de caractere (32 de octeți)</string> <string name="key_length_explanation_binary">: Cheile WireGuard trebuie să aibă 32 de octeți</string> <string name="key_length_explanation_hex">: Cheile hex WireGuard trebuie să aibă 64 de caractere (32 de octeți)</string> + <string name="latest_handshake">Cea mai recentă negociere</string> + <string name="latest_handshake_ago">%s în urmă</string> <string name="listen_port">Port de ascultare</string> <string name="log_export_error">Jurnalul nu poate fi exportat: %s</string> <string name="log_export_subject">Fișier de jurnal Android WireGuard</string> @@ -191,6 +197,8 @@ <string name="private_key">Cheie privată</string> <string name="public_key">Cheie publică</string> <string name="qr_code_hint">Sfat: generează cu `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">Adaugă secțiune la panoul de setări rapide</string> + <string name="quick_settings_tile_add_summary">Comanda rapidă comută cel mai recent tunel</string> <string name="restore_on_boot_summary_off">Tunelurile activate nu vor fi pornite odată cu pornirea dispozitivului</string> <string name="restore_on_boot_summary_on">Tunelurile activate vor fi pornite odată cu pornirea dispozitivului</string> <string name="restore_on_boot_title">Restaurare la pornire</string> diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml index fd747768..70f38ee1 100644 --- a/ui/src/main/res/values-ru/strings.xml +++ b/ui/src/main/res/values-ru/strings.xml @@ -88,13 +88,13 @@ <string name="allow_remote_control_intents_summary_off">Внешние приложения не могут переключать туннели (рекомендуется)</string> <string name="allow_remote_control_intents_summary_on">Внешние приложения могут переключать туннели (продвинутые)</string> <string name="allow_remote_control_intents_title">Разрешить управление через внешние приложения</string> - <string name="allowed_ips">Разрешенные IP-адреса</string> + <string name="allowed_ips">Разрешенные IP</string> <string name="bad_config_context">%1$s из %2$s</string> <string name="bad_config_context_top_level">%s</string> <string name="bad_config_error">%1$s в %2$s</string> <string name="bad_config_explanation_pka">: Значение должно быть больше нуля, но меньше 65535</string> <string name="bad_config_explanation_positive_number">: Значение должно быть больше нуля</string> - <string name="bad_config_explanation_udp_port">: Должен быть действительный номер порта UDP</string> + <string name="bad_config_explanation_udp_port">: Должен быть допустимым UDP-портом</string> <string name="bad_config_reason_invalid_key">Неправильный ключ</string> <string name="bad_config_reason_invalid_number">Неправильный номер</string> <string name="bad_config_reason_invalid_value">Недопустимое значение</string> @@ -135,14 +135,14 @@ <string name="tv_add_tunnel_get_started">Добавьте туннель, чтобы начать</string> <string name="donate_title">♥ Пожертвовать проекту WireGuard</string> <string name="donate_summary">Каждое пожертвование помогает</string> - <string name="donate_google_play_disappointment">Спасибо за поддержку проекта WireGuard!\n\nК сожалению, из-за политики Google, нельзя размещать ссылку на тот раздел сайта проекта, где можно сделать пожертвование. Надеемся, вы сможете разобраться самостоятельно!\n\nЕщё раз спасибо за ваш вклад.</string> + <string name="donate_google_play_disappointment">Спасибо за поддержку проекта WireGuard!\n\nК сожалению, из-за политики Google, нельзя размещать ссылку на тот раздел сайта проекта, где можно сделать пожертвование. Надеемся, вы сможете разобраться самостоятельно!\n\nЕщераз спасибо за ваш вклад.</string> <string name="disable_config_export_title">Отключить экспорт конфигурации</string> <string name="disable_config_export_description">Отключение экспорта конфигурации делает приватные ключи менее доступными</string> <string name="dns_servers">DNS-серверы</string> <string name="dns_search_domains">Домены поиска</string> <string name="edit">Изменить</string> <string name="endpoint">Конечная точка</string> - <string name="error_down">Ошибка при выходе из туннеля: %s</string> + <string name="error_down">Ошибка при отключении туннеля: %s</string> <string name="error_fetching_apps">Ошибка при получении списка приложений: %s</string> <string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string> <string name="error_prepare">Ошибка при подготовке туннеля: %s</string> @@ -202,7 +202,7 @@ <string name="parse_error_integer">число</string> <string name="parse_error_reason">Невозможно разобрать %1$s “%2$s”</string> <string name="peer">Пир</string> - <string name="permission_description">управлять туннелями WireGuard, включая и отключая туннели по желанию, потенциально перенаправляя интернет-трафик</string> + <string name="permission_description">контроль над туннелями WireGuard, включение и отключение туннелей по своему усмотрению, возможность неправильного управления сетевым трафиком</string> <string name="permission_label">управлять туннелями WireGuard</string> <string name="persistent_keepalive">Постоянное соединение</string> <string name="pre_shared_key">Общий ключ</string> @@ -210,6 +210,10 @@ <string name="private_key">Приватный ключ</string> <string name="public_key">Публичный ключ</string> <string name="qr_code_hint">Совет: генерировать с “qrencode -t ansiutf8 < tunnel.conf”.</string> + <string name="quick_settings_tile_add_title">Добавить элемент в панель быстрых настроек</string> + <string name="quick_settings_tile_add_summary">Элемент переключает последний активный туннель</string> + <string name="quick_settings_tile_add_failure">Не удается добавить ярлык: ошибка %d</string> + <string name="quick_settings_tile_action">Переключить туннель</string> <string name="restore_on_boot_summary_off">Не поднимать ранее выбранные туннели при загрузке</string> <string name="restore_on_boot_summary_on">Поднимать ранее выбранные туннели при загрузке</string> <string name="restore_on_boot_title">Восстановить при загрузке</string> @@ -251,10 +255,20 @@ <string name="tunnel_dns_failure">Не удалось определить DNS имя: “%s”</string> <string name="tunnel_rename_error">Не удалось переименовать туннель: %s</string> <string name="tunnel_rename_success">Туннель успешно переименован в “%s”</string> - <string name="type_name_go_userspace">Пользовательское пространство Go</string> + <string name="type_name_go_userspace">Go в пользовательском пространстве</string> <string name="type_name_kernel_module">Модуль ядра</string> <string name="unknown_error">Неизвестная ошибка</string> - <string name="version_summary">%1$s бэкенд %2$s</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="updater_corrupt_title">Приложение повреждено</string> + <string name="updater_corrupt_message">Приложение повреждено. Загрузите APK с сайта, указанного ниже, затем удалите это приложение и установите из загруженного APK.</string> + <string name="updater_corrupt_navigate">Открыть сайт</string> + <string name="version_summary">Бэкенд: %1$s %2$s</string> <string name="version_summary_checking">Проверка версии бэкэнда %s</string> <string name="version_summary_unknown">Неизвестная версия %s</string> <string name="version_title">WireGuard для Android v%s</string> diff --git a/ui/src/main/res/values-sk-rSK/strings.xml b/ui/src/main/res/values-sk-rSK/strings.xml index 0b86fb29..40df1d7e 100644 --- a/ui/src/main/res/values-sk-rSK/strings.xml +++ b/ui/src/main/res/values-sk-rSK/strings.xml @@ -8,7 +8,7 @@ <string name="addresses">Adresy</string> <string name="applications">Aplikácie</string> <string name="allow_remote_control_intents_summary_off">Externé aplikácie nemôžu spustiť tunely (odporúčané)</string> - <string name="allow_remote_control_intents_summary_on">Externé aplikácie môžu spustiť tunely (odporúčané)</string> + <string name="allow_remote_control_intents_summary_on">Externé aplikácie môžu spustiť tunely (pokročilé)</string> <string name="allow_remote_control_intents_title">Povoliť aplikáciám vzdialenú správu</string> <string name="allowed_ips">Povolené IP adresy</string> <string name="bad_config_context_top_level">%s</string> @@ -20,8 +20,8 @@ <string name="bad_config_reason_invalid_number">Neplatné číslo</string> <string name="bad_config_reason_invalid_value">Neplatná hodnota</string> <string name="bad_config_reason_missing_attribute">Chýbajúci atribút</string> - <string name="bad_config_reason_missing_section">Chýbajúc sekcia</string> - <string name="bad_config_reason_syntax_error">Chyba syntaxu</string> + <string name="bad_config_reason_missing_section">Chýbajúca sekcia</string> + <string name="bad_config_reason_syntax_error">Chyba syntaxe</string> <string name="bad_config_reason_unknown_attribute">Neznámy atribút</string> <string name="bad_config_reason_unknown_section">Neznáma sekcia</string> <string name="bad_config_reason_value_out_of_range">Hodnota mimo povoleného rozsahu</string> @@ -31,12 +31,12 @@ <string name="config_exists_error">Konfigurácia pre “%s” už existuje</string> <string name="config_file_exists_error">Konfiguračný súbor pre “%s” už existuje</string> <string name="config_not_found_error">Konfiguračný súbor “%s” sa nenašiel</string> - <string name="config_rename_error">Nemôžete premenovať konfiguračný súbor “%s”</string> - <string name="config_save_error">Nemôžete uložiť konfiguráciu pre “%1$s”: %2$s</string> + <string name="config_rename_error">Nepodarilo sa premenovať konfiguračný súbor “%s”</string> + <string name="config_save_error">Nepodarilo sa uložiť konfiguráciu pre “%1$s”: %2$s</string> <string name="config_save_success">Úspešne sa podarilo uložiť konfiguráciu pre “%s”</string> <string name="create_activity_title">Vytvoriť WireGuard tunel</string> - <string name="create_bin_dir_error">Nemôžete vytvoriť lokálny binárny súbor</string> - <string name="create_downloads_file_error">Nemôžete vytvoriť súbor v priečinku downloads</string> + <string name="create_bin_dir_error">Nepodarilo sa vytvoriť lokálny priečinok pre binárne súbory</string> + <string name="create_downloads_file_error">Nepodarilo sa vytvoriť súbor v priečinku stiahnuté</string> <string name="create_empty">Vytvoriť od počiatku</string> <string name="create_from_file">Importovať zo súboru alebo archívu</string> <string name="create_from_qr_code">Skenovať z QR kódu</string> @@ -50,10 +50,10 @@ <string name="delete">Odstrániť</string> <string name="tv_delete">Vyberte tunel na odstránenie</string> <string name="tv_select_a_storage_drive">Vyberte úložnú jednotku</string> - <string name="tv_no_file_picker">Prosím nainštalujte manažéra súbor aby ste mohli prehliadať súbory</string> + <string name="tv_no_file_picker">Prosím nainštalujte manažéra súborov aby ste mohli prehliadať súbory</string> <string name="tv_add_tunnel_get_started">Pridajte tunel aby ste mohli začať</string> <string name="disable_config_export_title">Zakázať export konfigurácie</string> - <string name="disable_config_export_description">Zakázanie exportu konfigurácii spôsobí, že prístup k súkromným kľúčom sa stáva zložitým</string> + <string name="disable_config_export_description">Zakázanie exportu konfigurácie spôsobí, že prístup k súkromným kľúčom sa stáva zložitým</string> <string name="dns_servers">Servery DNS</string> <string name="dns_search_domains">Prehľadávať domény</string> <string name="edit">Upraviť</string> @@ -61,46 +61,46 @@ <string name="error_down">Chyba pri vypínaní tunela: %s</string> <string name="error_fetching_apps">Chyba pri načítaní zoznamu aplikácií: %s</string> <string name="error_root">Získajte prístup root a skúste znova</string> - <string name="error_up">Chyba pri vyvolávaní tunela: %s</string> - <string name="exclude_private_ips">Vynechať privátne IP</string> - <string name="generate_new_private_key">Generovať nový privátny kľúč</string> + <string name="error_up">Chyba pri zapínaní tunela: %s</string> + <string name="exclude_private_ips">Vynechať súkromné IP</string> + <string name="generate_new_private_key">Generovať nový súkromný kľúč</string> <string name="generic_error">Neznáma “%s” chyba</string> <string name="hint_automatic">(automatické)</string> <string name="hint_generated">(generované)</string> <string name="hint_optional">(voliteľné)</string> <string name="hint_optional_discouraged">(voliteľné, neodporúča sa)</string> <string name="hint_random">(náhodné)</string> - <string name="illegal_filename_error">Ilegálne meno súboru “%s”</string> + <string name="illegal_filename_error">Nepovolené meno súboru “%s”</string> <string name="import_error">Nepodarilo sa importovať tunel: %s</string> <string name="import_from_qr_code">Importovať tunel z QR kódu</string> <string name="import_success">Podarilo sa importovať “%s”</string> <string name="interface_title">Rozhranie</string> - <string name="key_contents_error">Nepovolené charaktery v kľúči</string> + <string name="key_contents_error">Nepovolené znaky v kľúči</string> <string name="key_length_error">Nesprávna dĺžka kľúču</string> - <string name="key_length_explanation_base64">: WireGuard base64 kľúče musia mať 44 charakterov (32 bytes)</string> + <string name="key_length_explanation_base64">: WireGuard base64 kľúče musia mať 44 znakov (32 bytes)</string> <string name="key_length_explanation_binary">: WireGuard kľúče musia byť 32 bytové</string> - <string name="key_length_explanation_hex">: WireGuard hex kľúče musia mať 64 charakterov (32 bytes)</string> + <string name="key_length_explanation_hex">: WireGuard hex kľúče musia mať 64 znakov (32 bytes)</string> <string name="listen_port">Otvorený port</string> <string name="log_export_error">Nepodarilo sa exportovať log: %s</string> - <string name="log_export_subject">WireGuard Android Záznam</string> + <string name="log_export_subject">WireGuard Android Denník udalostí</string> <string name="log_export_success">Uložené do “%s”</string> <string name="log_export_title">Exportovať denník udalostí</string> <string name="log_saver_activity_label">Uložiť denník udalostí</string> - <string name="log_viewer_pref_summary">Denník udalosti môžu byt nápomocné pri ladení aplikácie</string> + <string name="log_viewer_pref_summary">Denníky udalostí môžu byt nápomocné pri ladení aplikácie</string> <string name="log_viewer_pref_title">Zobraziť denník udalostí aplikácie</string> <string name="log_viewer_title">Denník udalostí</string> - <string name="logcat_error">Nepodarilo sa prehrať logcat: </string> - <string name="module_enabler_enabled_summary">Pomalšie užívatelské prostredie môže zlepšiť stabilitu</string> + <string name="logcat_error">Nepodarilo sa spustiť logcat: </string> + <string name="module_enabler_enabled_summary">Pomalší userspace backend môže zlepšiť stabilitu</string> <string name="module_installer_error">Niečo sa pokazilo. Prosím, skúste znova</string> <string name="module_installer_not_found">Pre vaše zariadenie nie sú k dispozícii žiadne moduly</string> - <string name="module_installer_title">Stiahni a nainštaluj kernel modul</string> - <string name="module_installer_working">Sťahuje a inštalujem…</string> + <string name="module_installer_title">Stiahnutie a inštalácia kernelového modulu</string> + <string name="module_installer_working">Sťahuje sa a inštaluje sa…</string> <string name="mtu">Maximálna prenosová jednotka</string> <string name="multiple_tunnels_summary_off">Zapnutím jedného tunela vypnete ostatné</string> - <string name="multiple_tunnels_summary_on">Môžu byť zapnuté viaceré tunele naraz</string> - <string name="multiple_tunnels_title">Povoliť niekoľko tunel naraz</string> + <string name="multiple_tunnels_summary_on">Môžu byť zapnuté viaceré tunely naraz</string> + <string name="multiple_tunnels_title">Povoliť viacero tunelov naraz</string> <string name="name">Názov</string> - <string name="no_config_error">Pokúšam sa vyvolať tunel bez konfigurácie</string> + <string name="no_config_error">Pokúšam sa zapnúť tunel bez konfigurácie</string> <string name="no_configs_error">Nenašli sa žiadne konfigurácie</string> <string name="no_tunnels_error">Neexistujú žiadne tunely</string> <string name="parse_error_generic">reťazec</string> @@ -108,7 +108,7 @@ <string name="parse_error_inet_endpoint">koncový bod</string> <string name="parse_error_inet_network">IP sieť</string> <string name="parse_error_integer">číslo</string> - <string name="parse_error_reason">Nedá sa rozobrať %1$s “%2$s”</string> + <string name="parse_error_reason">Nedá sa parsovať %1$s “%2$s”</string> <string name="pre_shared_key">Vopred zdieľaný kľúč</string> <string name="pre_shared_key_enabled">povolené</string> <string name="private_key">Súkromný kľúč</string> @@ -121,10 +121,10 @@ <string name="toggle_all">Prepnúť všetko</string> <string name="tools_installer_already">wg a wg-quick už sú nainštalované</string> <string name="tools_installer_initial">Nainštalovať voliteľné nástroje pre skriptovanie</string> - <string name="tools_installer_initial_magisk">Nainštalovať voliteľné nástroje pre skriptovanie ako Magisk module</string> + <string name="tools_installer_initial_magisk">Nainštalovať voliteľné nástroje pre skriptovanie ako Magisk modul</string> <string name="tools_installer_success_magisk">wg a wg-quick sú nainštalované ako Magisk modul (reštart požadovaný)</string> <string name="tools_installer_title">Inštalácia nástrojov príkazového riadku</string> - <string name="tools_installer_working">Inštalácia wg a wg-quick</string> + <string name="tools_installer_working">Inštaluje sa wg a wg-quick</string> <string name="tools_unavailable_error">Potrebné nástroje nie sú k dispozícii</string> <string name="transfer">Prenos</string> <string name="transfer_bytes">%d B</string> @@ -141,7 +141,7 @@ <string name="tunnel_name">Meno tunelu</string> <string name="tunnel_rename_error">Nepodarilo sa premenovať tunel: %s</string> <string name="tunnel_rename_success">Úspešne premenovaný tunel na “%s”</string> - <string name="type_name_kernel_module">Modul jadra</string> + <string name="type_name_kernel_module">Kernelový modul</string> <string name="unknown_error">Neznáma chyba</string> <string name="version_summary_unknown">Neznáma %s verzia</string> <string name="version_title">WireGuard pre Android v%s</string> @@ -149,10 +149,10 @@ <string name="vpn_start_error">Nepodarilo sa spustiť Android VPN službu</string> <string name="zip_export_error">Nepodarilo sa exportovať tunely: %s</string> <string name="zip_export_success">Uložené ako “%s”</string> - <string name="zip_export_summary">Zip súbor bude uložený do priečinka na sťahovanie</string> + <string name="zip_export_summary">Zip súbor bude uložený do priečinka stiahnuté</string> <string name="zip_export_title">Export tunelov do zip súboru</string> <string name="biometric_prompt_zip_exporter_title">Overovanie pre export tunelov</string> - <string name="biometric_prompt_private_key_title">Overovanie pre zobrazenie privátneho kľúča</string> + <string name="biometric_prompt_private_key_title">Authenticate to view private key</string> <string name="biometric_auth_error">Overovanie zlyhalo</string> <string name="biometric_auth_error_reason">Overovanie zlyhalo: %s</string> </resources> diff --git a/ui/src/main/res/values-sv-rSE/strings.xml b/ui/src/main/res/values-sv-rSE/strings.xml index c919d15c..910f9e05 100644 --- a/ui/src/main/res/values-sv-rSE/strings.xml +++ b/ui/src/main/res/values-sv-rSE/strings.xml @@ -107,6 +107,9 @@ <string name="tv_select_a_storage_drive">Välj en lagringsenhet</string> <string name="tv_no_file_picker">Installera ett filhanteringsverktyg för att bläddra bland filer</string> <string name="tv_add_tunnel_get_started">Lägg till en tunnel för att komma igång</string> + <string name="donate_title">♥ Donera till WireGuard Projektet</string> + <string name="donate_summary">Varje bidrag hjälper</string> + <string name="donate_google_play_disappointment">Tack för att du stödjer WireGuard Projektet!\n\nPå grund av Googles policyer får vi dessvärre inte till den del av projektets webbsida där du kan göra en donation. Förhoppningsvis kan du hitta dit ändå!\n\nTack igen för ditt bidrag.</string> <string name="disable_config_export_title">Inaktivera export av konfiguration</string> <string name="disable_config_export_description">Inaktivering av konfigurationsexport gör privata nycklar mindre tillgängliga</string> <string name="dns_servers">DNS-servrar</string> @@ -116,6 +119,7 @@ <string name="error_down">Fel vid nedtagning av tunnel: %s</string> <string name="error_fetching_apps">Fel vid hämtning av applista: %s</string> <string name="error_root">Vänligen få rootbehörighet och försök igen</string> + <string name="error_prepare">Fel vid förberedelse av tunnel: %s</string> <string name="error_up">Fel vid uppstart av tunnel: %s</string> <string name="exclude_private_ips">Uteslut privata IP-adresser</string> <string name="generate_new_private_key">Skapa ny privat nyckel</string> @@ -135,6 +139,8 @@ <string name="key_length_explanation_base64">: WireGuard base64 nycklar måste vara 44 tecken (32 bytes)</string> <string name="key_length_explanation_binary">: WireGuard nycklar måste vara 32 bytes</string> <string name="key_length_explanation_hex">: WireGuard hex nycklar måste vara 64 tecken (32 bytes)</string> + <string name="latest_handshake">Senaste handskakning</string> + <string name="latest_handshake_ago">%s sedan</string> <string name="listen_port">Lyssningsport</string> <string name="log_export_error">Kan inte exportera loggen: %s</string> <string name="log_export_subject">WireGuard Android loggfil</string> @@ -178,6 +184,10 @@ <string name="private_key">Privat nyckel</string> <string name="public_key">Offentlig nyckel</string> <string name="qr_code_hint">Tips: generera med `qrencode -t ansiutf8 < tunnel.conf`.</string> + <string name="quick_settings_tile_add_title">Lägg till tile i snabbinställningarna</string> + <string name="quick_settings_tile_add_summary">Tilen växlar din senaste tunnel mellan på och av</string> + <string name="quick_settings_tile_add_failure">Misslyckades med att skapa tile: fel %d</string> + <string name="quick_settings_tile_action">Växla tunnel på/av</string> <string name="restore_on_boot_summary_off">Kommer inte ta upp aktiverade tunnlar vid uppstart</string> <string name="restore_on_boot_summary_on">Kommer ta upp aktiverade tunnlar vid uppstart</string> <string name="restore_on_boot_title">Återställ vid uppstart</string> @@ -213,6 +223,7 @@ <string name="tunnel_create_success">Lyckades skapa tunnel “%s”</string> <string name="tunnel_error_already_exists">Tunnel ”%s” finns redan</string> <string name="tunnel_error_invalid_name">Ogiltigt namn</string> + <string name="tunnel_list_placeholder">Lägg till en tunnel med knappen nedan</string> <string name="tunnel_name">Tunnelns namn</string> <string name="tunnel_on_error">Kunde inte aktivera tunneln (wgTurnOn returnerade %d)</string> <string name="tunnel_dns_failure">Det går inte att lösa DNS-värdnamn: ”%s”</string> @@ -221,6 +232,16 @@ <string name="type_name_go_userspace">Användarutrymme för Go</string> <string name="type_name_kernel_module">Kärnmodul</string> <string name="unknown_error">Okänt fel</string> + <string name="updater_avalable">Det finns en uppdatering till appen. Vänligen uppdatera nu.</string> + <string name="updater_action">Ladda ner & uppdatera</string> + <string name="updater_rechecking">Hämtar uppdateringens metadata…</string> + <string name="updater_download_progress">Laddar ner uppdatering: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Laddar ner uppdatering: %s</string> + <string name="updater_installing">Installerar uppdatering…</string> + <string name="updater_failure">Uppdatering misslyckades: %s. Försöker igen inom kort…</string> + <string name="updater_corrupt_title">Applikationen är korrupt</string> + <string name="updater_corrupt_message">Applikationen är korrupt. Vänligen ladda ner en APK från hemsidan länkad nedan. Avinstallera därefter denna applikation och installera den nerladdade APKn.</string> + <string name="updater_corrupt_navigate">Öppna hemsida</string> <string name="version_summary">%1$s bakstycke %2$s</string> <string name="version_summary_checking">Kontrollerar %s backstycke utgåva</string> <string name="version_summary_unknown">Okänd %s utgåva</string> diff --git a/ui/src/main/res/values-tr-rTR/strings.xml b/ui/src/main/res/values-tr-rTR/strings.xml index eafd3ded..57bc0ca5 100644 --- a/ui/src/main/res/values-tr-rTR/strings.xml +++ b/ui/src/main/res/values-tr-rTR/strings.xml @@ -119,6 +119,7 @@ <string name="error_down">Tünel kapatılırken hata oluştu: %s</string> <string name="error_fetching_apps">Uygulama listesi getirilirken hata oluştu: %s</string> <string name="error_root">Lütfen root erişimi elde edin ve tekrar deneyin</string> + <string name="error_prepare">Tünel hazırlanırken hata oluştu: %s</string> <string name="error_up">Tünel açılırken hata oluştu: %s</string> <string name="exclude_private_ips">Özel IP’leri hariç tut</string> <string name="generate_new_private_key">Yeni özel anahtar oluştur</string> @@ -138,6 +139,8 @@ <string name="key_length_explanation_base64">: WireGuard base64 anahtarları 44 karakter (32 bayt) olmalıdır</string> <string name="key_length_explanation_binary">: WireGuard anahtarları 32 bayt olmalıdır</string> <string name="key_length_explanation_hex">: WireGuard onaltılık anahtarları 64 karakter (32 bayt) olmalıdır</string> + <string name="latest_handshake">En son el sıkışma</string> + <string name="latest_handshake_ago">%s önce</string> <string name="listen_port">Dinlenen port</string> <string name="log_export_error">Günlük dışa aktarılamıyor: %s</string> <string name="log_export_subject">WireGuard Android Günlük Dosyası</string> @@ -181,6 +184,10 @@ <string name="private_key">Özel anahtar</string> <string name="public_key">Genel anahtar</string> <string name="qr_code_hint">İpucu: `qrencode -t ansiutf8 < tunnel.conf` ile oluşturun.</string> + <string name="quick_settings_tile_add_title">Hızlı ayarlar paneline kutucuk ekle</string> + <string name="quick_settings_tile_add_summary">Kısayol kutucuğu en son tüneli değiştirir</string> + <string name="quick_settings_tile_add_failure">Kısayol kutucuğu eklenemiyor: hata %d</string> + <string name="quick_settings_tile_action">Tüneli aç/kapat</string> <string name="restore_on_boot_summary_off">Önyüklemede etkin tünelleri açmayacak</string> <string name="restore_on_boot_summary_on">Önyüklemede etkin tünelleri açacak</string> <string name="restore_on_boot_title">Cihaz açılırken başlat</string> @@ -225,6 +232,16 @@ <string name="type_name_go_userspace">Kullanıcı alanına git</string> <string name="type_name_kernel_module">Çekirdek modülü</string> <string name="unknown_error">Bilinmeyen hata</string> + <string name="updater_avalable">Bir uygulama güncellemesi mevcut. Lütfen şimdi güncelleyin.</string> + <string name="updater_action">İndir & Güncelle</string> + <string name="updater_rechecking">Güncelleme meta verileri getiriliyor…</string> + <string name="updater_download_progress">Güncelleme indiriliyor: %1$s / %2$s (%3$.2f%%)</string> + <string name="updater_download_progress_nototal">Güncelleme indiriliyor: %s</string> + <string name="updater_installing">Güncelleme yükleniyor…</string> + <string name="updater_failure">Güncelleme hatası: %s. Kısa bir süre sonra tekrar denenecek…</string> + <string name="updater_corrupt_title">Uygulama Bozuk</string> + <string name="updater_corrupt_message">Bu uygulama bozuk. Lütfen APK\'yı aşağıda bağlantısı verilen web sitesinden yeniden indirin. Daha sonra bu uygulamayı kaldırın ve indirilen APK\'dan yeniden yükleyin.</string> + <string name="updater_corrupt_navigate">Web Sitesini Aç</string> <string name="version_summary">%1$s arka uç %2$s</string> <string name="version_summary_checking">%s arka uç sürümü kontrol ediliyor</string> <string name="version_summary_unknown">Bilinmeyen %s sürümü</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-vi-rVN/strings.xml b/ui/src/main/res/values-vi-rVN/strings.xml index 8149d7e5..79d8d6c3 100644 --- a/ui/src/main/res/values-vi-rVN/strings.xml +++ b/ui/src/main/res/values-vi-rVN/strings.xml @@ -94,6 +94,9 @@ <string name="tv_select_a_storage_drive">Chọn bộ lưu trữ</string> <string name="tv_no_file_picker">Vui lòng cài đặt tệp tiện ích lưu trữ để tìm kiếm các tệp</string> <string name="tv_add_tunnel_get_started">Thêm một tunnel để bắt đầu</string> + <string name="donate_title">❤️ Đóng góp cho Dự án Wireguard</string> + <string name="donate_summary">Mọi đóng góp đều giúp ích</string> + <string name="donate_google_play_disappointment">Cảm ơn bạn đã ủng hộ WireGuard!\n\nThật tiếc, dựa trên điều khoản của Google, chúng tôi không thể đưa vào liên kết dẫn đến trang đóng góp ở trang chủ của dự án. Mong rằng bạn có thể tìm cách cho việc này!\n\nXin cảm ơn bạn một lần nữa vì đã đóng góp.</string> <string name="disable_config_export_title">Vô hiệu hóa xuất cấu hình</string> <string name="disable_config_export_description">Vô hiệu hóa xuất cấu hình sẽ giúp giảm khả năng truy cập vào private keys</string> <string name="dns_servers">DNS servers</string> @@ -103,9 +106,34 @@ <string name="error_down">Có lỗi khi tắt tunnel: %s</string> <string name="error_fetching_apps">Lỗi khi lấy danh sách ứng dụng: %s</string> <string name="error_root">Vui lòng truy cập bằng quyền root và thử lại</string> + <string name="error_prepare">Lỗi khi chuẩn bị tunnel: %s</string> <string name="error_up">Có lỗi khi bật tunnel: %s</string> <string name="exclude_private_ips">Loại trừ IPs private</string> <string name="generate_new_private_key">Tạo private key mới</string> + <string name="generic_error">Lỗi \"%s\" không xác định</string> + <string name="hint_automatic">(tự động)</string> + <string name="hint_generated">(được tạo tự động)</string> + <string name="hint_optional">(tùy chọn)</string> + <string name="hint_optional_discouraged">(tùy chọn, không khuyến khích)</string> + <string name="hint_random">(ngẫu nhiên)</string> + <string name="illegal_filename_error">Tên file không hợp lệ \"%s\"</string> + <string name="import_error">Không thể nhập tunnel: %s</string> + <string name="import_from_qr_code">Nhập tunnel từ mã QR</string> + <string name="import_success">Đã nhập \"%s\"</string> + <string name="interface_title">Giao diện</string> + <string name="key_contents_error">Kí tự không hợp lệ trong khoá</string> + <string name="key_length_error">Độ dài khoá không hợp lệ</string> + <string name="key_length_explanation_base64">: Khoá WireGuard base64 phải đủ 44 ký tự (32 bytes)</string> + <string name="key_length_explanation_binary">: Khoá WireGuard phải đủ 32 bytes</string> + <string name="key_length_explanation_hex">: Khoá WireGuard hex phải đủ 64 ký tự (32 bytes)</string> + <string name="latest_handshake">Lần bắt tay cuối</string> + <string name="latest_handshake_ago">%s giây trước</string> + <string name="listen_port">Cổng</string> + <string name="log_export_error">Không thể xuất nhật ký: %s</string> + <string name="log_export_subject">File nhật ký WireGuard Android</string> + <string name="log_export_success">Đã lưu vào \"%s\"</string> + <string name="log_export_title">Xuất file nhật ký</string> + <string name="log_saver_activity_label">Lưu nhật ký</string> <string name="parse_error_inet_address">Địa chỉ IP</string> <string name="peer">Đồng trang lứa</string> </resources> diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml index 4b30afca..7757e2fe 100644 --- a/ui/src/main/res/values-zh-rCN/strings.xml +++ b/ui/src/main/res/values-zh-rCN/strings.xml @@ -171,6 +171,10 @@ <string name="private_key">私钥</string> <string name="public_key">公钥</string> <string name="qr_code_hint">提示:使用命令 `qrencode -t ansiutf8 < tunnel.conf` 生成二维码</string> + <string name="quick_settings_tile_add_title">添加磁贴到快速设置面板</string> + <string name="quick_settings_tile_add_summary">通过快捷磁贴开启/关闭上次使用的隧道</string> + <string name="quick_settings_tile_add_failure">无法添加快捷磁贴:错误 %d</string> + <string name="quick_settings_tile_action">开启/关闭隧道</string> <string name="restore_on_boot_summary_off">未启用</string> <string name="restore_on_boot_summary_on">设备启动时自动开启上次使用的隧道</string> <string name="restore_on_boot_title">启动时恢复</string> @@ -215,6 +219,16 @@ <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">WireGuard 可以更新了,请立即更新。</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="updater_corrupt_title">应用损坏</string> + <string name="updater_corrupt_message">此应用已损坏。请从下方链接的网站中重新下载 APK,然后卸载此应用并重新安装。</string> + <string name="updater_corrupt_navigate">打开网站</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..1cb642e2 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -67,6 +67,7 @@ <string name="bad_config_context">%1$s\'s %2$s</string> <string name="bad_config_context_top_level">%s</string> <string name="bad_config_error">%1$s in %2$s</string> + <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string> <string name="bad_config_explanation_pka">: Must be positive and no more than 65535</string> <string name="bad_config_explanation_positive_number">: Must be positive</string> <string name="bad_config_explanation_udp_port">: Must be a valid UDP port number</string> @@ -104,6 +105,7 @@ <string name="dark_theme_summary_on">Currently using dark (night) theme</string> <string name="dark_theme_title">Use dark theme</string> <string name="delete">Delete</string> + <string name="dynamic_addresses">Dynamic addresses</string> <string name="tv_delete">Select tunnel to delete</string> <string name="tv_select_a_storage_drive">Select a storage drive</string> <string name="tv_no_file_picker">Please install a file management utility to browse files</string> @@ -130,6 +132,10 @@ <string name="hint_optional">(optional)</string> <string name="hint_optional_discouraged">(optional, not recommended)</string> <string name="hint_random">(random)</string> + <string name="http_proxy">Proxy</string> + <string name="http_proxy_hostname">Proxy hostname</string> + <string name="http_proxy_pac">Proxy Auto-Config URL</string> + <string name="http_proxy_port">Proxy port</string> <string name="illegal_filename_error">Illegal file name “%s”</string> <string name="import_error">Unable to import tunnel: %s</string> <string name="import_from_qr_code">Import Tunnel from QR Code</string> @@ -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 < 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,9 @@ <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_title">Application Corrupt</string> + <string name="updater_corrupt_message">This application is corrupt. Please re-download the APK from the website linked below. After, uninstall this application, and reinstall it from the downloaded APK.</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> |