@@ -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 | |||
@@ -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 | |||
@@ -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); | |||
} | |||
}); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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. | |||
*/ | |||
@@ -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" | |||
@@ -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> |
@@ -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> | |||
@@ -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,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; | |||
@@ -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() | |||