Skip to content

Commit f6dc078

Browse files
committed
android: fix local endpoint parsing and add regression ifaceparse_test
Android reports interface addresses in CIDR form. Recent changes (see 9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105, 9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105) parsed addresses as CIDR prefixes, resulting in prefix base being advertised as a local endpoint. This change: -instead of parsing CIDR prefixes, the parser strips the /NN portion and uses netip.ParseAddr to parse the host IP only -extracts the interface parsing into a helper in a new package to make it testable on non-Android platforms -adds a unit test to verify that host addresses are present and prefix-base ones are not Fixes tailscale/tailscale#16836 Signed-off-by: kari-ts <kari@tailscale.com>
1 parent d0442f7 commit f6dc078

5 files changed

Lines changed: 437 additions & 74 deletions

File tree

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ import com.tailscale.ipn.util.NoSuchKeyException
4141
import com.tailscale.ipn.util.ShareFileHelper
4242
import com.tailscale.ipn.util.TSLog
4343
import java.io.IOException
44+
import java.lang.UnsupportedOperationException
4445
import java.net.NetworkInterface
4546
import java.security.GeneralSecurityException
4647
import java.util.Locale
48+
import java.util.Collections
4749
import kotlinx.coroutines.CoroutineScope
4850
import kotlinx.coroutines.Dispatchers
4951
import kotlinx.coroutines.SupervisorJob
@@ -54,8 +56,9 @@ import kotlinx.coroutines.flow.first
5456
import kotlinx.coroutines.launch
5557
import kotlinx.serialization.encodeToString
5658
import kotlinx.serialization.json.Json
59+
import kotlinx.serialization.encodeToString
60+
import kotlinx.serialization.Serializable
5761
import libtailscale.Libtailscale
58-
import java.lang.UnsupportedOperationException
5962
class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
6063
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
6164

@@ -333,6 +336,60 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
333336
return sb.toString()
334337
}
335338

