Skip to content

Instantly share code, notes, and snippets.

@ashokvarmamatta
Last active April 2, 2026 12:01
Show Gist options
  • Select an option

  • Save ashokvarmamatta/1bba0d91a839039428bd942b8fdcc968 to your computer and use it in GitHub Desktop.

Select an option

Save ashokvarmamatta/1bba0d91a839039428bd942b8fdcc968 to your computer and use it in GitHub Desktop.
Running Cloudflare Tunnels on Android - Complete Guide (every problem + solution)

☁️ Cloudflare Tunnels on Android

The guide nobody wrote. Until now.

Typing SVG

Android Cloudflare Kotlin Go Binary


πŸ’‘ What This Is

You have an Android app running a local server. You want a public HTTPS URL so the world can reach it.

πŸ“± Your Android App (localhost:8088)
              ↓
       ☁️ Cloudflare Tunnel
              ↓
🌍 https://cool-name.trycloudflare.com  ← anyone can access this

πŸ€” Can Android run cloudflared? β†’ Yes. Android IS Linux.

πŸ€” Does it need root? β†’ No. But you'll fight 5 different Android restrictions.

πŸ€” Is it free? β†’ Yes. Quick Tunnels need no account, no credit card, nothing.

πŸ€” Why is this guide so long? β†’ Because every existing "just run cloudflared" tutorial silently fails on Android. We document WHY and HOW to fix each failure.

Built for ZeroClaw Android. Works for any Android app.


🚨 The 5 Problems (and Solutions)

Problem 1: Getting the Binary

What Details
❌ Problem Cloudflare doesn't publish Android builds
βœ… Solution Android IS Linux β€” use the linux-arm64 build
πŸ“₯ Download cloudflared-linux-arm64 from GitHub releases
curl -L -o libcloudflared.so \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64

⚠️ We name it libcloudflared.so β€” this matters for Problem 2.


Problem 2: Permission Denied (W^X Policy)

What Details
❌ Problem Android 10+ blocks executing files from app directories
πŸ’€ Error error=13, Permission denied
🀯 Also fails Dynamic linker: unexpected e_type: 2
βœ… Solution Bundle as native library in APK
πŸ’€ What DOESN'T Work
// ❌ FAILS β€” filesDir is noexec
val binary = File(context.filesDir, "cloudflared")
binary.setExecutable(true)
ProcessBuilder(binary.absolutePath).start()  // Permission denied

// ❌ FAILS β€” linker expects shared libs, not executables
ProcessBuilder("/system/bin/linker64", binary.absolutePath).start()
// "unexpected e_type: 2"

βœ… The Fix β€” 4 Steps

Step 1 β€” Place binary in jniLibs:

app/src/main/jniLibs/arm64-v8a/libcloudflared.so

Step 2 β€” Force extraction (build.gradle.kts):

android {
    packaging {
        jniLibs { useLegacyPackaging = true }
    }
}

Step 3 β€” Force extraction (AndroidManifest.xml):

<application android:extractNativeLibs="true" ...>

Step 4 β€” Execute from nativeLibraryDir:

val binary = File(context.applicationInfo.nativeLibraryDir, "libcloudflared.so")
ProcessBuilder(binary.absolutePath, "tunnel", "--url", "http://localhost:8088").start()
// βœ… nativeLibraryDir HAS execute permission!

πŸ’‘ Why both build.gradle AND manifest? Modern Android keeps .so files compressed inside the APK. You need BOTH settings to extract them as real files on disk.


Problem 3: DNS Completely Broken

