Skip to content

Commit 1d3d31a

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 1d3d31a

5 files changed

Lines changed: 479 additions & 74 deletions

File tree

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

Lines changed: 51 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,53 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
333336
return sb.toString()
334337
}
335338

339+
@Serializable
340+
data class InterfaceJson(
341+
val name: String,
342+
val index: Int,
343+
val mtu: Int,
344+
val up: Boolean,
345+
val loopback: Boolean,
346+
val pointToPoint: Boolean,
347+
val multicast: Boolean,
348+
val addrs: List<String>,
349+
)
350+
351+
override fun getInterfacesAsJson(): String {
352+
val interfaces = Collections.list(NetworkInterface.getNetworkInterfaces())
353+
val out = ArrayList<InterfaceJson>(interfaces.size)
354+
355+
for (nif in interfaces) {
356+
try {
357+
val addrs = ArrayList<String>()
358+
for (ia in nif.interfaceAddresses) {
359+
val addr = ia.address ?: continue
360+
// hostAddress is stable and avoids InterfaceAddress.toString() formatting risks.
361+
val host = addr.hostAddress ?: continue
362+
addrs.add(String.format(Locale.ROOT, "%s/%d", host, ia.networkPrefixLength.toInt()))
363+
}
364+
365+
out.add(
366+
InterfaceJson(
367+
name = nif.name,
368+
index = nif.index,
369+
mtu = nif.mtu,
370+
up = nif.isUp,
371+
loopback = nif.isLoopback,
372+
pointToPoint = nif.isPointToPoint,
373+
multicast = nif.supportsMulticast(),
374+
addrs = addrs,
375+
)
376+
)
377+
} catch (_: Exception) {
378+
continue
379+
}
380+
}
381+
382+
// Avoid pretty printing to keep payload small.
383+
return Json { encodeDefaults = true }.encodeToString(out)
384+
}
385+
336386
@Throws(
337387
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
338388
override fun getSyspolicyBooleanValue(key: String): Boolean {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
"fmt"
10+
"net"
11+
"net/netip"
12+
"strings"
13+
14+
"tailscale.com/net/netmon"
15+
)
16+
17+
type ParseStats struct {
18+
LinesTotal int
19+
LinesParsed int
20+
LinesSkipped int
21+
AddrsTotal int
22+
AddrsParsed int
23+
AddrsSkipped int
24+
}
25+
26+
type ifaceJSON struct {
27+
Name string `json:"name"`
28+
Index int `json:"index"`
29+
MTU int `json:"mtu"`
30+
Up bool `json:"up"`
31+
Loopback bool `json:"loopback"`
32+
PointToPt bool `json:"pointToPoint"`
33+
Multicast bool `json:"multicast"`
34+
Addrs []string `json:"addrs"`
35+
}
36+
37+
var ErrNotJSON = errors.New("not a JSON interfaces payload")
38+
39+
// ParseInterfacesJSONAsNetmon parses a JSON payload produced by getInterfacesAsJson()
40+
// and returns netmon.Interfaces plus parsing stats.
41+
func ParseInterfacesJSONAsNetmon(b []byte) ([]netmon.Interface, ParseStats, error) {
42+
var st ParseStats
43+
trim := strings.TrimSpace(string(b))
44+
if trim == "" {
45+
return nil, st, nil
46+
}
47+
if !(strings.HasPrefix(trim, "[") || strings.HasPrefix(trim, "{")) {
48+
return nil, st, ErrNotJSON
49+
}
50+
51+
var in []ifaceJSON
52+
if err := json.Unmarshal([]byte(trim), &in); err != nil {
53+
return nil, st, err
54+
}
55+
56+
out := make([]netmon.Interface, 0, len(in))
57+
for _, it := range in {
58+
st.LinesTotal++
59+
60+
if it.Name == "" || it.Index == 0 || it.MTU == 0 {
61+
st.LinesSkipped++
62+
continue
63+
}
64+
65+
nif := netmon.Interface{
66+
Interface: &net.Interface{
67+
Name: it.Name,
68+
Index: it.Index,
69+
MTU: it.MTU,
70+
},
71+
AltAddrs: []net.Addr{},
72+
}
73+
74+
if it.Up {
75+
nif.Flags |= net.FlagUp
76+
}
77+
if it.Loopback {
78+
nif.Flags |= net.FlagLoopback
79+
}
80+
if it.PointToPt {
81+
nif.Flags |= net.FlagPointToPoint
82+
}
83+
if it.Multicast {
84+
nif.Flags |= net.FlagMulticast
85+
}
86+
87+
st.AddrsTotal += len(it.Addrs)
88+
for _, addr := range it.Addrs {
89+
ipStr, _, _ := strings.Cut(addr, "/")
90+
na, err := netip.ParseAddr(ipStr)
91+
if err != nil {
92+
st.AddrsSkipped++
93+
continue
94+
}
95+
st.AddrsParsed++
96+
nif.AltAddrs = append(nif.AltAddrs, &net.IPAddr{
97+
IP: net.IP(na.AsSlice()),
98+
Zone: na.Zone(),
99+
})
100+
}
101+
102+
out = append(out, nif)
103+
st.LinesParsed++
104+
}
105+
106+
return out, st, nil
107+
}
108+
109+
// ParseInterfacesAsNetmon parses the string returned by Android's GetInterfacesAsString()
110+
// into netmon.Interface values.
111+
func ParseInterfacesAsNetmon(ifaceString string) ([]netmon.Interface, ParseStats, error) {
112+
var ifaces []netmon.Interface
113+
var st ParseStats
114+
115+
for _, line := range strings.Split(ifaceString, "\n") {
116+
// Example of the strings we're processing:
117+
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
118+
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
119+
// mnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
120+
if strings.TrimSpace(line) == "" {
121+
continue
122+
}
123+
st.LinesTotal++
124+
125+
fields := strings.Split(line, "|")
126+
if len(fields) != 2 {
127+
st.LinesSkipped++
128+
continue
129+
}
130+
131+
var name string
132+
var index, mtu int
133+
var up, broadcast, loopback, pointToPoint, multicast bool
134+
if _, err := fmt.Sscanf(fields[0], "%s %d %d %t %t %t %t %t",
135+
&name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast); err != nil {
136+
st.LinesSkipped++
137+
continue
138+
}
139+
140+
newIf := netmon.Interface{
141+
Interface: &net.Interface{
142+
Name: name,
143+
Index: index,
144+
MTU: mtu,
145+
},
146+
AltAddrs: []net.Addr{}, // non-nil to avoid Go using netlink
147+
}
148+
if up {
149+
newIf.Flags |= net.FlagUp
150+
}
151+
if broadcast {
152+
newIf.Flags |= net.FlagBroadcast
153+
}
154+
if loopback {
155+
newIf.Flags |= net.FlagLoopback
156+
}
157+
if pointToPoint {
158+
newIf.Flags |= net.FlagPointToPoint
159+
}
160+
if multicast {
161+
newIf.Flags |= net.FlagMulticast
162+
}
163+
164+
addrField := strings.TrimSpace(fields[1])
165+
addrTokens := strings.Fields(addrField)
166+
st.AddrsTotal += len(addrTokens)
167+
168+
for _, addr := range addrTokens {
169+
// addr is typically in CIDR form: "10.0.0.159/8" or "2601:.../64".
170+
// For endpoints we want the host IP, not the CIDR prefix base, so strip "/NN".
171+
ipStr, _, _ := strings.Cut(addr, "/")
172+
173+
na, err := netip.ParseAddr(ipStr) // supports IPv6 zones (e.g. "%wlan0")
174+
if err != nil {
175+
st.AddrsSkipped++
176+
continue
177+
}
178+
st.AddrsParsed++
179+
180+
newIf.AltAddrs = append(newIf.AltAddrs, &net.IPAddr{
181+
IP: net.IP(na.AsSlice()),
182+
Zone: na.Zone(),
183+
})
184+
}
185+
186+
ifaces = append(ifaces, newIf)
187+
st.LinesParsed++
188+
}
189+
190+
return ifaces, st, nil
191+
}

0 commit comments

Comments
 (0)