funnel

overview

funnel is a tunneling proxy that exposes local services to the internet through websocket connections. it's similar to ngrok but self-hosted - you run your own server instead of relying on a third-party service.

i built it over a weekend as a Go learning project. the immediate motivation was needing to test webhooks from external services, but mostly i wanted an excuse to write something concurrent in Go.

architecture

the design is straightforward. a client connects to the server via websocket and registers a tunnel id. when someone makes an HTTP request to tunnel-id.server:port, the server proxies that request through the websocket to the client. the client forwards it to localhost, captures the response, and sends it back through the same path.

using websockets avoids NAT traversal issues - the client initiates the connection outbound, so no port forwarding or firewall configuration needed.

implementation

built with minimal external dependencies - primarily using Go's standard library. the core is structured around goroutines: each tunnel runs in its own goroutine, and each HTTP request spawns a goroutine for proxying.

the request/response flow works by serializing HTTP requests into a simple protocol that can travel over websockets. each message includes request/response metadata (method, headers, status) and the body.

technical challenges

routing with multiple clients. maintaining a mapping of tunnel IDs to active websocket connections while handling concurrent access. clients connecting and disconnecting meant the routing table needed thread-safe updates without blocking incoming HTTP requests.

HTTP header rewriting. some headers need modification when proxying. Host, X-Forwarded-For, and other proxy-related headers require special handling. figuring out which headers to preserve, which to rewrite, and which to strip took iteration. getting it wrong breaks things like absolute URLs in responses.

keeping connections alive. websockets and HTTP connections both have timeout issues. without explicit keepalive mechanisms, idle tunnels would die and clients wouldn't know until they tried to use them. implemented ping/pong frames for websockets and proper timeout handling for HTTP requests waiting on responses.

request/response correlation. when multiple HTTP requests hit the same tunnel concurrently, each needs its response matched back correctly. implemented a correlation ID system where each request gets a unique identifier that travels with it through the websocket.

connection lifecycle. handling in-flight requests when a tunnel disconnects. requests already sent to the client but not yet responded to need timeouts and error propagation back to the waiting HTTP client.

features

being self-hosted means there are no external rate limits or account requirements. traffic stays on your infrastructure and the code is open source under MIT, so it can be modified as needed.

the server includes a REST API for monitoring - tunnel statistics, active connections, historical data. this was initially added for debugging performance issues but turned out to be useful for general observability.

lessons

the main goal was learning Go, particularly its concurrency model. goroutine lifecycle management and proper context usage became clearer when dealing with actual connection handling rather than toy examples.

learning Go while also learning websockets made things harder than expected. reading the websocket RFC helped but implementing ping/pong, handling fragmentation, and understanding close handshakes only clicked when debugging real issues. got interested enough in the protocol to consider implementing it from scratch in C eventually.

keeping dependencies minimal forced working directly with protocols rather than relying on abstraction layers.

status

the project is functional and has users beyond myself. a public documentation site runs at funnel.karolbroda.com. source code is available on github under MIT license.

current work includes adding authentication and a web dashboard for monitoring. the core tunneling functionality is stable.