Implement statistics

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2019-11-20 11:44:34 +01:00
parent 16890a659e
commit 8b0123042f
11 changed files with 277 additions and 7 deletions

View File

@ -24,6 +24,8 @@ import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork; import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer; import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Collections; import java.util.Collections;
@ -47,6 +49,8 @@ public final class GoBackend implements Backend {
this.context = context; this.context = context;
} }
private static native String wgGetConfig(int handle);
private static native int wgGetSocketV4(int handle); private static native int wgGetSocketV4(int handle);
private static native int wgGetSocketV6(int handle); private static native int wgGetSocketV6(int handle);
@ -90,7 +94,45 @@ public final class GoBackend implements Backend {
@Override @Override
public Statistics getStatistics(final Tunnel tunnel) { 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 @Override

View File

@ -15,11 +15,13 @@ import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics; import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -83,7 +85,24 @@ public final class WgQuickBackend implements Backend {
@Override @Override
public Statistics getStatistics(final Tunnel tunnel) { 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 @Override

View File

@ -7,6 +7,8 @@ package com.wireguard.android.fragment;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -15,7 +17,13 @@ import android.view.ViewGroup;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelDetailPeerBinding;
import com.wireguard.android.model.Tunnel; 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. * Fragment that shows details about a specific tunnel.
@ -23,6 +31,20 @@ import com.wireguard.android.model.Tunnel;
public class TunnelDetailFragment extends BaseFragment { public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding; @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 @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
@ -35,6 +57,22 @@ public class TunnelDetailFragment extends BaseFragment {
inflater.inflate(R.menu.tunnel_detail, menu); inflater.inflate(R.menu.tunnel_detail, menu);
} }
@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 @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
@ -59,6 +97,8 @@ public class TunnelDetailFragment extends BaseFragment {
binding.setConfig(null); binding.setConfig(null);
else else
newTunnel.getConfigAsync().thenAccept(binding::setConfig); newTunnel.getConfigAsync().thenAccept(binding::setConfig);
lastState = State.TOGGLE;
updateStats();
} }
@Override @Override
@ -72,4 +112,52 @@ public class TunnelDetailFragment extends BaseFragment {
super.onViewStateRestored(savedInstanceState); 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);
}
});
}
} }

View File

@ -5,6 +5,9 @@
package com.wireguard.android.model; package com.wireguard.android.model;
import android.os.SystemClock;
import android.util.Pair;
import androidx.databinding.BaseObservable; import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable; import androidx.databinding.Bindable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -12,8 +15,11 @@ import androidx.annotation.Nullable;
import com.wireguard.android.BR; import com.wireguard.android.BR;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import com.wireguard.util.Keyed; import com.wireguard.util.Keyed;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java9.util.concurrent.CompletableFuture; import java9.util.concurrent.CompletableFuture;
@ -85,15 +91,13 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
@Bindable @Bindable
@Nullable @Nullable
public Statistics getStatistics() { public Statistics getStatistics() {
// FIXME: Check age of statistics. if (statistics == null || statistics.isStale())
if (statistics == null)
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E); TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics; return statistics;
} }
public CompletionStage<Statistics> getStatisticsAsync() { public CompletionStage<Statistics> getStatisticsAsync() {
// FIXME: Check age of statistics. if (statistics == null || statistics.isStale())
if (statistics == null)
return TunnelManager.getTunnelStatistics(this); return TunnelManager.getTunnelStatistics(this);
return CompletableFuture.completedFuture(statistics); return CompletableFuture.completedFuture(statistics);
} }
@ -154,5 +158,48 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
} }
public static class Statistics extends BaseObservable { 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;
}
} }
} }

View File

@ -7,6 +7,7 @@ package com.wireguard.crypto;
import com.wireguard.crypto.KeyFormatException.Type; import com.wireguard.crypto.KeyFormatException.Type;
import java.security.MessageDigest;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
@ -247,6 +248,24 @@ public final class Key {
return new String(output); 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. * The supported formats for encoding a WireGuard key.
*/ */

View File

@ -125,6 +125,7 @@
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<LinearLayout <LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"

View File

@ -89,6 +89,24 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_label" android:layout_below="@+id/endpoint_label"
android:text="@{item.endpoint}" /> 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> </RelativeLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</layout> </layout>

View File

@ -144,6 +144,13 @@
<string name="tools_installer_title">Install command line tools</string> <string name="tools_installer_title">Install command line tools</string>
<string name="tools_installer_working">Installing wg and wg-quick</string> <string name="tools_installer_working">Installing wg and wg-quick</string>
<string name="tools_unavailable_error">Required tools unavailable</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="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_config_error">Unable to configure tunnel (wg-quick returned %d)</string>
<string name="tunnel_create_error">Unable to create tunnel: %s</string> <string name="tunnel_create_error">Unable to create tunnel: %s</string>

View File

@ -15,6 +15,7 @@ import (
"golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc" "golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"bytes"
"log" "log"
"math" "math"
"net" "net"
@ -168,6 +169,22 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
return int32(fd) 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 //export wgVersion
func wgVersion() *C.char { func wgVersion() *C.char {
return C.CString(device.WireGuardGoVersion) return C.CString(device.WireGuardGoVersion)

View File

@ -12,6 +12,7 @@ extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settin
extern void wgTurnOff(int handle); extern void wgTurnOff(int handle);
extern int wgGetSocketV4(int handle); extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle); extern int wgGetSocketV6(int handle);
extern char *wgGetConfig(int handle);
extern char *wgVersion(); extern char *wgVersion();
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings) 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); 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) JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
{ {
jstring ret; jstring ret;

View File

@ -7,7 +7,7 @@ allprojects {
buildscript { buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.1'
} }
repositories { repositories {
google() google()