Browse Source

Implement statistics

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
master
Jason A. Donenfeld 4 years ago
parent
commit
8b0123042f
11 changed files with 277 additions and 7 deletions
  1. +43
    -1
      app/src/main/java/com/wireguard/android/backend/GoBackend.java
  2. +20
    -1
      app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
  3. +88
    -0
      app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java
  4. +51
    -4
      app/src/main/java/com/wireguard/android/model/Tunnel.java
  5. +19
    -0
      app/src/main/java/com/wireguard/crypto/Key.java
  6. +1
    -0
      app/src/main/res/layout/tunnel_detail_fragment.xml
  7. +18
    -0
      app/src/main/res/layout/tunnel_detail_peer.xml
  8. +7
    -0
      app/src/main/res/values/strings.xml
  9. +17
    -0
      app/tools/libwg-go/api-android.go
  10. +12
    -0
      app/tools/libwg-go/jni.c
  11. +1
    -1
      build.gradle

+ 43
- 1
app/src/main/java/com/wireguard/android/backend/GoBackend.java View File

@@ -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


+ 20
- 1
app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java 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.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


+ 88
- 0
app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java View File

@@ -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) {
@@ -35,6 +57,22 @@ public class TunnelDetailFragment extends BaseFragment {
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
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle 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);
}
});
}
}

+ 51
- 4
app/src/main/java/com/wireguard/android/model/Tunnel.java View File

@@ -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;
}
}
}

+ 19
- 0
app/src/main/java/com/wireguard/crypto/Key.java View File

@@ -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.
*/


+ 1
- 0
app/src/main/res/layout/tunnel_detail_fragment.xml View File

@@ -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"


+ 18
- 0
app/src/main/res/layout/tunnel_detail_peer.xml View File

@@ -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>

+ 7
- 0
app/src/main/res/values/strings.xml View File

@@ -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>


+ 17
- 0
app/tools/libwg-go/api-android.go View File

@@ -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)


+ 12
- 0
app/tools/libwg-go/jni.c 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 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;


+ 1
- 1
build.gradle View File

@@ -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()


Loading…
Cancel
Save