Cross-compiling Go for Android (Termux) With Working DNS
Go makes cross-compilation easy. Set GOOS and GOARCH, run go build, then you get a binary that Just Works. This means you can use Linux to build your project and get Windows, macOS and Android executables, across different CPU architectures too.
Today I learned that this completely breaks down the moment you try to run a Go binary on Android (specifically Termux).
tl:dr; use CGO with the Android NDK. Otherwise you end up with broken DNS and misleading errors.
This post walks through the whole lot, starting with the original failure, through the debugging steps, to the final fully-working Github Actions CI configuration. If you’re a Go beginner who’s never touched CGO or Android cross-compilation, this should hopefully explain things.
Full working CI config here:
https://github.com/dave1010/jorin/blob/main/.github/workflows/ci.yml
Background
The background to this is that I'm making (yet another) coding agent, called Jorin. Most of the coding is being done on my phone in Termux, by the agent itself. The build chain works fine when completely on my phone: running tests, building and running.
I wanted to set up a Github Action workflow to do the build and also cross compile to other platforms and architectures.
The symptom: DNS fails only on Termux
I got a matrix workflow set up, so Github would make a number of builds when I push a tag and save them as assets in a release.
The build ran fine and the executable even ran on my phone, outputting version and help information. The issue came when I tried to make an HTTP request:
ERR: Post "https://api.openai.com/v1/chat/completions":
dial tcp: lookup api.openai.com on [::1]:53: read udp [::1]:60100->[::1]:53: connection refused
This is a DNS lookup error:
- The Go runtime is trying to resolve
api.openai.com - It’s sending the DNS query to
::1:53(IPv6 localhost) - Nothing is listening on that port → connection refused
The obvious question is:
Why does Go think my DNS server is at
::1on Android?
Especially when my local Termux build worked perfectly, but the CI-built binary did not.
First clue: Go’s DNS resolver
Go has two DNS resolvers:
1. netgo — the pure Go DNS resolver
Used when:
- You compile with
CGO_ENABLED=0, or - You build statically
It reads /etc/resolv.conf and makes raw UDP DNS queries.
2. cgo/libc — the system resolver
Used when:
CGO_ENABLED=1, and- The OS has libc resolver support
This uses the OS’s own DNS logic.
Android’s DNS is not based on /etc/resolv.conf — it uses system APIs. Termux does not have a writable or meaningful /etc/resolv.conf, so netgo has no config and falls back to “best guess”, often ::1.
So the difference between “works locally” and “fails from CI” was simply:
- Local build:
GOOS=android, native Termux →CGO_ENABLED=1→ Android system resolver - CI build:
GOOS=android, butCGO_ENABLED=0→ pure Go resolver →/etc/resolv.confmissing → fallback to::1→ failure
That alone explains the problem. But fixing it requires an actual Android toolchain.
What is CGO?
CGO is Go’s way to call C code from Go. When you enable CGO_ENABLED=1, the Go compiler delegates parts of the build to a C toolchain. That means it uses the target system's C headers, libraries, and linker, rather than Go’s own pure Go substitutes.
For most desktop/server systems this isn’t very noticeable, but sometimes it’s essential. With Android the system resolver, libc implementation (Bionic), and platform headers all live on the C side. Without CGO, Go falls back to its pure-Go implementations for anything relying on system facilities, like DNS, crypto, networking, threading, etc.
Why you need to use CGO for Android
Termux gives you a normal go compiler, but when you cross-compile on Linux you are building a binary for an OS with:
- no glibc
- no standard UNIX headers
- no
/etc/resolv.conf - no
/usr/include
So if you tell Go “compile for GOOS=android, CGO_ENABLED=1”, it needs:
- a C compiler that targets Android
- a sysroot with Android headers
- libc stubs
- Bionic’s include files
This means:
To build a real Android binary, you need the Android NDK.
This is true regardless of language.
Debugging Go’s DNS behaviour
Along the way, ChatGPT pointed out a handy Go feature: the GODEBUG=netdns= flag.
On the failing binary:
GODEBUG=netdns=go+1 ./jorin
Output:
go package net: built with netgo build tag; using Go's DNS resolver
lookup api.openai.com on [::1]:53
This confirmed:
- It's using netgo (pure Go resolver)
- It is querying
::1→ bad fallback
On the working binary:
GODEBUG=netdns=cgo+1 ./jorin
Result:
go package net: using cgo DNS resolver
Exactly what I needed.
The fix: proper Android cross-compilation in CI
Requirements
- Install the Android NDK in Linux (Github Actions)
- Use the NDK's toolchain clang wrapper:
aarch64-linux-android21-clang
- Set
CGO_ENABLED=1for Android builds only - Point
CCat the NDK compiler - Let Go use CGO → libc resolver → working DNS on Android
Why the NDK clang works
The NDK toolchain clang...
- selects the correct sysroot
- includes Android’s headers
- uses Android’s Bionic libc
- sets correct ABI, API level, and linker flags
There may be ways to do this without the NDK but that sounds painful.
The complete working CI snippet
(From the linked repo)
- name: Setup Android NDK
if: matrix.goos == 'android'
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r26d
add-to-path: false
- name: Build
env:
GOOS: $
GOARCH: $
CGO_ENABLED: $
ANDROID_API: $
ANDROID_NDK_HOME: $NaN
run: |
if [ "$GOOS" = "android" ]; then
TOOLCHAIN_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
export CC="$TOOLCHAIN_BIN/aarch64-linux-android${ANDROID_API}-clang"
echo "Using Android NDK CC=$CC"
fi
go build -o "dist/jorin-${GOOS}-${GOARCH}" ./cmd/jorin
This produces actual Android binaries, with working DNS.
Verifying the fix on Termux
Download the artifact:
chmod +x jorin-android-arm64
GODEBUG=netdns=cgo+1 ./jorin-android-arm64
You should see:
go package net: using cgo DNS resolver
What I learned
(And seems obvious in hindsight.)
-
Go cross-compilation “just works”—until you need CGO.
When you need CGO, you need an actual toolchain for the target OS. -
Termux is not Linux.
It’s Android with a Linux-like userland./etc/resolv.confis meaningless. A Debianprootmight have been a better option. -
Go’s pure DNS resolver cannot work on Android.
It depends on POSIX filesystem layout; Android doesn’t provide it. -
The Android NDK is needed for real Android targets.
Nothing else gives you Bionic headers, the correct sysroot, and proper API-level selection. -
Use
GODEBUG=netdns=go+1to debug DNS.
It instantly shows whether you're using netgo or cgo.
Final thoughts
If you’re distributing Go binaries and expect them to run on Android (Termux or otherwise), save yourself the pain:
If you want DNS, HTTPS, or anything network-y to work on Android, build with CGO and the NDK.
Hopefully the next person who hits [::1]:53 will find this in time.