Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ import com.tailscale.ipn.util.NoSuchKeyException
import com.tailscale.ipn.util.ShareFileHelper
import com.tailscale.ipn.util.TSLog
import java.io.IOException
import java.lang.UnsupportedOperationException
import java.net.NetworkInterface
import java.security.GeneralSecurityException
import java.util.Locale
import java.util.Collections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -54,8 +56,9 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.Serializable
import libtailscale.Libtailscale
import java.lang.UnsupportedOperationException
class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

Expand Down Expand Up @@ -333,6 +336,60 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
return sb.toString()
}

@Serializable
data class AddrJson(
val ip: String,
val prefixLen: Int,
)

@Serializable
data class InterfaceJson(
val name: String,
val index: Int,
val mtu: Int,
val up: Boolean,
val broadcast: Boolean,
val loopback: Boolean,
val pointToPoint: Boolean,
val multicast: Boolean,
val addrs: List<AddrJson>,
)
override fun getInterfacesAsJson(): String {
val interfaces = Collections.list(NetworkInterface.getNetworkInterfaces())
val out = ArrayList<InterfaceJson>(interfaces.size)

for (nif in interfaces) {
try {
val addrs = ArrayList<AddrJson>()
for (ia in nif.interfaceAddresses) {
val addr = ia.address ?: continue
// hostAddress is stable and avoids InterfaceAddress.toString() formatting risks.
val host = addr.hostAddress ?: continue
addrs.add(AddrJson(ip = host, prefixLen = ia.networkPrefixLength.toInt()))
}

out.add(
InterfaceJson(
name = nif.name,
index = nif.index,
mtu = nif.mtu,
up = nif.isUp,
broadcast = nif.supportsMulticast(),
loopback = nif.isLoopback,
pointToPoint = nif.isPointToPoint,
multicast = nif.supportsMulticast(),
addrs = addrs,
)
)
} catch (_: Exception) {
continue
}
}

// Avoid pretty printing to keep payload small.
return Json { encodeDefaults = true }.encodeToString(out)
}

@Throws(
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
override fun getSyspolicyBooleanValue(key: String): Boolean {
Expand Down
141 changes: 141 additions & 0 deletions libtailscale/ifaceparse/ifaceparse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package ifaceparse

import (
"encoding/json"
"errors"
"net"
"net/netip"
"slices"
"strings"

"tailscale.com/net/netmon"
)

type ParseStats struct {
IfacesTotal int
IfacesParsed int
IfacesSkipped int
AddrsTotal int
AddrsParsed int
AddrsSkipped int
}

type addrJSON struct {
IP string `json:"ip"`
PrefixLen int `json:"prefixLen"`
}
type ifaceJSON struct {
Name string `json:"name"`
Index int `json:"index"`
MTU int `json:"mtu"`
Up bool `json:"up"`
Broadcast bool `json:"broadcast"`
Loopback bool `json:"loopback"`
PointToPt bool `json:"pointToPoint"`
Multicast bool `json:"multicast"`
Addrs []addrJSON `json:"addrs"`
}

var ErrNotJSON = errors.New("not a JSON interfaces payload")

// ParseInterfacesJSONAsNetmon parses a JSON payload produced by getInterfacesAsJson()
// and returns netmon.Interfaces plus parsing stats.
func ParseInterfacesJSONAsNetmon(b []byte) ([]netmon.Interface, ParseStats, error) {
var st ParseStats
trim := strings.TrimSpace(string(b))
if trim == "" {
return nil, st, nil
}
if !(strings.HasPrefix(trim, "[") || strings.HasPrefix(trim, "{")) {
return nil, st, ErrNotJSON
}

var in []ifaceJSON
if err := json.Unmarshal([]byte(trim), &in); err != nil {
return nil, st, err
}

out := make([]netmon.Interface, 0, len(in))
for _, it := range in {
st.IfacesTotal++

if it.Name == "" {
st.IfacesSkipped++
continue
}

nif := netmon.Interface{
Interface: &net.Interface{
Name: it.Name,
Index: it.Index,
MTU: it.MTU,
},
AltAddrs: []net.Addr{},
}

if it.Up {
nif.Flags |= net.FlagUp
}
if it.Broadcast {
nif.Flags |= net.FlagBroadcast
}
if it.Loopback {
nif.Flags |= net.FlagLoopback
}
if it.PointToPt {
nif.Flags |= net.FlagPointToPoint
}
if it.Multicast {
nif.Flags |= net.FlagMulticast
}

st.AddrsTotal += len(it.Addrs)
for _, a := range it.Addrs {
na, err := a.NetAddr()
if err != nil {
st.AddrsSkipped++
continue
}
nif.AltAddrs = append(nif.AltAddrs, na)
st.AddrsParsed++
}

out = append(out, nif)
st.IfacesParsed++
}

return out, st, nil
}

func (a addrJSON) NetAddr() (net.Addr, error) {
na, err := netip.ParseAddr(a.IP)
if err != nil {
return nil, err
}

zone := na.Zone()
ip := net.IP(slices.Clone(na.AsSlice()))

// Zoned addresses can't be represented as *net.IPNet.
if zone != "" {
return &net.IPAddr{IP: ip, Zone: zone}, nil
}

bits := 128
if na.Is4() {
bits = 32
}
if a.PrefixLen < 0 || a.PrefixLen > bits {
// Keep the IP but drop the prefix if it's invalid.
return &net.IPAddr{IP: ip}, nil
}

// Host IP + prefixLen mask (not prefix base).
return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(a.PrefixLen, bits),
}, nil
}
Loading