diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2019-11-20 11:44:34 +0100 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2019-11-20 12:44:54 +0100 |
commit | 8b0123042f54d843301fbef458b249fcf12466d2 (patch) | |
tree | b3e7bd9f143568666d21014eafe544affb0a89b7 | |
parent | 16890a659e6d7877b86e870fe4f0ef9cc19aee5b (diff) |
Implement statistics
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
-rw-r--r-- | app/src/main/java/com/wireguard/android/backend/GoBackend.java | 44 | ||||
-rw-r--r-- | app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java | 21 | ||||
-rw-r--r-- | app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java | 88 | ||||
-rw-r--r-- | app/src/main/java/com/wireguard/android/model/Tunnel.java | 55 | ||||
-rw-r--r-- | app/src/main/java/com/wireguard/crypto/Key.java | 19 | ||||
-rw-r--r-- | app/src/main/res/layout/tunnel_detail_fragment.xml | 1 | ||||
-rw-r--r-- | app/src/main/res/layout/tunnel_detail_peer.xml | 18 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 7 | ||||
-rw-r--r-- | app/tools/libwg-go/api-android.go | 17 | ||||
-rw-r--r-- | app/tools/libwg-go/jni.c | 12 | ||||
-rw-r--r-- | build.gradle | 2 |
11 files changed, 277 insertions, 7 deletions
diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java index e85f2b0d..3e8e1ec9 100644 --- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -24,6 +24,8 @@ import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.config.Config; import com.wireguard.config.InetNetwork; import com.wireguard.config.Peer; +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; import java.net.InetAddress; import java.util.Collections; @@ -47,6 +49,8 @@ public final class GoBackend implements Backend { this.context = context; } + private static native String wgGetConfig(int handle); + private static native int wgGetSocketV4(int handle); private static native int wgGetSocketV6(int handle); @@ -90,7 +94,45 @@ public final class GoBackend implements Backend { @Override public Statistics getStatistics(final Tunnel tunnel) { - return new Statistics(); + final Statistics stats = new Statistics(); + if (tunnel != currentTunnel) { + return stats; + } + final String config = wgGetConfig(currentTunnelHandle); + Key key = null; + long rx = 0, tx = 0; + for (final String line : config.split("\\n")) { + if (line.startsWith("public_key=")) { + if (key != null) + stats.add(key, rx, tx); + rx = 0; + tx = 0; + try { + key = Key.fromHex(line.substring(11)); + } catch (final KeyFormatException ignored) { + key = null; + } + } else if (line.startsWith("rx_bytes=")) { + if (key == null) + continue; + try { + rx = Long.parseLong(line.substring(9)); + } catch (final NumberFormatException ignored) { + rx = 0; + } + } else if (line.startsWith("tx_bytes=")) { + if (key == null) + continue; + try { + tx = Long.parseLong(line.substring(9)); + } catch (final NumberFormatException ignored) { + tx = 0; + } + } + } + if (key != null) + stats.add(key, rx, tx); + return stats; } @Override diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index 71427d8a..99b90af2 100644 --- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -15,11 +15,13 @@ import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.Statistics; import com.wireguard.config.Config; +import com.wireguard.crypto.Key; import java.io.File; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -83,7 +85,24 @@ public final class WgQuickBackend implements Backend { @Override public Statistics getStatistics(final Tunnel tunnel) { - return new Statistics(); + final Statistics stats = new Statistics(); + final Collection<String> output = new ArrayList<>(); + try { + if (Application.getRootShell().run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0) + return stats; + } catch (final Exception ignored) { + return stats; + } + for (final String line : output) { + final String[] parts = line.split("\\t"); + if (parts.length != 3) + continue; + try { + stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2])); + } catch (final Exception ignored) { + } + } + return stats; } @Override diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java index 8d2be476..f28a7b67 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java @@ -7,6 +7,8 @@ package com.wireguard.android.fragment; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; + import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -15,7 +17,13 @@ import android.view.ViewGroup; import com.wireguard.android.R; import com.wireguard.android.databinding.TunnelDetailFragmentBinding; +import com.wireguard.android.databinding.TunnelDetailPeerBinding; import com.wireguard.android.model.Tunnel; +import com.wireguard.android.model.Tunnel.State; +import com.wireguard.crypto.Key; + +import java.util.Timer; +import java.util.TimerTask; /** * Fragment that shows details about a specific tunnel. @@ -23,6 +31,20 @@ import com.wireguard.android.model.Tunnel; public class TunnelDetailFragment extends BaseFragment { @Nullable private TunnelDetailFragmentBinding binding; + @Nullable private Timer timer; + @Nullable private State lastState = State.TOGGLE; + + private static class StatsTimerTask extends TimerTask { + final TunnelDetailFragment tdf; + private StatsTimerTask(final TunnelDetailFragment tdf) { + this.tdf = tdf; + } + + @Override + public void run() { + tdf.updateStats(); + } + } @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -36,6 +58,22 @@ public class TunnelDetailFragment extends BaseFragment { } @Override + public void onStop() { + super.onStop(); + if (timer != null) { + timer.cancel(); + timer = null; + } + } + + @Override + public void onResume() { + super.onResume(); + timer = new Timer(); + timer.scheduleAtFixedRate(new StatsTimerTask(this), 0, 1000); + } + + @Override public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); @@ -59,6 +97,8 @@ public class TunnelDetailFragment extends BaseFragment { binding.setConfig(null); else newTunnel.getConfigAsync().thenAccept(binding::setConfig); + lastState = State.TOGGLE; + updateStats(); } @Override @@ -72,4 +112,52 @@ public class TunnelDetailFragment extends BaseFragment { super.onViewStateRestored(savedInstanceState); } + private String formatBytes(final long bytes) { + if (bytes < 1024) + return getContext().getString(R.string.transfer_bytes, bytes); + else if (bytes < 1024*1024) + return getContext().getString(R.string.transfer_kibibytes, bytes/1024.0); + else if (bytes < 1024*1024*1024) + return getContext().getString(R.string.transfer_mibibytes, bytes/(1024.0*1024.0)); + else if (bytes < 1024*1024*1024*1024) + return getContext().getString(R.string.transfer_gibibytes, bytes/(1024.0*1024.0*1024.0)); + return getContext().getString(R.string.transfer_tibibytes, bytes/(1024.0*1024.0*1024.0)/1024.0); + } + + private void updateStats() { + if (binding == null || !isResumed()) + return; + final State state = binding.getTunnel().getState(); + if (state != State.UP && lastState == state) + return; + lastState = state; + binding.getTunnel().getStatisticsAsync().whenComplete((statistics, throwable) -> { + if (throwable != null) { + for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) { + final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)); + if (peer == null) + continue; + peer.transferLabel.setVisibility(View.GONE); + peer.transferText.setVisibility(View.GONE); + } + return; + } + for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) { + final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)); + if (peer == null) + continue; + final Key publicKey = peer.getItem().getPublicKey(); + final long rx = statistics.peerRx(publicKey); + final long tx = statistics.peerTx(publicKey); + if (rx == 0 && tx == 0) { + peer.transferLabel.setVisibility(View.GONE); + peer.transferText.setVisibility(View.GONE); + continue; + } + peer.transferText.setText(getContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))); + peer.transferLabel.setVisibility(View.VISIBLE); + peer.transferText.setVisibility(View.VISIBLE); + } + }); + } } diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/Tunnel.java index 49e78a22..87b607d0 100644 --- a/app/src/main/java/com/wireguard/android/model/Tunnel.java +++ b/app/src/main/java/com/wireguard/android/model/Tunnel.java @@ -5,6 +5,9 @@ package com.wireguard.android.model; +import android.os.SystemClock; +import android.util.Pair; + import androidx.databinding.BaseObservable; import androidx.databinding.Bindable; import androidx.annotation.Nullable; @@ -12,8 +15,11 @@ import androidx.annotation.Nullable; import com.wireguard.android.BR; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.config.Config; +import com.wireguard.crypto.Key; import com.wireguard.util.Keyed; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Pattern; import java9.util.concurrent.CompletableFuture; @@ -85,15 +91,13 @@ public class Tunnel extends BaseObservable implements Keyed<String> { @Bindable @Nullable public Statistics getStatistics() { - // FIXME: Check age of statistics. - if (statistics == null) + if (statistics == null || statistics.isStale()) TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E); return statistics; } public CompletionStage<Statistics> getStatisticsAsync() { - // FIXME: Check age of statistics. - if (statistics == null) + if (statistics == null || statistics.isStale()) return TunnelManager.getTunnelStatistics(this); return CompletableFuture.completedFuture(statistics); } @@ -154,5 +158,48 @@ public class Tunnel extends BaseObservable implements Keyed<String> { } public static class Statistics extends BaseObservable { + private long lastTouched = SystemClock.elapsedRealtime(); + private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); + + public void add(final Key key, final long rx, final long tx) { + peerBytes.put(key, Pair.create(rx, tx)); + lastTouched = SystemClock.elapsedRealtime(); + } + + private boolean isStale() { + return SystemClock.elapsedRealtime() - lastTouched > 900; + } + + public Key[] peers() { + return peerBytes.keySet().toArray(new Key[0]); + } + + public long peerRx(final Key peer) { + if (!peerBytes.containsKey(peer)) + return 0; + return peerBytes.get(peer).first; + } + + public long peerTx(final Key peer) { + if (!peerBytes.containsKey(peer)) + return 0; + return peerBytes.get(peer).second; + } + + public long totalRx() { + long rx = 0; + for (final Pair<Long, Long> val : peerBytes.values()) { + rx += val.first; + } + return rx; + } + + public long totalTx() { + long tx = 0; + for (final Pair<Long, Long> val : peerBytes.values()) { + tx += val.second; + } + return tx; + } } } diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java index f743ddd2..6648a5f3 100644 --- a/app/src/main/java/com/wireguard/crypto/Key.java +++ b/app/src/main/java/com/wireguard/crypto/Key.java @@ -7,6 +7,7 @@ package com.wireguard.crypto; import com.wireguard.crypto.KeyFormatException.Type; +import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; @@ -247,6 +248,24 @@ public final class Key { return new String(output); } + @Override + public int hashCode() { + int ret = 0; + for (int i = 0; i < key.length / 4; ++i) + ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24); + return ret; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != getClass()) + return false; + final Key other = (Key) obj; + return MessageDigest.isEqual(key, other.key); + } + /** * The supported formats for encoding a WireGuard key. */ diff --git a/app/src/main/res/layout/tunnel_detail_fragment.xml b/app/src/main/res/layout/tunnel_detail_fragment.xml index 34332c2f..23056ecf 100644 --- a/app/src/main/res/layout/tunnel_detail_fragment.xml +++ b/app/src/main/res/layout/tunnel_detail_fragment.xml @@ -125,6 +125,7 @@ </androidx.cardview.widget.CardView> <LinearLayout + android:id="@+id/peers_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="4dp" diff --git a/app/src/main/res/layout/tunnel_detail_peer.xml b/app/src/main/res/layout/tunnel_detail_peer.xml index f528a8df..0f7ae336 100644 --- a/app/src/main/res/layout/tunnel_detail_peer.xml +++ b/app/src/main/res/layout/tunnel_detail_peer.xml @@ -89,6 +89,24 @@ android:layout_height="wrap_content" android:layout_below="@+id/endpoint_label" android:text="@{item.endpoint}" /> + + <TextView + android:id="@+id/transfer_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/endpoint_text" + android:layout_marginTop="8dp" + android:labelFor="@+id/transfer_text" + android:text="@string/transfer" + android:visibility="gone" /> + + <TextView + android:id="@+id/transfer_text" + style="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/transfer_label" + android:visibility="gone" /> </RelativeLayout> </androidx.cardview.widget.CardView> </layout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33470706..336cd4ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,6 +144,13 @@ <string name="tools_installer_title">Install command line tools</string> <string name="tools_installer_working">Installing wg and wg-quick</string> <string name="tools_unavailable_error">Required tools unavailable</string> + <string name="transfer">Transfer</string> + <string name="transfer_rx_tx">rx: %s, tx: %s</string> + <string name="transfer_bytes">%d B</string> + <string name="transfer_kibibytes">%.2f KiB</string> + <string name="transfer_mibibytes">%.2f MiB</string> + <string name="transfer_gibibytes">%.2f GiB</string> + <string name="transfer_tibibytes">%.2f TiB</string> <string name="tun_create_error">Unable to create tun device</string> <string name="tunnel_config_error">Unable to configure tunnel (wg-quick returned %d)</string> <string name="tunnel_create_error">Unable to create tunnel: %s</string> diff --git a/app/tools/libwg-go/api-android.go b/app/tools/libwg-go/api-android.go index 7e951b9c..7a393cae 100644 --- a/app/tools/libwg-go/api-android.go +++ b/app/tools/libwg-go/api-android.go @@ -15,6 +15,7 @@ import ( "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/ipc" "golang.zx2c4.com/wireguard/tun" + "bytes" "log" "math" "net" @@ -168,6 +169,22 @@ func wgGetSocketV6(tunnelHandle int32) int32 { return int32(fd) } +//export wgGetConfig +func wgGetConfig(tunnelHandle int32) *C.char { + handle, ok := tunnelHandles[tunnelHandle] + if !ok { + return nil + } + settings := new(bytes.Buffer) + writer := bufio.NewWriter(settings) + err := handle.device.IpcGetOperation(writer) + if err != nil { + return nil + } + writer.Flush() + return C.CString(settings.String()) +} + //export wgVersion func wgVersion() *C.char { return C.CString(device.WireGuardGoVersion) diff --git a/app/tools/libwg-go/jni.c b/app/tools/libwg-go/jni.c index f6229a49..3f877d47 100644 --- a/app/tools/libwg-go/jni.c +++ b/app/tools/libwg-go/jni.c @@ -12,6 +12,7 @@ extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settin extern void wgTurnOff(int handle); extern int wgGetSocketV4(int handle); extern int wgGetSocketV6(int handle); +extern char *wgGetConfig(int handle); extern char *wgVersion(); JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings) @@ -47,6 +48,17 @@ JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV return wgGetSocketV6(handle); } +JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle) +{ + jstring ret; + char *config = wgGetConfig(handle); + if (!config) + return NULL; + ret = (*env)->NewStringUTF(env, config); + free(config); + return ret; +} + JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c) { jstring ret; diff --git a/build.gradle b/build.gradle index e22f92cf..aa7a4692 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ allprojects { buildscript { dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' } repositories { google() |