What Details
❌ Problem Go reads /etc/resolv.conf for DNS β€” doesn't exist on Android
πŸ’€ Error lookup api.trycloudflare.com on [::1]:53: connection refused
🧱 Root cause Path is HARDCODED in Go source. No env var override exists.
πŸ’€ Everything We Tried That FAILED
Attempt Why It Failed
πŸ“ Create /etc/resolv.conf /etc/ is read-only (symlink to /system/etc/)
πŸ”§ RES_CONF env var Not a real Go thing β€” we made it up hoping it existed
πŸ”§ GODEBUG=netdns=cgo cloudflared is statically linked, no cgo available
πŸ”Œ DNS relay on port 53 EACCES β€” non-root can't bind ports below 1024
🌐 HTTPS_PROXY env var cloudflared uses custom http.Transport{} with Proxy: nil β€” ignores proxy
πŸ”€ HTTP CONNECT proxy Same β€” cloudflared bypasses proxy entirely
πŸ“¦ Build with GOOS=android Would fix DNS, but requires compiling cloudflared from source

7 approaches. All failed. Then we found the actual solution:

βœ… The Fix β€” Do DNS from Java, Bypass Go Entirely

Java's InetAddress.getByName() uses Android's native DNS resolver. It works perfectly. The trick: do ALL DNS work from Java and pass results to cloudflared.

Phase 1 β€” Register tunnel from Java (not cloudflared):

// βœ… Java DNS works on Android!
val client = OkHttpClient()
val request = Request.Builder()
    .url("https://api.trycloudflare.com/tunnel")
    .post("".toRequestBody("application/json".toMediaType()))
    .build()

val response = client.newCall(request).execute()
val result = JSONObject(response.body!!.string()).getJSONObject("result")

val tunnelId   = result.getString("id")           // UUID
val hostname   = result.getString("hostname")      // xxx.trycloudflare.com
val accountTag = result.getString("account_tag")
val secret     = result.getString("secret")        // base64

Phase 2 β€” Write credentials file:

File(context.cacheDir, "tunnel_creds.json").writeText(
    JSONObject().apply {
        put("AccountTag", accountTag)
        put("TunnelID", tunnelId)
        put("TunnelSecret", secret)
    }.toString()
)

Phase 3 β€” Write config file:

File(context.cacheDir, "tunnel_config.yml").writeText("""
    tunnel: $tunnelId
    credentials-file: ${credsFile.absolutePath}
    protocol: http2
    ingress:
      - hostname: $hostname
        service: http://localhost:8088
      - service: http_status:404
""".trimIndent())

Phase 4 β€” Resolve edge IPs from Java:

val edgeIps = mutableListOf<String>()
for (host in listOf("region1.v2.argotunnel.com", "region2.v2.argotunnel.com")) {
    InetAddress.getAllByName(host)
        .filter { it is Inet4Address }
        .forEach { edgeIps.add("${it.hostAddress}:7844") }
}

Phase 5 β€” Run cloudflared with zero DNS needed:

val cmd = mutableListOf(binary.absolutePath, "tunnel",
    "--config", configFile.absolutePath,
    "--edge-ip-version", "4",
    "--no-autoupdate")

for (ip in edgeIps.take(4)) cmd.addAll(listOf("--edge", ip))
cmd.addAll(listOf("run", tunnelId))

ProcessBuilder(cmd).directory(context.cacheDir).redirectErrorStream(true).start()

πŸŽ‰ The tunnel URL is known INSTANTLY from Phase 1 β€” no stdout parsing needed!


Problem 4: Edge Discovery Fails Too

What Details
❌ Problem After registration, cloudflared needs SRV records for edge servers
πŸ’€ Error lookup _v2-origintunneld._tcp.argotunnel.com on [::1]:53: refused
βœ… Solution Resolve edge hostnames from Java, pass via --edge flag

Already handled in Phase 4 above. The key insight: cloudflared needs DNS for TWO things β€” the API call AND edge discovery. You must handle both from Java.


Problem 5: --edge Flag Format

What Details
❌ Problem Passing comma-separated IPs to --edge
πŸ’€ Error too many colons in address
βœ… Solution One --edge per address
# ❌ WRONG
--edge 198.41.192.77:7844,198.41.192.107:7844

# βœ… RIGHT
--edge 198.41.192.77:7844 --edge 198.41.192.107:7844

