Hi everyone,
I built a CLI tool in Go called park that solves a small but annoying problem: freeing a TCP port without killing the process that owns it.
Demo: Park Usage Demo - asciinema.org
The idea: you’re running a dev server on :8080. You need that port for something else. Instead of killing the server (and losing its warm state), you run park 8080 — the process freezes and the port genuinely
frees. When you’re done, park resume 8080 brings it back. Same PID, same memory.
Install:
curl -fsSL https://raw.githubusercontent.com/mr-vaibh/park/main/install.sh | sh
GitHub: GitHub - mr-vaibh/park: Pause a process and free its TCP port. Resume later, same PID and memory intact. Ctrl+Z for servers. · GitHub
Website: park - Pause processes, free ports, resume later
How it works
SIGSTOP alone doesn’t free the port — the kernel keeps the socket bound. park actually reaches inside the target process and injects syscalls to close the listening fd.
Linux (x86_64): Pure Go, no CGO. Uses ptrace to attach, finds the listener fd via /proc/net/tcp, writes a syscall; int3 stub to the vdso page, injects close(fd). Resume injects socket + setsockopt + bind + listen + dup2 to recreate the listener at the original fd number.
macOS (Apple Silicon): Uses CGO for Mach APIs. task_for_pid to get the task port, mach_vm_allocate for scratch pages (16KB each — Apple Silicon page size), thread_create_running to spawn a fresh
injection thread inside the target. The stub is arm64 LDR + svc + brk instructions with PC-relative loads from a separate data page. Breakpoints are caught via a Mach exception port. Resume also injects
kevent() to re-register the listener with kqueue and fcntl(O_NONBLOCK) so async frameworks survive.
Some Go-specific details
-
Build tags:
ptrace_linux_amd64.go(pure Go),ptrace_darwin_arm64.go(CGO +//go:build darwin && arm64 && cgo),ptrace_darwin.go(fallback stub for!cgoor!arm64). Cross-compilation from any host
works because the CGO path is gated. -
The arm64 instruction encoding (
encodeLdrLiteral,encodeBR) is done in pure Go at runtime — no assembler, no hardcoded bytes. The stub bytes are computed from the actual scratch page addresses. -
One external dependency:
golang.org/x/sysfor the Linux ptrace wrappers. -
CI runs the full integration test on
ubuntu-latest: spawns an HTTP server, parks it, binds something else to the port, resumes, and asserts it still serves.
Tested with python http.server, uvicorn/FastAPI, Flask. Works on macOS Sequoia (M1) and Linux x86_64.
Would love feedback from anyone who’s worked with ptrace or Mach APIs in Go. The Mach exception port handling in particular was an adventure — happy to discuss.