diff --git a/android/build.gradle b/android/build.gradle index 7361fc7..69c5cad 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,7 +24,7 @@ android { compileSdkVersion 30 defaultConfig { minSdkVersion 22 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 69 versionName "1.15.210-tbabd163aa-g9b52c6b357b" } diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index 8e3081a..7a472d5 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -39,8 +39,18 @@ import java.io.IOException; import java.io.File; import java.io.FileOutputStream; +import java.lang.StringBuilder; + +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; + import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -312,4 +322,54 @@ public class App extends Application { private static native void onConnectivityChanged(boolean connected); static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes); static native void onWriteStorageGranted(); + + // Returns details of the interfaces in the system, encoded as a single string for ease + // of JNI transfer over to the Go environment. + // + // Example: + // rmnet_data0 10 2000 true false false false false | fe80::4059:dc16:7ed3:9c6e%rmnet_data0/64 + // dummy0 3 1500 true false false false false | fe80::1450:5cff:fe13:f891%dummy0/64 + // wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24 + // r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64 + // rmnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64 + // r_rmnet_data1 22 1500 true false false false false | fe80::b6cd:5cb0:8ae6:fe92%r_rmnet_data1/64 + // rmnet_data1 11 1500 true false false false false | fe80::51f2:ee00:edce:d68b%rmnet_data1/64 + // lo 1 65536 true false true false false | ::1/128 127.0.0.1/8 + // v4-rmnet_data2 68 1472 true true false true true | 192.0.0.4/32 + // + // Where the fields are: + // name ifindex mtu isUp hasBroadcast isLoopback isPointToPoint hasMulticast | ip1/N ip2/N ip3/N; + String getInterfacesAsString() { + List interfaces; + try { + interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + } catch (Exception e) { + return ""; + } + + StringBuilder sb = new StringBuilder(""); + for (NetworkInterface nif : interfaces) { + try { + // Android doesn't have a supportsBroadcast() but the Go net.Interface wants + // one, so we say the interface has broadcast if it has multicast. + sb.append(String.format("%s %d %d %b %b %b %b %b |", nif.getName(), + nif.getIndex(), nif.getMTU(), nif.isUp(), nif.supportsMulticast(), + nif.isLoopback(), nif.isPointToPoint(), nif.supportsMulticast())); + + for (InterfaceAddress ia : nif.getInterfaceAddresses()) { + // InterfaceAddress == hostname + "/" + IP + String[] parts = ia.toString().split("/", 0); + if (parts.length > 1) { + sb.append(String.format("%s/%d ", parts[1], ia.getNetworkPrefixLength())); + } + } + } catch (Exception e) { + // TODO(dgentry) should log the exception not silently suppress it. + continue; + } + sb.append("\n"); + } + + return sb.toString(); + } } diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 2f8d006..72f8c1d 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -12,6 +12,7 @@ import ( "io" "log" "mime" + "net" "net/http" "net/url" "os" @@ -33,6 +34,7 @@ import ( "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" "tailscale.com/net/dns" + "tailscale.com/net/interfaces" "tailscale.com/net/netns" "tailscale.com/paths" "tailscale.com/tailcfg" @@ -218,6 +220,7 @@ func main() { fatalErr(err) } a.store = newStateStore(a.jvm, a.appCtx) + interfaces.RegisterInterfaceGetter(a.getInterfaces) go func() { if err := a.runBackend(); err != nil { fatalErr(err) @@ -1251,6 +1254,86 @@ func (a *App) contextForView(view jni.Object) jni.Object { return ctx } +// Report interfaces in the device in net.Interface format. +func (a *App) getInterfaces() ([]interfaces.Interface, error) { + var ifaceString string + err := jni.Do(a.jvm, func(env *jni.Env) error { + cls := jni.GetObjectClass(env, a.appCtx) + m := jni.GetMethodID(env, cls, "getInterfacesAsString", "()Ljava/lang/String;") + n, err := jni.CallObjectMethod(env, a.appCtx, m) + ifaceString = jni.GoString(env, jni.String(n)) + return err + + }) + var ifaces []interfaces.Interface + if err != nil { + return ifaces, err + } + + for _, iface := range strings.Split(ifaceString, "\n") { + // Example of the strings we're processing: + // wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24 + // r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64 + // mnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64 + + if strings.TrimSpace(iface) == "" { + continue + } + + fields := strings.Split(iface, "|") + if len(fields) != 2 { + log.Printf("getInterfaces: unable to split %q", iface) + continue + } + + var name string + var index, mtu int + var up, broadcast, loopback, pointToPoint, multicast bool + _, err := fmt.Sscanf(fields[0], "%s %d %d %t %t %t %t %t", + &name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast) + if err != nil { + log.Printf("getInterfaces: unable to parse %q: %v", iface, err) + continue + } + + newIf := interfaces.Interface{ + Interface: &net.Interface{ + Name: name, + Index: index, + MTU: mtu, + }, + AltAddrs: []net.Addr{}, // non-nil to avoid Go using netlink + } + if up { + newIf.Flags |= net.FlagUp + } + if broadcast { + newIf.Flags |= net.FlagBroadcast + } + if loopback { + newIf.Flags |= net.FlagLoopback + } + if pointToPoint { + newIf.Flags |= net.FlagPointToPoint + } + if multicast { + newIf.Flags |= net.FlagMulticast + } + + addrs := strings.Trim(fields[1], " \n") + for _, addr := range strings.Split(addrs, " ") { + ip, err := netaddr.ParseIPPrefix(addr) + if err == nil { + newIf.AltAddrs = append(newIf.AltAddrs, ip.IPNet()) + } + } + + ifaces = append(ifaces, newIf) + } + + return ifaces, nil +} + func fatalErr(err error) { // TODO: expose in UI. log.Printf("fatal error: %v", err)