Skip to content

Build with a Go toolchain patched by Termux, drop the netmon and DNS patches - #13

Merged
bropines merged 2 commits into
mainfrom
feat/termux-go-toolchain
Sep 8, 2026
Merged

Build with a Go toolchain patched by Termux, drop the netmon and DNS patches#13
bropines merged 2 commits into
mainfrom
feat/termux-go-toolchain

Conversation

@bropines

@bropines bropines commented Sep 8, 2026

Copy link
Copy Markdown
Owner

The two hardest things this project patches — interface discovery and DNS — are not tailscale problems. They are Go standard-library problems on Android, and Termux already fixes both in the Go it ships, and applies the same patches when cross-compiling other packages. This does the same, and deletes our versions.

Evidence

Same probe program, built two ways, run on an Android 16 phone in a real untrusted_app context (not via su, which runs in a privileged SELinux domain and hides the failure):

vanilla Go:   net.Interfaces(): err=route ip+net: netlinkrib: permission denied  count=0
              LookupHost:       err=... on [::1]:53 ... connection refused

patched Go:   net.Interfaces(): err=<nil>  count=8
                lo, wlan0 (v4 + 3× v6), rmnet_data4 (cellular), rmnet_data3 (global IPv6), tun0, ...
              LookupHost:       err=<nil>  ips=[...]

The patched build sees more than our own patch did: cellular rmnet interfaces and a global IPv6 address, which the anet/proc/net/if_inet6ifconfig → UDPv6-probe ladder never reliably got.

Then verified with the real daemon on the same phone, on its existing tailnet state: it registered with the control plane (machineAuthorized=true), and curl --socks5-hostname through its proxy returned 200 — the DNS path that actually matters. tailscale-test came back green after restoring the service.

What changed

patches/fix_android_netmon.go (470 lines) is gone, along with the net/tsdial resolver injection and the wlynxg/anet dependency. What survived is the hostinfo hook — a deliberate choice, not a workaround — now in patches/fix_hostinfo_android.go.

In its place, patches/go/ vendors Termux's four Go patches, applied to the toolchain before building.

Consequences worth reading

  • All four architectures are now GOOS=android with -buildmode=pie. Previously one was PIE and three were ET_EXEC. That also covers Termux builds targeting API 29+, which launch binaries through /system/bin/linker and reject ET_EXEC.
  • Cross-compiling arm/i686/x86_64 now needs the NDK, because Go refuses GOOS=android without cgo there. CI runners have one; the workflow now fails loudly if that changes. aarch64 needs nothing.
  • Building on a phone needs nothing extra. Termux's own Go already carries these patches, so build.sh detects an on-device build and skips the toolchain download entirely — a linux-amd64 toolchain could not run there anyway.
  • GOTOOLCHAIN=local is load-bearing. Without it Go silently downloads the toolchain named in tailscale's go.mod and discards every patch above it.
  • DNS is now $PREFIX/etc/resolv.conf — the same resolver as the rest of your Termux, editable by hand. TS_DNS_SERVER is gone and nothing is hardcoded to a public resolver any more. resolv-conf became a package dependency, since without that file there is no resolver at all.
  • verify_patched now greps the artifact for the toolchain's resolv.conf path. A toolchain patch that quietly stops applying otherwise produces a binary that looks fine and cannot resolve a name or see an interface.
  • Patch application is fatal on failure. These diffs patch Go's own sources and are sensitive to its version; they applied cleanly to go1.27.1, and patches/go/refresh.sh re-vendors them.

Not done here

Adopting termux-packages' build-package.sh wholesale. This takes the part that carries the benefit — the toolchain treatment — without depending on their Docker image or their repository layout. The recipe in termux-packages/ for upstream submission is unaffected.

🤖 Generated with Claude Code

bropines and others added 2 commits September 8, 2026 22:48
…tches

The interface-discovery and DNS problems this project works around are not
tailscale problems, they are Go standard-library problems on Android -- and
Termux already fixes both in the Go it ships, and applies the same patches when
cross-compiling other packages. Doing the same here replaces ~470 lines of our
own code with four vendored diffs.

Measured on an Android 16 phone in a real untrusted_app context, running the
same probe built two ways:

  vanilla Go:  net.Interfaces(): err=netlinkrib: permission denied  count=0
               LookupHost: ... on [::1]:53 ... connection refused
  patched Go:  net.Interfaces(): err=<nil>  count=8
               LookupHost: err=<nil>  ips=[...]

The patched build sees more than our patch did: cellular rmnet interfaces and
a global IPv6 address, which the ifconfig/procfs ladder never reliably got.
Then verified with the real daemon on the same phone -- registered with the
control plane, and `curl --socks5-hostname` through it returned 200, which is
the DNS path that matters.

So patches/fix_android_netmon.go is gone, along with the tsdial resolver
injection and the wlynxg/anet dependency. What is left of that file is the
hostinfo hook, which is a deliberate choice rather than a workaround, moved to
patches/fix_hostinfo_android.go.

Consequences worth stating:

- Every architecture is now GOOS=android with -buildmode=pie, so all four ship
  as ET_DYN against /system/bin/linker instead of one PIE and three ET_EXEC.
  That also covers Termux builds targeting API 29+, which launch binaries
  through the linker and reject ET_EXEC.
- Cross-compiling arm/i686/x86_64 now needs the NDK, because Go refuses
  GOOS=android without cgo there. Building on a phone needs nothing extra:
  Termux's own Go already carries the patches, so build.sh detects that and
  skips the toolchain download entirely.
- GOTOOLCHAIN=local is not optional. Without it Go silently downloads the
  toolchain named in tailscale's go.mod and discards every patch above.
- DNS is now whatever $PREFIX/etc/resolv.conf says -- the same resolver as the
  rest of Termux, editable by the user -- so TS_DNS_SERVER is gone and nothing
  is hardcoded to a public resolver. resolv-conf became a package dependency,
  since without that file there is no resolver at all.
- verify_patched checks the artifact for the toolchain's resolv.conf path: a
  toolchain patch that stops applying otherwise yields a binary that looks fine
  and cannot resolve a name or see an interface.
- Patch application is fatal on failure. These diffs patch Go's own sources and
  are sensitive to its version; they applied cleanly to go1.27.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TS_DNS_SERVER stopped existing in the previous commit. Anyone who had it in
.env would otherwise find it silently ignored, with no hint that the address
now belongs in $PREFIX/etc/resolv.conf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bropines
bropines merged commit f390234 into main Sep 8, 2026
6 checks passed
@bropines
bropines deleted the feat/termux-go-toolchain branch September 8, 2026 20:22
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.

1 participant