339+
@Serializable
340+
data class AddrJson(
341+
val ip: String,
342+
val prefixLen: Int,
343+
)
344+
345+
@Serializable
346+
data class InterfaceJson(
347+
val name: String,
348+
val index: Int,
349+
val mtu: Int,
350+
val up: Boolean,
351+
val broadcast: Boolean,
352+
val loopback: Boolean,
353+
val pointToPoint: Boolean,
354+
val multicast: Boolean,
355+
val addrs: List<AddrJson>,
356+
)
357+
override fun getInterfacesAsJson(): String {
358+
val interfaces = Collections.list(NetworkInterface.getNetworkInterfaces())
359+
val out = ArrayList<InterfaceJson>(interfaces.size)
360+
361+
for (nif in interfaces) {
362+
try {
363+
val addrs = ArrayList<AddrJson>()
364+
for (ia in nif.interfaceAddresses) {
365+
val addr = ia.address ?: continue
366+
// hostAddress is stable and avoids InterfaceAddress.toString() formatting risks.
367+
val host = addr.hostAddress ?: continue
368+
addrs.add(AddrJson(ip = host, prefixLen = ia.networkPrefixLength.toInt()))
369+
}
370+
371+
out.add(
372+
InterfaceJson(
373+
name = nif.name,
374+
index = nif.index,
375+
mtu = nif.mtu,
376+
up = nif.isUp,
377+
broadcast = nif.supportsMulticast(),
378+
loopback = nif.isLoopback,
379+
pointToPoint = nif.isPointToPoint,
380+
multicast = nif.supportsMulticast(),
381+
addrs = addrs,
382+
)
383+
)
384+
} catch (_: Exception) {
385+
continue
386+
}
387+
}
388+
389+
// Avoid pretty printing to keep payload small.
390+
return Json { encodeDefaults = true }.encodeToString(out)
391+
}
392+
336393
@Throws(
337394
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
338395
override fun getSyspolicyBooleanValue(key: String): Boolean {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package ifaceparse
5+
6+
import (
7+
"encoding/json"
8+
"errors"
9+
"net"
10+
"net/netip"
11+
"strings"
12+
13+
"tailscale.com/net/netmon"
14+
)
15+
16+
type ParseStats struct {
17+
IfacesTotal int
18+
IfacesParsed int
19+
IfacesSkipped int
20+
AddrsTotal int
21+
AddrsParsed int
22+
AddrsSkipped int
23+
}
24+
25+
type addrJSON struct {
26+
IP string `json:"ip"`
27+
PrefixLen int `json:"prefixLen"`
28+
}
29+
type ifaceJSON struct {
30+
Name string `json:"name"`
31+
Index int `json:"index"`
32+
MTU int `json:"mtu"`
33+
Up bool `json:"up"`
34+
Broadcast bool `json:"broadcast"`
35+
Loopback bool `json:"loopback"`
36+
PointToPt bool `json:"pointToPoint"`
37+
Multicast bool `json:"multicast"`
38+
Addrs []addrJSON `json:"addrs"`
39+
}
40+
41+
var ErrNotJSON = errors.New("not a JSON interfaces payload")
42+
43+
// ParseInterfacesJSONAsNetmon parses a JSON payload produced by getInterfacesAsJson()
44+
// and returns netmon.Interfaces plus parsing stats.
45+
func ParseInterfacesJSONAsNetmon(b []byte) ([]netmon.Interface, ParseStats, error) {
46+
var st ParseStats
47+
trim := strings.TrimSpace(string(b))
48+
if trim == "" {
49+
return nil, st, nil
50+
}
51+
if !(strings.HasPrefix(trim, "[") || strings.HasPrefix(trim, "{")) {
52+
return nil, st, ErrNotJSON
53+
}
54+
55+
var in []ifaceJSON
56+
if err := json.Unmarshal([]byte(trim), &in); err != nil {
57+
return nil, st, err
58+
}
59+
60+
out := make([]netmon.Interface, 0, len(in))
61+
for _, it := range in {
62+
st.IfacesTotal++
63+
64+
if it.Name == "" {
65+
st.IfacesSkipped++
66+
continue
67+
}
68+
69+
nif := netmon.Interface{
70+
Interface: &net.Interface{
71+
Name: it.Name,
72+
Index: it.Index,
73+
MTU: it.MTU,
74+
},
75+
AltAddrs: []net.Addr{},
76+
}
77+
78+
if it.Up {
79+
nif.Flags |= net.FlagUp
80+
}
81+
if it.Broadcast {
82+
nif.Flags |= net.FlagBroadcast
83+
}
84+
if it.Loopback {
85+
nif.Flags |= net.FlagLoopback
86+
}
87+
if it.PointToPt {
88+
nif.Flags |= net.FlagPointToPoint
89+
}
90+
if it.Multicast {
91+
nif.Flags |= net.FlagMulticast
92+
}
93+
94+
st.AddrsTotal += len(it.Addrs)
95+
for _, a := range it.Addrs {
96+
appendAltAddr(&nif, &st, a.IP, a.PrefixLen)
97+
}
98+
99+
out = append(out, nif)
100+
st.IfacesParsed++
101+
}
102+
103+
return out, st, nil
104+
}
105+
106+
func appendAltAddr(nif *netmon.Interface, st *ParseStats, ipStr string, prefixLen int) {
107+
na, err := netip.ParseAddr(ipStr) // supports IPv6 zones (e.g. "%wlan0")
108+
if err != nil {
109+
st.AddrsSkipped++
110+
return
111+
}
112+
st.AddrsParsed++
113+
114+
zone := na.Zone()
115+
ip := net.IP(append([]byte(nil), na.AsSlice()...)) // copy to be safe
116+
117+
// If there's a zone (e.g. fe80::...%wlan0), we can't represent it in net.IPNet.
118+
if zone != "" {
119+
nif.AltAddrs = append(nif.AltAddrs, &net.IPAddr{IP: ip, Zone: zone})
120+
return
121+
}
122+
123+
// Prefer net.IPNet for compatibility with code that expects *net.IPNet.
124+
bits := 128
125+
if na.Is4() {
126+
bits = 32
127+
}
128+
if prefixLen < 0 || prefixLen > bits {
129+
// Weird prefix; fall back to IPAddr rather than dropping it.
130+
nif.AltAddrs = append(nif.AltAddrs, &net.IPAddr{IP: ip})
131+
return
132+
}
133+
134+
nif.AltAddrs = append(nif.AltAddrs, &net.IPNet{
135+
IP: ip,
136+
Mask: net.CIDRMask(prefixLen, bits),
137+
})
138+
}

0 commit comments

Comments
 (0)