Skip to content

Commit 8825bad

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 8825bad

5 files changed

Lines changed: 440 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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
"slices"
12+
"strings"
13+
14+
"tailscale.com/net/netmon"
15+
)
16+
17+
type ParseStats struct {
18+
IfacesTotal int
19+
IfacesParsed int
20+
IfacesSkipped int
21+
AddrsTotal int
22+
AddrsParsed int
23+
AddrsSkipped int
24+
}
25+
26+
type addrJSON struct {
27+
IP string `json:"ip"`
28+
PrefixLen int `json:"prefixLen"`
29+
}
30+
type ifaceJSON struct {
31+
Name string `json:"name"`
32+
Index int `json:"index"`
33+
MTU int `json:"mtu"`
34+
Up bool `json:"up"`
35+
Broadcast bool `json:"broadcast"`
36+
Loopback bool `json:"loopback"`
37+
PointToPt bool `json:"pointToPoint"`
38+
Multicast bool `json:"multicast"`
39+
Addrs []addrJSON `json:"addrs"`
40+
}
41+
42+
var ErrNotJSON = errors.New("not a JSON interfaces payload")
43+
44+
// ParseInterfacesJSONAsNetmon parses a JSON payload produced by getInterfacesAsJson()
45+
// and returns netmon.Interfaces plus parsing stats.
46+
func ParseInterfacesJSONAsNetmon(b []byte) ([]netmon.Interface, ParseStats, error) {
47+
var st ParseStats
48+
trim := strings.TrimSpace(string(b))
49+
if trim == "" {
50+
return nil, st, nil
51+
}
52+
if !(strings.HasPrefix(trim, "[") || strings.HasPrefix(trim, "{")) {
53+
return nil, st, ErrNotJSON
54+
}
55+
56+
var in []ifaceJSON
57+
if err := json.Unmarshal([]byte(trim), &in); err != nil {
58+
return nil, st, err
59+
}
60+
61+
out := make([]netmon.Interface, 0, len(in))
62+
for _, it := range in {
63+
st.IfacesTotal++
64+
65+
if it.Name == "" {
66+
st.IfacesSkipped++
67+
continue
68+
}
69+
70+
nif := netmon.Interface{
71+
Interface: &net.Interface{
72+
Name: it.Name,
73+
Index: it.Index,
74+
MTU: it.MTU,
75+
},
76+
AltAddrs: []net.Addr{},
77+
}
78+
79+
if it.Up {
80+
nif.Flags |= net.FlagUp
81+
}
82+
if it.Broadcast {
83+
nif.Flags |= net.FlagBroadcast
84+
}
85+
if it.Loopback {
86+
nif.Flags |= net.FlagLoopback
87+
}
88+
if it.PointToPt {
89+
nif.Flags |= net.FlagPointToPoint
90+
}
91+
if it.Multicast {
92+
nif.Flags |= net.FlagMulticast
93+
}
94+
95+
st.AddrsTotal += len(it.Addrs)
96+
for _, a := range it.Addrs {
97+
na, err := a.NetAddr()
98+
if err != nil {
99+
st.AddrsSkipped++
100+
continue
101+
}
102+
nif.AltAddrs = append(nif.AltAddrs, na)
103+
st.AddrsParsed++
104+
}
105+
106+
out = append(out, nif)
107+
st.IfacesParsed++
108+
}
109+
110+
return out, st, nil
111+
}
112+
113+
func (a addrJSON) NetAddr() (net.Addr, error) {
114+
na, err := netip.ParseAddr(a.IP)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
zone := na.Zone()
120+
ip := net.IP(slices.Clone(na.AsSlice()))
121+
122+
// Zoned addresses can't be represented as *net.IPNet.
123+
if zone != "" {
124+
return &net.IPAddr{IP: ip, Zone: zone}, nil
125+
}
126+
127+
bits := 128
128+
if na.Is4() {
129+
bits = 32
130+
}
131+
if a.PrefixLen < 0 || a.PrefixLen > bits {
132+
// Keep the IP but drop the prefix if it's invalid.
133+
return &net.IPAddr{IP: ip}, nil
134+
}
135+
136+
// Host IP + prefixLen mask (not prefix base).
137+
return &net.IPNet{
138+
IP: ip,
139+
Mask: net.CIDRMask(a.PrefixLen, bits),
140+
}, nil
141+
}

0 commit comments

Comments
 (0)