πŸ—οΈ Complete Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              πŸ“± ANDROID APP                      β”‚
β”‚                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚    β˜• Java/Kotlin  (DNS works here!)      β”‚   β”‚
β”‚  β”‚                                          β”‚   β”‚
β”‚  β”‚  1️⃣ POST api.trycloudflare.com/tunnel    β”‚   β”‚
β”‚  β”‚     β†’ tunnel ID, hostname, credentials   β”‚   β”‚
β”‚  β”‚                                          β”‚   β”‚
β”‚  β”‚  2️⃣ InetAddress.getAllByName(             β”‚   β”‚
β”‚  β”‚       "region1.v2.argotunnel.com")       β”‚   β”‚
β”‚  β”‚     β†’ edge server IPs                    β”‚   β”‚
β”‚  β”‚                                          β”‚   β”‚
β”‚  β”‚  3️⃣ Write creds.json + config.yml        β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                   ↓                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  πŸ”΅ cloudflared (libcloudflared.so)       β”‚   β”‚
β”‚  β”‚     from: nativeLibraryDir (exec βœ…)      β”‚   β”‚
β”‚  β”‚                                          β”‚   β”‚
β”‚  β”‚  --config config.yml                     β”‚   β”‚
β”‚  β”‚  --edge 198.41.192.77:7844               β”‚   β”‚
β”‚  β”‚  --edge-ip-version 4                     β”‚   β”‚
β”‚  β”‚  --no-autoupdate                         β”‚   β”‚
β”‚  β”‚  run <tunnel-id>                         β”‚   β”‚
β”‚  β”‚                                          β”‚   β”‚
β”‚  β”‚  β†’ Direct IP connection. ZERO DNS. βœ…     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                   ↓                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚     πŸ–₯️ Your Local Server (:8088)          β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ↓
    🌍 https://xxx.trycloudflare.com
         accessible worldwide!

βœ… Quick Checklist

# Step Done?
1 libcloudflared.so in jniLibs/arm64-v8a/ ☐
2 android:extractNativeLibs="true" in manifest ☐
3 useLegacyPackaging = true in build.gradle ☐
4 API call to trycloudflare.com done from Java ☐
5 Edge IPs resolved from Java ☐
6 Credentials + config written to cacheDir ☐
7 Separate --edge IP:port flags (not comma-separated) ☐
8 --edge-ip-version 4 flag ☐
9 --no-autoupdate flag ☐
10 INTERNET permission in manifest ☐

⚠️ Non-Fatal Errors (Ignore These)

After tunnel connects, you'll see these. They're harmless:

Error Why It's Fine
Failed to fetch features ... cfd-features.argotunnel.com Optional feature flags β€” tunnel works without them
GID not within ping_group_range Android restricts ping β€” tunnel doesn't need it
open /proc/sys/net/ipv4/ping_group_range: permission denied Same β€” irrelevant to tunnel operation

❓ FAQ

Question Answer
πŸ”„ URL changes on restart? Quick Tunnels: yes. Named Tunnels (with token): no
πŸ“¦ APK size impact? +37MB. Use Git LFS for the repo
πŸ–₯️ x86 emulator? No β€” ARM64 binary only. Need real device or ARM64 emulator
πŸ”¨ Why not GOOS=android? Would fix DNS natively, but requires compiling cloudflared from Go source
πŸ”€ ngrok instead? Same jniLibs approach works. ngrok may handle DNS better
πŸ‘€ Need Cloudflare account? Quick Tunnels: no. Named Tunnels: yes

πŸ“Š Bugs We Hit

All documented in BUGS.md

Bug Severity Problem Fix
BUG-25 W^X policy blocks execution Bundle in jniLibs
BUG-26 Go DNS broken on Android Java-side DNS
BUG-27 HTTPS_PROXY ignored Java-side API call
BUG-28 Edge discovery DNS fails Java edge resolution
BUG-29 --edge comma format Separate flags

GitHubΒ  LinkedInΒ  PortfolioΒ  Email


If this saved you time, star the repo! ⭐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment