Codex CLI Termux Fix - IPv6 Timeout Issue

technology ai llm codex claude android termux issue ipv6
Originally published on othernotherone.com

Problem Summary

codex login --device-auth fails in Termux with:

Error logging in with device code: error sending request for url
(https://auth.openai.com/api/accounts/deviceauth/usercode)

The command hangs for a long time before failing. However, the same command works instantly in proot Ubuntu on the same device.

>>WARNING: the following was generated in large part by Claude Code, which was reviewed and tested by me as functioning and mostly logical. It has also been reviewed by Codex, with notes called out below where there were deviations in this same highlight style. Your mileage and secops may vary from mine…there’s no implied warranty or guarantee that you don’t completely bronk your device. Backups and version control and your brain are required. Be warned.<<

Root Cause

The Codex CLI Architecture

The codex command installed via npm is actually a thin Node.js wrapper that spawns a native Rust binary:

/data/data/com.termux/files/usr/lib/node_modules/@openai/codex/vendor/aarch64-unknown-linux-musl/codex/codex

This binary is:

  • Statically linked against musl libc (not glibc)
  • Uses its own DNS resolver and TLS stack (likely rustls)
  • Does NOT use Node.js for network requests

The IPv6 Timeout Issue

  1. The musl DNS resolver queries for both IPv4 (A) and IPv6 (AAAA) records
  2. OpenAI’s servers return both IPv4 and IPv6 addresses
  3. The Rust HTTP client tries IPv6 first
  4. IPv6 is unreachable on the network (confirmed via ping6)
  5. The connection attempt hangs until timeout (~30+ seconds)
  6. Only then does it fall back to IPv4
  7. By this point, something in the auth flow has already failed

>>FROM CODEX AUDIT warning: This flow is plausible but not guaranteed without evidence; note that client fallback behavior can vary, so consider softening absolute claims.<<

Why Proot Ubuntu Works

In proot Ubuntu:

  • The system uses glibc which handles IPv6 fallback differently
  • The /etc/hosts file is writable and can be configured
  • The network stack behavior differs from native Termux

Why Native Termux Fails

In native Termux:

  • /etc/hosts is read-only (Android system partition)
  • The musl binary looks at /etc/hosts, not $PREFIX/etc/hosts
  • Cannot modify system DNS/network behavior without root
  • IPv6 timeout causes the auth request to fail

>>FROM CODEX AUDIT concern: The /etc/hosts lookup claim may vary by libc/build; consider verifying or rephrasing as observed behavior.<<

The Fix

Run a temporary Node.js proxy that forces IPv4 connections. The Rust binary respects HTTPS_PROXY, and Node.js in Termux already has --dns-result-order=ipv4first configured.

>>FROM CODEX AUDIT caution: The fix assumes both `HTTPS_PROXY` support in the Rust binary and `–dns-result-order=ipv4first` in Termux Node; recommend noting how to verify these assumptions.<<

Files Created

>>*FROM CODEX AUDIT warning: Local unauthenticated CONNECT proxy; any local process can use it while running. Loopback-only helps, but call out the risk.<<*

~/codex-fix/ipv4-proxy.mjs

Minimal HTTP CONNECT proxy that forces IPv4:

// Minimal HTTP CONNECT proxy that forces IPv4
// Node.js already has --dns-result-order=ipv4first in NODE_OPTIONS
import { createServer, request } from 'http';
import { connect } from 'net';

const server = createServer();

server.on('connect', (req, clientSocket, head) => {
  const [host, port] = req.url.split(':');

  // Connect using IPv4 (Node respects dns-result-order=ipv4first)
  const serverSocket = connect({ host, port: +port, family: 4 }, () => {
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    serverSocket.write(head);
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });

  serverSocket.on('error', (e) => {
    clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
    clientSocket.end();
  });
});

server.listen(18321, '127.0.0.1', () => {
  console.log('ready');
});

~/codex-fix/codex-wrapper

Wrapper script that starts proxy, runs codex, then cleans up:

# Wrapper that runs a temporary IPv4-only proxy for codex
# No proot dependency - uses Node.js which already has ipv4first configured

PROXY_SCRIPT="$HOME/codex-fix/ipv4-proxy.mjs"
CODEX_BIN="/data/data/com.termux/files/usr/lib/node_modules/@openai/codex/vendor/aarch64-unknown-linux-musl/codex/codex"

# Start proxy in background
node "$PROXY_SCRIPT" &
PROXY_PID=$!

# Wait for proxy to be ready (max 2 seconds)
for i in {1..20}; do
    if nc -z 127.0.0.1 18321 2>/dev/null; then
        break
    fi
    sleep 0.1
done

# Run codex with proxy
HTTPS_PROXY=http://127.0.0.1:18321 HTTP_PROXY=http://127.0.0.1:18321 "$CODEX_BIN" "$@"
EXIT_CODE=$?

# Cleanup
kill $PROXY_PID 2>/dev/null
wait $PROXY_PID 2>/dev/null

exit $EXIT_CODE

Shell Alias

Added to ~/.zshrc:

>>FROM CODEX AUDIT note: This only targets zsh; many Termux setups use bash, so mention ~/.bashrc or ~/.profile as alternatives.<<

alias codex="~/codex-fix/codex-wrapper"

Installation / Recreation

If you need to recreate the fix from scratch:

# Create directory
mkdir -p ~/codex-fix

# Create the IPv4 proxy script
cat > ~/codex-fix/ipv4-proxy.mjs << 'EOF'
import { createServer, request } from 'http';
import { connect } from 'net';

const server = createServer();

server.on('connect', (req, clientSocket, head) => {
  const [host, port] = req.url.split(':');
  const serverSocket = connect({ host, port: +port, family: 4 }, () => {
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    serverSocket.write(head);
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });
  serverSocket.on('error', (e) => {
    clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
    clientSocket.end();
  });
});

server.listen(18321, '127.0.0.1', () => {
  console.log('ready');
});
EOF

# Create wrapper script
cat > ~/codex-fix/codex-wrapper << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
PROXY_SCRIPT="$HOME/codex-fix/ipv4-proxy.mjs"
CODEX_BIN="/data/data/com.termux/files/usr/lib/node_modules/@openai/codex/vendor/aarch64-unknown-linux-musl/codex/codex"

node "$PROXY_SCRIPT" &
PROXY_PID=$!

for i in {1..20}; do
    if nc -z 127.0.0.1 18321 2>/dev/null; then
        break
    fi
    sleep 0.1
done

HTTPS_PROXY=http://127.0.0.1:18321 HTTP_PROXY=http://127.0.0.1:18321 "$CODEX_BIN" "$@"
EXIT_CODE=$?

kill $PROXY_PID 2>/dev/null
wait $PROXY_PID 2>/dev/null

exit $EXIT_CODE
EOF

# Make executable
chmod +x ~/codex-fix/codex-wrapper

# Add alias (skip if already in .zshrc)
echo 'alias codex="~/codex-fix/codex-wrapper"' >> ~/.zshrc

# Reload shell
source ~/.zshrc

>>FROM CODEX AUDIT caution: Appending alias blindly can add duplicates; suggest a guard or manual edit.<<

Dependencies

  • nodejs-lts (already installed for codex)
  • netcat-openbsd for the nc command (install with pkg install netcat-openbsd if missing)

Usage

After installation:

# Login (uses wrapper automatically via alias)
codex login --device-auth

# Or explicitly use wrapper
~/codex-fix/codex-wrapper login --device-auth

Troubleshooting

Verify IPv6 is the Issue

# This should fail/timeout
ping6 -c 1 -W 2 2606:4700:4406::6812:29f1

# This should work
curl -4 -v https://auth.openai.com 2>&1 | head -20

>>*FROM CODEX AUDIT note: These checks validate IPv6 reachability but do not confirm the Rust client’s proxy support or fallback behavior.<<*

Test Without Wrapper

# This will hang then fail (proves the issue)
/data/data/com.termux/files/usr/lib/node_modules/@openai/codex/vendor/aarch64-unknown-linux-musl/codex/codex login --device-auth

Check if nc is Installed

which nc || pkg install netcat-openbsd

Technical Details

Codex Binary Info

Path: /data/data/com.termux/files/usr/lib/node_modules/@openai/codex/vendor/aarch64-unknown-linux-musl/codex/codex
Type: ELF 64-bit LSB executable, ARM aarch64, statically linked
Libc: musl (not glibc)

How the Fix Works

  1. Wrapper starts a Node.js HTTP CONNECT proxy on port 18321
  2. Node.js uses family: 4 to force IPv4 connections
  3. Wrapper sets HTTPS_PROXY env var pointing to the local proxy
  4. Codex binary respects HTTPS_PROXY and routes traffic through it
  5. Proxy connects to OpenAI using IPv4 only - no IPv6 timeout
  6. Wrapper cleans up proxy process when codex exits

>>*FROM CODEX AUDIT concern: Wrapper does not fail fast if proxy fails to start (e.g., port in use); consider adding readiness checks and error handling.<<*

Why This Affects Musl Specifically

  • Musl’s DNS resolver is minimal and reads /etc/resolv.conf and /etc/hosts directly
  • It doesn’t use Android’s DNS resolution system
  • It follows RFC 6724 for address selection, preferring IPv6
  • No built-in mechanism to prefer IPv4 or handle IPv6 timeout gracefully

Alternative Fix (requires proot)

If the proxy approach doesn’t work, you can use proot to bind a custom /etc/hosts:

>>FROM CODEX AUDIT warning: Hard-coded CDN IPs can change and break auth; pinning also risks misrouting. Prefer DNS-based solutions if possible.<<

mkdir -p ~/codex-fix/etc
echo "127.0.0.1 localhost
104.18.41.241 auth.openai.com
172.64.146.15 api.openai.com" > ~/codex-fix/etc/hosts

# Run with proot
proot -b ~/codex-fix/etc/hosts:/etc/hosts codex login --device-auth

Environment Info

  • Device: Pixel Fold
  • Platform: Termux on Android
  • Node.js: 24.13.0 (nodejs-lts)
  • Codex CLI: 0.80.0
  • Date discovered: 2026-01-14

References