goports runs as a macOS menu‑bar app and CLI utility that shows every
listening TCP socket (and now UDP listeners too). IPv4 and IPv6 addresses are
handled, and the output identifies the protocol so you can distinguish
tcp/80 from udp/53 easily. It's a single Go binary with no external
runtime requirements.
Whether you’re developing servers, debugging networking issues or simply curious, goports lets you inspect, open or kill port owners without leaving the keyboard.
- Install – From this repo’s GitHub Releases (tags
v*), grab the binary for your OS, or build:make build→./bin/goports. - Inspect –
./bin/goportslists listeners once;./bin/goports --watchrefreshes every 5s. - Script –
./bin/goports --jsonor--csvfor machine-readable output;./bin/goports --helpfor grouped flags and exit codes;./bin/goports --versionfor the build label. Use-q/--quietfor plain tables (and non-TTY-friendly--watchwithout cursor tricks). - Optional API –
./bin/goports --http-port 8765then openhttp://127.0.0.1:8765/for the activity UI (see HTTP API).
On macOS, ./bin/goports --gui starts the menu-bar app (if built with GUI support).
goports exposes the same discovery engine to both a menu-bar GUI and a command-line interface. Highlights:
- Real-time port monitoring – all listening TCP sockets (and UDP listeners) are listed and updated every few seconds.
- Host name resolution — reverse DNS is performed on each address, so
127.0.0.1may appear aslocalhost. - Application identification — see the executable name and, when
possible, its
CFBundleIdentifier. The GUI looks up a matching bundle and icon via Spotlight; for plain binaries we fall back to built-in PNGs for a handful of common tools (ruby,prometheus,netcat,nxrunner,onedrive,rapportd, etc.). Icons require thesipstool; the code uses a temporary file to avoid earlier "exit status 13" problems. Launch the GUI from a terminal and check stderr foriconForBundle:messages if something still goes wrong. - Process control — terminate listeners directly from the menu or
with
--kill. - Activity events — the internal API now publishes open/close events for
ports; consumers can
SubscribeActivityin-process or connect over HTTP. The binary can start a small local web server (--http-port) exposing/events(SSE) and a basic web UI that shows a live log of opens/closes. External applications may also consume the SSE stream for their own graphs or dashboards. - Browser integration —
--openor the GUI menu item launcheshttp://localhost:<port>in the default browser. - Lightweight Go binary — single executable produced with
go build; legacy Python/py2app support is only retained for historical reference. - Configurable preferences — a Settings submenu controls start‑at‑login
(launch automatically when you sign in), notifications, and refresh
interval; preferences persist in
~/.config/goports/settings.json.
- macOS (apps use
lsofand Cocoa menu bar APIs) lsofmust be present on your PATH (installed by default on macOS)sipsrequired if you want application icons in the GUI- optional: Spotlight indexing enabled for icon resolution
Note: the CLI engine is now abstracted so ports discovery can be implemented on non‑macOS platforms. The application separates GUI code behind build tags; non‑darwin builds produce a CLI-only binary that will compile anywhere. Currently only macOS provides a working backend, but the scaffolding is in place for Linux/Windows support.
See the Quick start at the top of this file for the shortest path from binary to JSON/API.
You can download a pre‑built bundle from the dist directory on the
repository, for example:
``` https://raw.githubusercontent.com/alextheberge/goports/master/dist/goports.zip ```
Alternatively clone the repo and build locally (see Building below).
Legacy Python source has been moved to legacy/python for historical reference; the modern code is all Go.
The project is now a pure Go application; there is no Python dependency.
Use the standard make targets to compile and package.
```bash
make build
make build-app # writes goports.app
make run-app
make dist # writes dist/goports.zip ```
Drop goports.app in your /Applications folder after building, or unzip the
archive produced by make dist.
⚠️ make python-buildandsetup.pyare maintained only for historic reference; they no longer produce a usable application.
This repo tracks a multidimensional version in mvs.json ([ARCH].[FEAT].[PROT]-[CONT]). Source comments such as @mvs-feature(...) and @mvs-protocol(...) tag capability and integration surfaces; the public CLI entrypoint cli.Run is the Go API boundary for lint. After changing flags, HTTP contract, or those tags, refresh the manifest and commit it:
# install mvs-manager once (see upstream README for Windows)
curl -fsSL https://raw.githubusercontent.com/alextheberge/MVSengine/master/scripts/install.sh | bash
make mvs-generate # reconcile mvs.json with the tree
make lint-mvs # CI check: manifest matches codeUse make install-hooks to point Git at .githooks so commits run make lint-mvs. GitHub Actions (.github/workflows/ci.yml) runs go vet, go test, go build, a Windows cross-compile, and mvs-manager lint on pushes and pull requests.
Pushing a tag matching v* (for example v0.3.0) triggers .github/workflows/release.yml, which uploads Linux, Windows, and macOS binaries plus a checksums.txt file to the GitHub release.
goports enumerates listening sockets via a platform-specific engine.
On macOS the implementation now reads listener information via the
sysctl interface (net.inet.{tcp,udp}.pcblist) and subsequently scans
process file descriptors via libproc, so PIDs, names and command-line
paths are obtained without ever spawning netstat/lsof. A short in‑memory
cache (≈1 s) avoids repeated kernel calls during rapid polling. Netstat is
only consulted as a fallback if the syscall fails. On Linux we likewise
parse /proc/net/* and then inspect /proc/<pid>/fd entries to map sockets
back to processes, populating PID, command line and executable name; the
native map is also cached briefly. On Windows the backend uses the IP Helper
API (GetExtendedTcpTable/GetExtendedUdpTable) plus
QueryFullProcessImageName to provide equivalent native data. In all cases,
fallback to the appropriate system tool (lsof/netstat) remains possible if
the native call fails.
The code is structured so that fully native backends (e.g. Win32 APIs) may
replace the external command dependencies later without touching rendering
logic.
The CLI output includes a PROTO column so the protocol is obvious. When
running the GUI you will initially need macOS; non-darwin builds are CLI-
only. Both GUI and CLI share the same engine; items appear in the menu bar
and the table automatically.
Behaviors you get “for free”:
- reverse DNS on local addresses (
127.0.0.1→localhost, bracketed IPv6 addresses are also handled) - process bundle ID and icon lookups when available
- GUI menu‑bar titles now include the PID(s) and a short command‑line snippet for each listening process, making it easy to identify and drill into the responsible application without opening a submenu
Run goports --help for the full grouped list. Summary (note: PROTO and
FAMILY columns appear when no filters are applied):
-h, --help # usage (exit 0)
--version # build version (set at link time for releases)
# Exit codes: 0 ok, 1 error, 2 bad flags (Go flag package), 3 kill had failures
--watch, -w # refresh every 5 seconds
-q, --quiet # plain table columns; with --watch, plain refresh if stdout is not a TTY
--json # structured JSON
--csv # CSV rows
--tui # ASCII graph in the terminal
--export # dump activity history as JSON and exit
--spec # print OpenAPI JSON and exit
--kill PORT # kill listeners on PORT (any protocol)
--kill-name SUBSTR # kill processes whose command contains SUBSTR
--kill-bundle SUBSTR # kill processes whose bundle id contains SUBSTR
--signal NAME # TERM, KILL, etc. (default KILL)
--proto tcp|udp # filter by protocol
--name SUBSTR # filter by process name
--bundle SUBSTR # filter by bundle id substring
--family IPv4|IPv6 # filter by address family
--native # native discovery only (no lsof fallback)
--http-port N # serve HTTP API (see below) on port N
--open PORT # open http://localhost:PORT in the default browser
# Parsed on all platforms for a consistent argv; effective on macOS GUI only:
--gui # menu-bar app (handled in main before CLI)
--webview-width N
--webview-height N
--webview-x N
--webview-y N
--webview-debug
--webview-title TEXT
--webview-child [URL] # internal webview helper processWhen the CLI is started with --http-port (or via the GUI "Open in
Browser" action), a tiny HTTP server listens on localhost. It
supports:
GET /events– a continuous stream of port open/close events. By default it uses Server-Sent Events (SSE); clients may upgrade to a WebSocket by supplying theUpgrade: websocketheader. The stream optionally acceptssince(RFC3339 timestamp) andtokenquery parameters for historical replay and authentication.GET /status– return a simple JSON object containing the current number of open ports (e.g.{ "open": 5 }). This is useful for initializing dashboards.GET /history– a JSON array of recent events whose history is kept in memory (size configurable at compile time). Supports filtering byprotocol,port,sinceandlimit. Authentication is enforced via theGOPORTS_API_TOKENenvironment variable; supply the token either as atokenquery parameter or as aBearerAuthorization header.GET /openapi.json– a machine-readable OpenAPI 3.0 schema describing the above endpoints, including parameter types and response models. Third-party tools (Swagger UI, curl, etc.) can consume this spec.
The server honors GOPORTS_TLS_CERT/GOPORTS_TLS_KEY if present and will
fall back to generating a short-lived self-signed certificate if
--http-tls is requested but no files are provided. It also supports
listen addresses prefixed with unix: to bind a UNIX domain socket.
The embedded web UI served at / displays a simple Chart.js graph of
port activity and can be used as an example client. The page is lightly
styled with Tailwind CSS for a cleaner look. On startup it first queries
/status for the current open‑port count, then replays any recent history;
this ensures the chart always has a sensible baseline even if no ports have
opened or closed yet. A second panel shows a live snapshot of all listening
ports and associated metadata (PID, command line, bundle identifier) drawn
from the /ports endpoint. The snapshot is refreshed whenever an open/close
event arrives. The page updates in real time, and controls let you reset
the stored history or download the entire buffer as JSON for offline
analysis.
Fetch the spec with curl:
curl http://localhost:<port>/openapi.json | jqStream events using SSE from bash:
curl -N "http://localhost:<port>/events?since=$(date -u +%FT%TZ)"A minimal JavaScript consumer:
<script>
let url = "/events";
let evt = new EventSource(url);
evt.onmessage = e => console.log("activity", JSON.parse(e.data));
</script>You can also view the API interactively by opening /swagger in your
browser; it renders the spec with Swagger UI.
Treat the HTTP server as local-only unless you know what you are doing.
Bind to loopback (default :port is all interfaces — prefer 127.0.0.1:PORT
or unix:/path for a socket). Set GOPORTS_API_TOKEN when anything other
than trusted clients can reach the port; pass the token as query token= or
Authorization: Bearer …. For TLS, supply GOPORTS_TLS_CERT and
GOPORTS_TLS_KEY, or use a reverse proxy.
Clients that send Accept: application/json receive structured errors on
failure, for example {"error":{"code":"unauthorized","message":"unauthorized"}}.
Other clients get plain text/plain bodies from net/http.
Set GOPORTS_LOG=debug for verbose slog output to stderr (and the same
lines append to settings.json.log next to your config). See
docs/RELEASING.md for tagging releases and
docs/MACOS_SIGNING.md for optional signing/notarization.
When running in GUI mode a Settings submenu is available. Highlights:
-
Show TCP / Show UDP – toggle visibility of each protocol from Settings.
-
Filter… – show only menu items matching a case-insensitive substring; the current filter is shown in the menu and persisted across launches.
-
Per-port notifications – each port submenu can mute or enable alerts; choices persist between sessions.
-
Use native discovery only – when checked the GUI avoids invoking
lsofand similar helpers and uses built-in platform APIs (sysctl/libprocon macOS,/procon Linux, IP Helper on Windows). Results are cached.Why enable this mode?
- Self-contained binary with no external command dependencies –
containers, sandboxes, or hosts without
lsof. - Less CPU/battery than repeated
lsofduring polling. - On Linux/Windows, native discovery is the primary path; the flag matches
the CLI’s
--native.
Downsides
The OS may hide process metadata (macOS SIP/sandbox, Linux
/procpermissions). You may seePid == 0and empty command lines; turn native mode off to allowlsoffallback when you need rich PID/cmdline data. - Self-contained binary with no external command dependencies –
containers, sandboxes, or hosts without
-
The menu bar icon switches between light and dark variants depending on system appearance.
-
Reset all preferences… – restores defaults (notifications, TCP/UDP, native mode, filter text, refresh interval, webview fields, blocked notification map). Confirms via a dialog before writing
settings.json. -
Embedded webview dimensions & title – if you prefer a different size for the activity graph window, you can set
webview_widthandwebview_heightin the config file (~/.config/goports/settings.json) or launch the GUI with--webview-width/--webview-height. You can also specify a custom window title using thewebview_titlefield or the--webview-titleflag. A--webview-debugflag enables the underlyingwebviewlibrary's debug mode (useful when the window fails to appear). The Settings menu now offers Webview width…, height…, and title… items, plus Reset webview settings to restore defaults.
When the embedded view is requested the app no longer tries to host it in
its own process; instead it spawns a helper copy of itself (--webview-child)
on the main thread so macOS’s WebKit requirements are satisfied. The
parent menu-bar process therefore cannot crash if webview initialization
fails. The helper is launched directly (not via open) which lets us capture
its stdout/stderr for logging; this is also why you may see file descriptors
in use after the call. The child process explicitly locks its OS thread
before creating and running the webview; failing to do so previously caused
nothing to appear while the helper stayed alive with no errors logged. If
you still encounter a blank window check the diagnostic log – you should see
messages like
webview child starting with URL=…
webview.Run about to execute
webview.Run returned, exiting child
On non‑macOS builds the internal/gui package is a no-op stub – there is no
menu bar or embedded webview, but the HTTP API still runs and you can open
the activity graph manually in your browser (--browse flag). A proper
tray‑icon GUI for Linux/Windows is intended for a future release.
The helper now also attempts to place the window at sensible coordinates –
by default it sits about 100 points from the left edge and 50 points below
the top of the main display (i.e. just under the menu bar). It will then
slide into position with a short animation and activate the app so the
window floats above other applications such as VSCode. You can customise
the position via the --webview-x/--webview-y CLI flags or in the
Preferences menu; y is interpreted as a distance from the top of the
screen. The settings are persisted along with the width/height/title. If
the helper still refuses to appear the log will contain panic information.
If the child cannot be started a notification is shown, the menu item is
disabled and relabelled "Activity Graph (unavailable)", and the page opens in
your default browser. Diagnostic messages are emitted to stderr and also
appended to a log file alongside the configuration (e.g.
/Users/…/Library/Application Support/goports/settings.json.log). Inspect
that file if you don’t run from a terminal – it will contain the child
process output and error reasons.
- Descriptive tooltips on menu items improve accessibility for assistive technologies such as VoiceOver.
The following preferences are persisted across launches:
- Start at Login – toggles whether goports is added to your macOS login items. Enabling will attempt to create/delete a System Events login item via AppleScript; failures are non‑fatal.
- Enable Notifications – when on, the menu will send native notifications for ports opening and closing. Turning it off silences those alerts.
- Refresh interval – click repeatedly to cycle the polling interval between 5, 10 and 15 seconds; the current value is shown in the menu label.
These settings are stored in ~/.config/goports/settings.json.
--gui— launch the menu‑bar GUI (default when no flags are provided). Ignored on non‑macOS builds where only the CLI/API is available.--native— avoid calling external tools (likelsof); use built-in platform APIs only. See the discussion above about native discovery limitations: metadata may still be missing if the kernel refuses to expose it. The environment variablesGOPORTS_DEBUG=1andGOPORTS_FAKE_SYSCTL=1can be used to diagnose or simulate failures when running from a terminal.--watch,-w— refresh the CLI output every 5 seconds.--kill <port>— terminate all processes listening on<port>(any protocol). On Unix-like systems this sends the chosen signal, while on Windowstaskkillis invoked.--kill-name <substr>— terminate processes whose command name contains<substr>.--kill-bundle <substr>— terminate processes whose bundle identifier contains<substr>.--signal <name>— specify signal to use for kills (TERM, INT, KILL, etc.).--open <port>— openhttp://localhost:<port>in the default browser.--http-port <port>— if nonzero, start a local HTTP server on the given port. In addition to streaming activity events at/events, the server serves a minimal web UI on/that displays a scrolling log of changes and a live count graph. The page loads recent history on startup and provides buttons to reset the stored history or download it as JSON for offline analysis. A browser will automatically update as ports open and close; this makes it easy to view activity graphically without any external tooling. When running in GUI mode the menu bar shows a “View Activity Graph” item that opens the page at the appropriate address.
Examples:
# show a one‑shot table of current ports (proto column added)
./bin/goports```bash
./bin/goports
./bin/goports --watch
./bin/goports --kill 8080
./bin/goports --open 3000
./bin/goports --gui ```
The GUI mode is also the default when you launch goports.app from Finder.
Feel free to open issues or PRs. The implementation lives under internal/ and
is intentionally small — adding new features such as cross-platform support,
stats collection, or UI polish is straightforward.
