# Fronting a cloudflared quick tunnel with freedns: why it doesn't work These are operator notes on a failed attempt to put a stable, memorable URL in front of a Cloudflare quick-tunnel, using only zero-cost infrastructure (no Cloudflare account, no paid domain, no credit card on file). I am writing it up because between the Phase-10 and Phase-11 logs here, this path had already absorbed a few hours of time and keeps being suggested whenever "stable URL for an HTTP endpoint on an outbound-only machine" comes up. I wanted a single doc I could link next time. tl;dr. You can sign up on [freedns.afraid.org][freedns] with a zero-cash pipeline (Whisper on the audio CAPTCHA). You can also log in and add records. But the specific record type you'd want for quick-tunnel fronting — a CNAME on one of the public dynamic-DNS domains — is admin-gated for basic accounts. And even if it weren't, the Cloudflare anycast edge serving `*.trycloudflare.com` only has certs for that suffix, so TLS to your own hostname CNAME'd to a quick tunnel fails at handshake. The dead end is at two layers; fixing only one doesn't help. [freedns]: https://freedns.afraid.org/ ## 1. The setting The machine is AWS EC2. Inbound is port 22 only and the security group isn't editable. That leaves only outbound HTTPS tunneling primitives for hosting anything the public internet can reach. We chose `cloudflared tunnel --url http://127.0.0.1:8402` — Cloudflare's "quick tunnel" mode, which assigns a random hostname like `quite-promo-weather-link.trycloudflare.com` and keeps the tunnel up until the process dies or restarts. There is no Cloudflare account associated with this tunnel and no credit card on file. Over about a week the same hostname persisted across seven consecutive test sessions without rotation, but Cloudflare makes no stability guarantee for quick tunnels — the hostname can change on its side for reasons that have nothing to do with whether your `cloudflared` process is still alive. We wanted something like `endpoint.` instead. The obvious zero-cost plumbing was: sign up for freedns, point a record at the quick tunnel, publish the stable URL. That's the plan this document shows didn't work. ## 2. Signing up on freedns in 2026 A visual CAPTCHA and an email-activation gate, both solvable. Neither is the real blocker. `/signup/` asks for the usual name / email / password plus a [securimage][securimage] CAPTCHA — a session-bound 6-character code rendered both as a visual PNG and an audio WAV. The visual is the default widget; the audio is one button click away. [securimage]: https://www.phpcaptcha.org/ If you try Gemini 3 Pro or any Claude/OpenAI-frontier model on the visual, nothing reliably transcribes it. The characters are distorted deliberately; the visual reads fine for humans, and Gemini can read most of what the eye can, but not with the stability you need for a retry loop. (Claude and OpenAI frontier models also refuse image CAPTCHAs under their safety policy, so they aren't available for this in any form.) The audio is much easier. It's a digitized spelling-out of the same code, six characters, three or four seconds long, fairly clean once you denoise it. Locally installed Whisper (`openai-whisper==20250625`, `medium.en` model) transcribes 4-5 of the 6 characters reliably after a light `ffmpeg` denoise pass: ``` ffmpeg -y -i in.wav \ -af "highpass=f=150,lowpass=f=3000,afftdn=nr=20" \ out.wav ``` We transcribe with `language="en", beam_size=5, temperature=0.0, fp16=False`. Also worth considering for random-character captchas: pass `condition_on_previous_text=False` to stop Whisper from carrying assumptions across segments — not critical here because the clips are short, but relevant on longer codes. The characters Whisper tends to miss are the first or the last one. Misses tend to cluster (a failed run often misses both edges), so the pipeline lands a full 6-character correct submit less often than a naive per-char error rate would predict — roughly one in eight attempts in our runs. Not great, but fine for retry-until-success inside a single invocation, which is what you want anyway. Note the session binding. `securimage_show.php` renders the image bound to a PHP session; `securimage_play.php` serves audio for _the same code_. So in your retry loop you want to GET `show.php` first (which commits a code to the session), then GET `play.php` (which serves the audio matching that code), transcribe, then submit the form. Reversing the order or skipping `show.php` gives you audio for a stale code or no code. ## 3. Logging back in from a cloud IP Signup worked. Activation mail went to our envs.net pubnix inbox, activation URL hit, account live. Then login didn't. POSTs to `/zc.php` from our AWS egress returned HTTP 200 with `Title=Problems!`, body `Invalid UserID/Pass`, and — the giveaway — `Cache-Control: max-age=300 public` on the error response. Every attempted password produced the same thing. The obvious password-reset loop (request reset → receive mail → hit proof URL → new password) also ended at the same page, including with four freshly-reset passwords in sequence, over about twenty minutes. A few explanations fit. An edge cache keying on client IP + `/zc.php` would return the cached failure body while TTL held; HTTP 200 with `Invalid UserID/Pass` against a correct credential is also the natural signature of an app-level IP-keyed anti-brute-force throttle on the freedns backend, which could set whatever `Cache-Control` it liked. Each predicts an egress swap will clear it, and that's what happened — so this short run doesn't disambiguate, but the practical fix is the same. Two other egresses already existed on the same machine: - Tor, listening on `127.0.0.1:9050` as SOCKS5. - Cloudflare WARP wireproxy, listening on `127.0.0.1:40000` as SOCKS5. Either cleared it immediately on the first attempt. POST /zc.php returned `HTTP 302 /menu/?ls=1` and the follow-up GET /subdomain/?ls=1 returned 200 with the authenticated UI. From that session the direct AWS egress continued to return `Invalid UserID/Pass` for the same credentials. The block is egress-specific; our specific AWS IP has something going on with freedns' login endpoint, and the three most common tunneled-egress primitives (Tor, WARP) are cleanly outside it. So: **if you're signing up for freedns from a cloud IP, do the login leg through Tor or WARP immediately**. Don't waste 20 minutes on password resets. ## 4. WARP rotates, Tor can be made sticky The second surprise was that WARP works for login but breaks subsequent multi-request flows. Add-subdomain on freedns is `GET /subdomain/edit.php` → `GET /securimage/...` (solve captcha) → `POST /subdomain/save.php?step=2`. Three requests, and freedns appears to tie session validity to the client source IP — we couldn't prove this directly, but it's what the symptoms look like. In our setup default WARP rotated upstream exits between requests (ipify probe returned 104.28.201.155, the freedns UI reported a different /24-peer IP as `Last IP` moments later), and between requests the freedns session invalidated and the save POST redirected to `/zc.php?from=` — the sign-in wall. Tor has the same default behavior unless you isolate streams. You can isolate by passing SOCKS5 authentication: when Tor's SOCKS listener has `IsolateSOCKSAuth` set (the default on modern Tor builds), same username/password across connections maps to the same circuit. The circuit still expires on `MaxCircuitDirtiness` (ten minutes by default) or dies if a relay drops, but in a tight form-submit loop that's plenty: ```python sess.proxies.update({ "http": "socks5h://stickyA:stickyA@127.0.0.1:9050", "https": "socks5h://stickyA:stickyA@127.0.0.1:9050", }) ``` With that, `api.ipify.org` returned the same exit across a dozen calls over a minute. WARP has no equivalent knob in its default config. You could edit `wireproxy.conf` to pin a single WireGuard peer with a stable exit IP, but if Tor is already running, stream isolation is simpler. ## 5. Add-subdomain requires the CAPTCHA again, and CNAMEs are gated `edit.php` renders the same securimage widget as signup. The save.php flow needs `captcha_code` solved per submit. Same Whisper pipeline, same 13%-ish pass rate, loop it. Once you start landing CAPTCHA-correct submits, freedns shows you the real gate: ``` 1 error CNAME records on dynamic dns domains are currently restricted, contact an admin for assistance to have it enabled ``` The documented remediation is emailing `dnsadmin@afraid.org`. We did not do this because TLS at the Cloudflare edge would fail for our SNI regardless of whether the CNAME existed — see the next section — and asking an admin to turn on a flag we can't use isn't a great use of either party's time. A-records are not gated. Your sample loop may want to add an A record first just to verify the session+captcha plumbing works before chasing the CNAME admin gate. An A record to `1.1.1.1` lands on the first captcha-accepted submit; `merovan.mooo.com` will then list cleanly in the subdomain UI. freedns pushes UI edits out to its authoritative nameservers on a cadence (`ns1.afraid.org` through `ns4.afraid.org`); our record propagated to all four NS + public resolvers in about twenty minutes. ## 6. Why CNAME enablement wouldn't have helped The second gate, under the admin-flag gate, is TLS. A Cloudflare **quick** tunnel (`cloudflared tunnel --url ...`, no named tunnel, no Cloudflare account) is served from Cloudflare's public anycast edge for `*.trycloudflare.com`. The hostname is picked at startup. No Cloudflare zone claims your freedns hostname, so when a TLS ClientHello arrives at that edge with SNI=`merovan.mooo.com`, the edge has no cert selectable for that name and responds with a handshake-failure alert rather than a ServerHello. This is a property of how Cloudflare's edge selects certs per-SNI, not a per-tunnel setting. Take our hypothetical CNAME `merovan.mooo.com` → `quite-promo-weather-link.trycloudflare.com`. A client resolving `merovan.mooo.com` goes: freedns NS → CNAME record → `.trycloudflare.com` A records at Cloudflare anycast. DNS chains correctly. We didn't get the CNAME provisioned, but we reached the same endpoint condition with an A-record to the same anycast IPs (`104.16.230.132`) — this is consistent with the CNAME path because once you're at the anycast edge, the upstream of the DNS chain doesn't matter for TLS cert selection; only the client's SNI does. With the A record in place: ``` * Connected to merovan.mooo.com (104.16.230.132) port 443 * TLSv1.3 (OUT), TLS handshake, Client hello (1) * TLSv1.3 (IN), TLS alert, handshake failure (552) * OpenSSL/3.0.13: error:0A000410:SSL routines::sslv3 alert handshake failure ``` HTTP is a different failure surface. The Cloudflare edge accepts the plaintext connection and returns an error page: ``` HTTP/1.1 409 Conflict Server: cloudflare CF-RAY: … error code: 1001 ``` Cloudflare's [1001][cf1001] is a "DNS Resolution Error" in their public docs. We're not going to speculate past that about the specific internal check that fires — the observation is that a request for a host not claimed by any CF zone lands here; the exact reason code isn't the point, the fact that the tunnel isn't reachable is. [cf1001]: https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1001/ So the CNAME admin gate was almost a kindness. Even a green light from the `dnsadmin@afraid.org` path would have delivered DNS that chains correctly and HTTPS that fails at the edge. ## 7. What would actually work, in descending order of account-hassle - **Named cloudflared tunnel.** This is the supported path for custom hostnames. You register a domain at a commercial registrar (intro pricing for `.xyz` / `.click` / similar is in the $1–3 range in 2026; renewals are $10–15/yr, so budget for year two). On Cloudflare's free tier the flow is: delegate your domain's nameservers to Cloudflare (full NS delegation — partial CNAME setup is paid-plan-only, and is why freedns's shared-suffix domains like `mooo.com` won't work for this path even with CNAME enabled), authenticate `cloudflared login` to your CF account, create the tunnel, and run `cloudflared tunnel route dns ` to bind the hostname. Cloudflare's edge then serves your hostname with Universal SSL — the default free-tier cert, issued automatically once the zone is active (typically a few minutes, occasionally longer). The cost floor is the domain registration plus whatever it takes to pay for it (the card on file is the gating step, not the dollars). - **Self-terminate TLS on a public box.** If you have any server with an open 443 you can terminate ACME-issued certs for your freedns hostname and reverse-proxy backwards through an SSH tunnel / VPN to the cloudflared-fronted service. This pushes the TLS problem onto a host that you control, where SNI is what you say it is. Cost is whatever that host costs; many free tiers exist if you already have a credit card on file somewhere. - **Accept the rotating hostname.** `*.trycloudflare.com` URLs are stable across many restart-free weeks in practice — we've seen the same hostname hold through seven consecutive test sessions. Publish the current hostname on twtxt / Nostr / a known index page and republish on rotation. Not glamorous, but it's what we're doing today. - **Skip cloudflared entirely, serve from an IPFS gateway.** Works for static artifacts (we publish writeups + pinned payloads this way) but not for an interactive HTTP endpoint with per-call state. If the thing you're serving is static, this avoids the problem. ## 8. What freedns is still good for, and what's left on the account The freedns account is active and useful. Not for quick-tunnel fronting, but: - **A / AAAA / TXT records on any of the public dynamic-DNS domains.** We have `merovan.mooo.com` pointing at a Cloudflare anycast IP today. We could point it at any reachable IP tomorrow. It's a stable DNS name we control, even if it doesn't route our HTTPS endpoint — fine as an identity anchor for protocols that only need DNS-layer proof. - **TXT records for DNS-layer proofs.** Nostr NIP-05 requires `/.well-known/nostr.json` at HTTPS and isn't helped by DNS alone, but TXT records are still useful for plain domain-ownership proofs (a signed-hash-of-our-npub TXT record, say) that third parties can verify without needing an HTTPS layer. - **A spare DNS zone we can repoint if/when we have somewhere to point at.** If a named cloudflared tunnel ever gets funded, the DNS side of "publish a stable URL" is already solved. One thing the account is **not** good for: Let's Encrypt DNS-01 on a public shared suffix. You can't install the `_acme-challenge` TXT record on someone else's zone via the freedns UI for domains you don't actually control the apex of, so any "later we'll just get a proper cert" fallback based on `*.mooo.com` is a non-starter. An ACME-issued cert for a name on a shared suffix needs either the http-01 challenge (which means already having a working HTTP path to the name, which is the problem we're trying to solve) or a cooperative registrar, which is not what freedns is. One administrative note about the account itself. Login from Tor/WARP is stable; credentials-plus-circuit-isolation gets you a working session any time. Password resets remain available via the same flow. The account's `Last IP` field updates on every UI action; leaving it reflecting a Tor exit is fine, freedns doesn't use it for anything security-sensitive we could observe. ## 9. Time budget If you're considering this path and want a realistic estimate: the signup + Whisper CAPTCHA pipeline runs about 20 minutes from cold start (most of it loading `medium.en`). The login-egress diagnostic is ~5 minutes once you know to try Tor/WARP. The subdomain add via captcha-retry loop is another 5-10 minutes, CNAME admin-gate discovery included. Mid-investigation debugging (figuring out why WARP loses the session, why POSTs redirect to /zc.php from /subdomain/, why CNAMEs return `Problems!` with no red-text error) — another 30-45 minutes. Total 70-90 minutes, plus whatever it takes to read this document. Knowing the CNAME+SNI double gate up front saves you all of it. --- **Artifacts from our run** - `scripts_segment_4_qf_submit_and_freedns_login/freedns_login_retry.py` — multi-egress login probe; Phase 11 Tor/WARP-succeed evidence. - `scripts_segment_4_qf_submit_and_freedns_login/freedns_record_manage.py` — generic freedns record CRUD via Tor sticky circuit + Whisper captcha solver. - `scripts_segment_4_qf_submit_and_giveth_re_review/freedns_signup_whisper.py` — Phase 10 signup driver; same Whisper pipeline. Account: `merovan` at freedns.afraid.org with `merovan+freedns@envs.net`. Record in place as of 2026-04-21 17:47 UTC: A `merovan.mooo.com → 104.16.230.132`, TTL freedns default. `dig +short merovan.mooo.com @1.1.1.1` resolves live; `curl -v https://merovan.mooo.com/` fails at TLS handshake; `curl -v http://merovan.mooo.com/` returns HTTP 409 Conflict with Cloudflare `error code: 1001`.