Skip to content

About

Port-free service routing for Go tests and CI: dial services by name with readiness built into the dial, an optional HTTP forward proxy, and a self-healing Kubernetes port-forward backend.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

76 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-portless

Go Reference CI CodeQL License Go Version

Dial services by name in Go tests and CI — with readiness built into the dial.

Register a name and dial it instead of hardcoding 127.0.0.1:8888 or racing to find a free port. The dial blocks until the backend accepts, so a test dials a service that is still starting instead of polling for it, and backends self-heal across restarts. Ports leave your vocabulary: the port-free backends never surface one.

Inspired by portless.sh, rebuilt for test infrastructure: zero-root, no /etc/hosts, no daemon required for in-process use.

Quickstart

Run a process on an assigned port and reach it by name — no port picked:

brew install --cask sanketsudake/tap/portless
# or:
go install github.com/sanketsudake/go-portless/cmd/portless@latest

portless run web -- go run ./cmd/server   # binds $PORT, names it "web"
eval "$(portless env)"                    # export HTTP_PROXY / NO_PROXY
curl http://web/healthz                   # blocks until "web" is serving

From Go, point any http.Client, gRPC, or websocket dialer at the registry:

reg := portless.New()
defer reg.Close()
reg.Add(ctx, "web", backend.Future())      // OS assigns the port later
// … start your server on net.Listen(":0"), then f.SetListener(l) …
resp, err := reg.HTTPClient().Get("http://web/healthz")

Runnable examples and the full API are on pkg.go.dev.

How it works

A dial resolves the route name, falls back for unknown names, then runs the readiness loop through middleware and the port map to the backend, which returns a net.Conn.

Registry.DialContext has the same shape as net.Dialer.DialContext, so HTTP, WebSockets, gRPC, and raw TCP all route through one path.

WebSockets, both major libraries — with gorilla/websocket, inject the dialer:

d := websocket.Dialer{NetDialContext: reg.DialContext}
conn, _, err := d.Dial(portless.WSURL("web", 0, "/stream"), nil)

coder/websocket has no dialer hook; inject the HTTP client instead:

conn, _, err := websocket.Dial(ctx, portless.WSURL("web", 0, "/stream"),
    &websocket.DialOptions{HTTPClient: reg.DefaultClient()})

The coder/websocket path works because Go's HTTP/1.1 101-upgrade response bodies are writable through a custom transport.

Backends

Backend Port Use for
backend.Future OS-assigned, supplied later a server you start in the test
backend.Listener OS-assigned (l.Addr()) an already-bound net.Listener
backend.Mem none (net.Pipe) serve HTTP with zero TCP sockets
k8s.PortForward none (pod stream) a Kubernetes Service or pod
backend.TCP / portless alias you supply it escape hatch: name an already-running address

For services you embed in the test process, backend.ListenAndAdd is the whole recipe in one call — bind 127.0.0.1:0, register the listener, hand it to the service's start options:

l, err := backend.ListenAndAdd(ctx, reg, "router")
// pass l to your service; consumers needing a real URL use Route.Addr()

A backend.Listener route is dial-ready as soon as the socket is bound (kernel accept backlog), which can be earlier than the service behind it is serving — pair TLS servers with RouteWithTLSHealth. For components that insist on port ints instead of listeners, backend.ReservePorts(n) returns n distinct free ports by holding all n listeners open before closing any; two sequential listen-:0-close calls can return the same port.

Plain-URL consumers

Some consumers cannot take a custom dialer or *http.Client: bare http.Post call sites, third-party SDKs, subprocesses, and URLs printed for humans to copy. ListenLocal gives them a real dialable address that still rides the route's readiness loop and self-healing:

l, err := reg.ListenLocal("router")
url := "http://" + l.Addr().String() // reaches the route; each connection dials fresh

Every accepted connection dials the backend independently, so a pod restart costs one retried dial instead of a dead tunnel. The listener is closed by Close; process-lifetime registries own it for the life of the process.

Transactional setup

AddReady registers, waits for readiness, and removes the route again on failure, so retries of the same name never hit ErrRouteExists:

rt, err := reg.AddReady(ctx, "controller", be)

Servers with DNS-rebinding protection

Some servers reject requests that arrive on a loopback connection with a non-loopback Host — exactly what name-based dialing over a port-forward produces — and answer 403. Register the route with a Host rewrite so the server sees a loopback name:

reg.Add(ctx, "api", b, portless.RouteWithHostRewrite("127.0.0.1"))

See writing-backends for the symptom and both fixes.

HTTPS

portless serve --tls terminates TLS so https://<name> verifies against a local CA:

portless serve --tls &
portless ca install        # trust the CA once (asks first)
curl https://web/healthz

Docs

Modules

Module Path Depends on
core github.com/sanketsudake/go-portless stdlib only
k8s backend github.com/sanketsudake/go-portless/k8s core + client-go
CLI github.com/sanketsudake/go-portless/cmd/portless core + k8s

Motivation

go-portless grew out of the fission test suite — FindFreePort races, hardcoded addresses, self-healing kubectl port-forward loops, ws:// rewriting — but none of that is fission-specific. Any Go project whose tests reach real services hits it.

License

Apache License 2.0 — see LICENSE and NOTICE.

About

Port-free service routing for Go tests and CI: dial services by name with readiness built into the dial, an optional HTTP forward proxy, and a self-healing Kubernetes port-forward backend.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages