From 7f58506d3eaff8c26fad336a6fc3caa97e287dd3 Mon Sep 17 00:00:00 2001 From: Mikael Magnusson Date: Fri, 2 Jun 2023 22:50:10 +0200 Subject: tunnel: renew DHCP leases * Add valid and preferred lifetime to dhcp leases * Delay renew until VPN network is available --- .../java/com/wireguard/android/backend/Dhcp.java | 45 +++++++- .../com/wireguard/android/backend/GoBackend.java | 121 ++++++++++++++++++--- tunnel/tools/libwg-go/dhcp.go | 9 +- .../android/databinding/BindingAdapters.kt | 7 ++ ui/src/main/res/layout/tunnel_detail_fragment.xml | 2 +- 5 files changed, 160 insertions(+), 24 deletions(-) diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java index 59a3e69c..41060ddf 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java @@ -8,6 +8,11 @@ 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; /** @@ -15,14 +20,44 @@ import java.util.Set; */ @NonNullForAll public class Dhcp { - private Set addresses; + private Set leases = new LinkedHashSet<>(); - Dhcp(Set addresses) { - this.addresses = addresses; + 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 Set getAddresses() { - return addresses; + public final Set getLeases() { + return leases; } public String toString() { 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 6bd42936..79b724e8 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -5,6 +5,7 @@ package com.wireguard.android.backend; +import android.app.AlarmManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -18,8 +19,12 @@ 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; @@ -75,10 +80,10 @@ 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.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -104,6 +109,7 @@ 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; @Nullable private static AlwaysOnCallback alwaysOnCallback; private static GhettoCompletableFuture vpnService = new GhettoCompletableFuture<>(); private final Context context; @@ -117,6 +123,8 @@ public final class GoBackend implements Backend { private ManagedChannel channel; private boolean obtainDhcpLease = false; @Nullable private Bgp bgp; + private HandlerThread thread; + private Handler handler; /** * Public constructor for GoBackend. @@ -398,6 +406,10 @@ public final class GoBackend implements Backend { } 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 @@ -421,23 +433,27 @@ public final class GoBackend implements Backend { DhcpResponse resp = stub.dhcp(request); Log.i(TAG, "Dhcp: " + resp.getError().getMessage()); - Set addresses = new LinkedHashSet<>(); + 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); - addresses.add(new InetNetwork(addr, 128)); + 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 } } } - Dhcp dhcp = new Dhcp(addresses); - // Replace the vpn tunnel - final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getAddresses()); + final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getLeases()); Log.i(TAG, "Builder: " + builder); @@ -455,8 +471,14 @@ public final class GoBackend implements Backend { service.protect(wgGetSocketV6(currentTunnelHandle)); Log.i(TAG, "Dhcp done"); - bgp = new Bgp(this, channel, currentTunnel, currentTunnelHandle); - bgp.startServer(); + 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); } @@ -543,7 +565,7 @@ public final class GoBackend implements Backend { Log.i(TAG, "Exit streamReverse"); } - private VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set leases) throws PackageManager.NameNotFoundException { + private VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set leases) throws PackageManager.NameNotFoundException { Log.i(TAG, "Builder 1"); final VpnService.Builder builder = service.getBuilder(); Log.i(TAG, "Builder 2"); @@ -559,8 +581,9 @@ public final class GoBackend implements Backend { Log.i(TAG, "Builder 5"); if (leases != null) { - for (final InetNetwork lease: leases) { - builder.addAddress(lease.getAddress(), lease.getMask()); + for (final Dhcp.Lease lease: leases) { + InetNetwork addr = lease.getAddress(); + builder.addAddress(addr.getAddress(), addr.getMask()); } } @@ -659,7 +682,11 @@ public final class GoBackend implements Backend { activeNetwork = connectivityManager.getActiveNetwork(); - if (!connectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + 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; } @@ -708,6 +735,33 @@ public final class GoBackend implements Backend { 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; + 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); @@ -723,6 +777,14 @@ 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; @@ -802,6 +864,14 @@ public final class GoBackend implements Backend { 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) { @@ -847,7 +917,7 @@ public final class GoBackend implements Backend { @Override public void onAvailable(Network network) { Log.w(TAG, "VPN onAvailable: " + network); - if (obtainDhcpLease) { + if (obtainDhcpLease && activeNetwork != null) { Log.w(TAG, "Obtaindhcplease"); try { Log.w(TAG, "Before Dhcp"); @@ -867,6 +937,20 @@ public final class GoBackend implements Backend { 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))); @@ -889,4 +973,13 @@ public final class GoBackend implements Backend { } } } + + private AlarmManager.OnAlarmListener alarmListener = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + if (handler != null) { + handler.sendEmptyMessage(MSG_DHCP_EXPIRE); + } + } + }; } diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go index b7919151..4af5b0cb 100644 --- a/tunnel/tools/libwg-go/dhcp.go +++ b/tunnel/tools/libwg-go/dhcp.go @@ -12,6 +12,7 @@ import ( "github.com/insomniacslk/dhcp/iana" gen "golang.zx2c4.com/wireguard/android/gen" + "google.golang.org/protobuf/types/known/durationpb" ) const ( @@ -131,13 +132,13 @@ func getAddressesFromReply(reply *dhcpv6.Message) []*gen.Lease{ ianaOpts := iana.Options.Get(dhcpv6.OptionIAAddr) for _, opt := range ianaOpts { - addr :=opt.(*dhcpv6.OptIAAddress).IPv6Addr + addrOpt := opt.(*dhcpv6.OptIAAddress) lease := &gen.Lease{ Address: &gen.InetAddress{ - Address: addr, + Address: addrOpt.IPv6Addr, }, - // PreferredLifetime: , - // ValidLifetime: , + PreferredLifetime: durationpb.New(addrOpt.PreferredLifetime), + ValidLifetime: durationpb.New(addrOpt.ValidLifetime), } leases = append(leases, lease) } diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt index afba41cb..6b2040e1 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.wireguard.android.BR import com.wireguard.android.R +import com.wireguard.android.backend.Dhcp; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler import com.wireguard.android.widget.ToggleSwitch import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener @@ -169,6 +170,12 @@ object BindingAdapters { view.text = if (strings != null) Attribute.join(strings) else "" } + @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) diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index 425b364d..9b17a06c 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -171,7 +171,7 @@ android:nextFocusDown="@id/dns_servers_text" android:nextFocusForward="@id/dns_servers_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{tunnel.dhcp.addresses}" + android:text="@{tunnel.dhcp}" android:textAppearance="?attr/textAppearanceBodyLarge" android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" -- cgit v1.2.3