Skip to content

android: fix local endpoint parsing and add regression ifaceparse_test#728

Merged
kari-ts merged 1 commit into
mainfrom
kari/netadd
Jan 7, 2026
Merged

android: fix local endpoint parsing and add regression ifaceparse_test#728
kari-ts merged 1 commit into
mainfrom
kari/netadd

Conversation

@kari-ts

@kari-ts kari-ts commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator

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

@kari-ts
kari-ts requested review from raggi and sfllaw January 6, 2026 23:06
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
AddrsSkipped int
}

// ParseInterfacesAsNetmon parses the string returned by Android's GetInterfacesAsString()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if that string interface is stable?

I wonder if we should be using GetInterfaces() and building the structure that we want to pass to go?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's internally stable, but brittle - we're relying on the number of fields and the order staying the same, etc. We're also relying on InterfaceAddress.toString(), which might not work across OEMs,
We could pass a JSON across the Android/go boundary. I'll work on this approach and rerequest review when it's ready.

@sfllaw sfllaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kari-ts Thanks for trying to fix this properly. I have a few concerns because I’m unfamiliar with the code, but I also don’t want to block tailscale/tailscale#16836 which seems like it is pretty annoying. I am OK if you agree with my concerns but want to address them in a separate PR.

Comment thread libtailscale/ifaceparse/ifaceparse_test.go Outdated
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated

@sfllaw sfllaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kari-ts The refactor looks very promising. Sorry to be a bit picky about this, but it looks like this is a brittle piece of code and it would be good to fix it properly. Is there any need to keep the old string parsing code if we can reliably marshal as JSON?

Comment thread libtailscale/net.go Outdated
Comment thread android/src/main/java/com/tailscale/ipn/App.kt Outdated
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated

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

@sfllaw sfllaw Jan 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Nit (non-blocking): It seems weird to be counting lines when really we are processing interfaces. I think you’re encoding JSON in a compact form, so there aren’t any line breaks anyway?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed "lines" to "interfaces" since lines no longer maps to anything meaningful

@sfllaw sfllaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kari-ts Approved, because this is basically done. I only have a couple style concerns, but they are not super important. You can merge whether you want to change them or not.

Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
return
}

// Prefer net.IPNet for compatibility with code that expects *net.IPNet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Note: this restores the old behaviour, from before #706, for code like: https://github.com/tailscale/tailscale/blob/6c67deff3805e7c90894c9aced1b594854747b87/net/netmon/state.go#L196-L207

for _, a := range addrs {
	switch v := a.(type) {
	case *net.IPNet:
		if pfx, ok := netaddr.FromStdIPNet(v); ok {
			fn(iface, pfx)
		}
	case *net.IPAddr:
		if ip, ok := netip.AddrFromSlice(v.IP); ok {
			fn(iface, netip.PrefixFrom(ip, ip.BitLen()))
		}
	}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok; the pre-#706 implementation appended the *net.IPNet directly, and here we're setting IP to the host IP, so we don't reintroduce that previous behavior where we were just using the prefix base

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Sorry for the confusion. I was just leaving a note here for our future selves to explain why we moved away from IPNet in #706 and then restored it in this PR.

Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
st.AddrsParsed++

zone := na.Zone()
ip := net.IP(append([]byte(nil), na.AsSlice()...)) // copy to be safe

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✍️ Style (non-blocking): I admit that I haven’t ever encountered this danger before, can you help me understand why the obvious implementation isn’t safe? And if we really want to clone the slice, have you seen slices.Clone?

Suggested change
ip := net.IP(append([]byte(nil), na.AsSlice()...)) // copy to be safe
ip := net.IP(na.AsSlice())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, my fear was aliasing, but I don't know how big of a risk that is here. Updated to use slices.Clone - thanks!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m 99% sure that netip.Addr.AsSlice will never be changed to reuse memory. That would probably break so many pieces of software. But there’s no harm with cloning and it’s not like this is a hot loop or anything.

Comment thread libtailscale/ifaceparse/ifaceparse.go Outdated
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>
@kari-ts
kari-ts merged commit 94f2829 into main Jan 7, 2026
4 checks passed
@kari-ts
kari-ts deleted the kari/netadd branch January 7, 2026 22:32
kari-ts added a commit that referenced this pull request Jan 12, 2026
#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
kari-ts added a commit that referenced this pull request Jan 12, 2026
#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728)

Android reports interface addresses in CIDR form. Recent changes
(see tailscale@9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105,  tailscale@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>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728)

Android reports interface addresses in CIDR form. Recent changes
(see tailscale@9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105,  tailscale@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>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728)

Android reports interface addresses in CIDR form. Recent changes
(see tailscale@9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105,  tailscale@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>
serein-213 pushed a commit to serein-213/tailscale-android that referenced this pull request Feb 9, 2026
tailscale#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
nadeemakhter0602 pushed a commit to nadeemakhter0602/TailMon that referenced this pull request Mar 22, 2026
tailscale#728)

Android reports interface addresses in CIDR form. Recent changes
(see tailscale@9c933a0#diff-1d627686c31972e04ef60d7d301e8a2a93714c60096a50055a6bbe9aa041ca8fL105,  tailscale@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>
nadeemakhter0602 pushed a commit to nadeemakhter0602/TailMon that referenced this pull request Mar 22, 2026
tailscale#728 replaced getInterfacesAsString with getInterfacesAsJson. This cleans up that unused method.

Updates tailscale/tailscale#16836

Signed-off-by: kari-ts <kari@tailscale.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android reports wrong local endpoint addresses, degrading local connectivity between tailscale nodes

3 participants