DEV Community

linou518
linou518

Posted on

Solving cloudflared's Random URLs — How I Fixed LINE Bot Permanently with a WebSocket Relay Server

Solving cloudflared's Random URLs — How I Fixed LINE Bot Permanently with a WebSocket Relay Server

The Problem: URL Changes Every Restart

I wanted to run a LINE Bot on a home network PC (a GMK mini PC).

LINE Webhooks require a fixed HTTPS URL. To expose a PC with only a private IP to the outside world, you need some kind of tunnel. My first choice was cloudflared (Cloudflare Tunnel).

cloudflared tunnel --url http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

This generates a temporary HTTPS URL that can be used for the webhook. It seemed perfect at first.

The issue was the "temporary" part.

Every time the PC restarts, the URL changes. Something like https://random-words-1234.trycloudflare.com. You have to log into the LINE Developers Console and manually update the Webhook URL every single time.

For a personal bot that's barely tolerable, but I was trying to embed this into a consumer-facing product (the GMK AI mini PC). You can't tell users "please update the URL after every restart."

Options I Considered

Approach Problem
cloudflared named tunnel Requires Cloudflare auth — too complex for end users
ngrok fixed URL Requires paid plan
Port forwarding Depends on user's router config — not practical
WebSocket relay server ← This is what I went with

The Solution: Build Your Own Relay Server

A shift in thinking. Instead of "expose the PC as a server to the outside," I changed to "have the PC connect out to a server."

LINE Platform
    ↓ Webhook (HTTPS)
https://linebot.techsfree.com/gmk/{token}/callback  ← Fixed URL!
    ↓ Nginx (443) → FastAPI (3500)
web server: gmk_relay.py  ← Relay server
    ↓ WebSocket forwarding
GMK PC: line_bridge_v2.py  ← WebSocket client
    ↓ OpenClaw chatCompletions API
    ↓ LINE Reply API (direct)
Enter fullscreen mode Exit fullscreen mode

The key is that the direction is reversed.

Old way: LINE → cloudflared → GMK PC (outside comes into the PC)

New way: LINE → relay server ← GMK PC (PC maintains connection to server)

If you keep a persistent WebSocket connection from the PC to the server, NAT traversal and firewalls become irrelevant. WebSocket is client-initiated, so it works in any environment.

Implementation

Relay server side (FastAPI + WebSocket)

# Core of gmk_relay.py (conceptual)
app = FastAPI()

# Hold WebSocket connections for each GMK unit
connections: dict[str, WebSocket] = {}

@app.websocket("/gmk/{token}/ws")
async def websocket_endpoint(websocket: WebSocket, token: str):
    await websocket.accept()
    connections[token] = websocket
    try:
        while True:
            await websocket.receive_text()  # keep alive ping/pong
    except:
        del connections[token]

@app.post("/gmk/{token}/callback")
async def line_webhook(token: str, request: Request):
    body = await request.body()
    ws = connections.get(token)
    if ws:
        await ws.send_text(body.decode())  # Forward to GMK
    return {"status": "ok"}
Enter fullscreen mode Exit fullscreen mode

GMK PC side (WebSocket client)

# Core of line_bridge_v2.py (conceptual)
async def run():
    relay_url = f"wss://linebot.techsfree.com/gmk/{token}/ws"
    async with websockets.connect(relay_url) as ws:
        async for message in ws:
            data = json.loads(message)
            # Process LINE event and generate AI reply
            reply = await call_openclaw(data)
            await send_line_reply(reply)
Enter fullscreen mode Exit fullscreen mode

Each GMK unit has a unique relay_token at startup and maintains a connection to the relay server. The LINE Webhook URL is fixed at https://linebot.techsfree.com/gmk/{token}/callback. It never changes after a restart.

The Gotcha: Hairpin NAT

I hit one snag during testing.

When the test GMK (on the local network) tried to connect to wss://linebot.techsfree.com/..., it timed out. The relay server was also on the same LAN, so hairpin NAT (accessing an external domain from within the same LAN) was being blocked by the router.

Solution: change only the test machine to connect via direct IP.

{
  "relay_url": "ws://192.168.x.x:3500/gmk/{token}/ws"
}
Enter fullscreen mode Exit fullscreen mode

Production users' devices are on different networks (their home LAN), so wss://linebot.techsfree.com/... works fine for them.

Results

  • ✅ Fixed Webhook URL (doesn't change on restart)
  • ✅ No cloudflared dependency (fewer moving parts)
  • ✅ Simple user setup: just paste a URL
  • ✅ One relay server handles multiple GMK units

The final user setup flow looks like this:

  1. Boot the GMK
  2. Create a LINE Bot in LINE Developers (get Channel Secret/Token)
  3. Enter Secret/Token in the Dashboard UI
  4. Paste the displayed fixed URL into LINE Console
  5. Done

That's consumer-product quality.

Summary

By shifting from "expose a service externally" to "have the service connect out to a server," we solved the cloudflared random URL problem at its root.

This pattern of using persistent WebSocket connections has applications beyond LINE Bots — IoT device remote management, reverse proxies for home servers, and more.


Tags: linebot, websocket, fastapi, nat, python, infrastructure, openclaw

Top comments (0)