CLI / TUI that lists listening TCP/UDP ports on Windows, Linux, and macOS.
Think of it as a port-focused lsof: quickly see who is holding a port, then kill the process if you need to.
Binaries are attached to GitHub Releases (lsoff-darwin-arm64, lsoff-linux-amd64, lsoff-windows-amd64.exe, …).
winget install yutat23.lsoffInstall it from the Homebrew tap. Homebrew 6 requires trusting a third-party tap first:
brew tap yutat23/tap
brew trust yutat23/tap
brew install lsoffgo install github.com/yutat23/lsoff@latestgit clone https://github.com/yutat23/lsoff
cd lsoff
go build -o lsoff .A tagged push (git tag v0.1.0 && git push origin v0.1.0) builds those binaries and publishes the release.
macOS uses libproc, so CGO is required (enabled by a normal go build).
lsoff # TUI (prints a table if stdout is not a TTY)
lsoff 8080 # show processes holding port 8080
lsoff nginx # search by name, path, PID, cmdline, project, service…
lsoff -q "node 8080" # space-separated words are AND
lsoff -t # TCP only (TUI)
lsoff -p # hide rows with an unknown PID
lsoff -u 53 # UDP port 53
lsoff --json nginx # search results as JSON
lsoff -k 8080 # kill those processes (with confirmation)
lsoff -k -y 8080 # skip confirmation (required when piped)
lsoff -h
lsoff -vExample output for a port query:
PROTO PORT ADDRESS PID PROJECT PROCESS PATH CMD CWD
tcp 8080 127.0.0.1 41233 lsoff node /usr/local/bin/node /usr/local/bin/node ~/mywork/lsoff
| Key / action | What it does |
|---|---|
/ / click Search: / ctrl+f |
Search (port, PID, name, project, path, cmdline; spaces are AND) |
↑ / ↓ / j / k / click / wheel |
Move and select (works while searching too) |
ctrl+u / ctrl+d |
Move one page up / down (works while searching too) |
| Click a header | Sort by that column (click again for descending) |
s / S |
Cycle sort column / toggle ascending-descending |
p |
Hide rows with an unknown PID (toggle) |
4 |
Show IPv4 rows only (press again to clear) |
6 |
Show IPv6 rows only (press again to clear) |
y |
Copy the selected addr:port |
a |
Auto-refresh every 2 seconds (toggle) |
enter / space / click ▸ |
Expand or collapse sockets within a group |
h / l |
Collapse / expand |
esc / ctrl+c |
Clear the search |
r |
Reload the list now |
x |
Kill the selected process (asks first) |
q |
Quit |
In the TUI, tcp is green and udp is amber. The selected row uses the highlight background instead. The CLI table stays uncolored so it stays script-friendly. Process names, command lines, and paths are sanitized for the terminal (control characters and ANSI/OSC sequences). JSON keeps the original strings.
Multiple bindings for the same logical process or Docker container endpoint (same protocol and port, such as an IPv4 + IPv6 pair) start collapsed as one row with ▸ and a +N count. enter expands them into a small tree: the head shows ▾, the last child is drawn with └─, and every child before it with ├─. So a pair of sockets shows a single └─ child, and three or more chain as ├─, … , └─. Different ports and protocols remain separate groups; Docker groups use the container identity rather than a PID.
The 4 and 6 filters are mutually exclusive. Press the other key to switch families, or press the same key again to show both.
Known service names (http, postgres, redis, vite, …) are searchable and shown on the SVC footer line. Ambiguous ports such as 3000 are aliases-only and have no single display name. Historic ports (echo, chargen) are not included. JSON may include "service" when a display name exists.
After x, press y to confirm, or n / esc to cancel. Kill verifies that the process is still the same one that was listed: Linux uses pidfd_open (the fd refers to that process, not the PID number), macOS checks pbi_start_tvsec/usec then signals, and Windows checks CreationTime on an open process handle. On Unix it sends SIGTERM first, then SIGKILL if that same process is still alive after 2 seconds. On Windows it uses TerminateProcess. pid 1 and the lsoff process itself are never killed.
CLI -k follows the same rules. If the same PID appears twice (IPv4 and IPv6), it is killed only once. When stdin is not a TTY, -y is required so a pipe cannot kill by accident.
- macOS kill is not atomic. There is no pidfd. lsoff re-reads
pbi_start_tvsec/usecimmediately beforekill(2), but a PID can still be reused in that tiny window. Linux pidfd and Windows process handles do not have this gap. - Windows cmdline/cwd use
NtQueryInformationProcess, which Microsoft documents as an internal API that may change. A 64-bit build reads WOW64 (32-bit) process strings from the 32-bit PEB;ProcessCommandLineInformationis parsed with the caller's pointer size, then PEBCommandLineis used as a target-bitness fallback. A 32-bit lsoff cannot inspect 64-bit processes' cmdline or cwd. - Version is baked in at
go buildtime. Rebuild after pulling iflsoff -vdisagrees withmain.go.
No external commands (lsof / ss / netstat).
| OS | API |
|---|---|
| Linux | /proc/net/{tcp,tcp6,udp,udp6}, /proc/<pid>/{fd,cmdline,cwd} |
| macOS | sysctl net.inet.{tcp,udp}.pcblist64 (all listeners), libproc (owning process and cwd) and sysctl kern.procargs2 |
| Windows | IP Helper, QueryFullProcessImageName, NtQueryInformationProcess (cmdline and cwd) |
Without permission, PID, path, and cmdline may be empty. Run as root / Administrator in that case.
Docker-published ports are supported as well. lsoff supplements kernel socket
discovery with published-port metadata from the local Docker Engine API over
/var/run/docker.sock; it does not imply that the container owns a host
namespace socket. The current user must have permission to access that socket.
If Docker is absent, the socket is unavailable, or access/API calls fail,
normal host-process discovery continues unchanged.
Docker rows show a container name such as docker:grafana in PROCESS. The TUI details show the container-side port and protocol, and the container ID. These rows have no PID, so p / -p hides them, and x / -k cannot stop containers.
UDP has no LISTEN state, so sockets bound to a port with no remote peer are shown.
MIT