uss is a Go library and CLI tool for parsing Linux socket statistics from the ss command output. It supports both INET sockets (tcp, udp) and UNIX sockets (u_str, u_dgr, u_seq) with multiple output formats.
- Parse
ssoutput: Handles both INET and UNIX socket formats - Multiple output formats: Human-readable tables, CSV, JSON, and YAML
- Robust parsing: Handles IPv4, IPv6 (with brackets and scopes), interface suffixes, abstract sockets
- Process metadata extraction: Extracts users, UID, inode, cgroup, and more
- Reusable library: Clean API for use in other Go applications
- CLI tool: Built with Cobra for easy command-line use
go install github.com/mobydeck/uss/cmd/uss@latestgit clone https://github.com/mobydeck/uss
cd uss
go build -o uss ./cmd/ussBy default, uss reads from stdin and outputs human-readable format:
ss -tunap | ussUse the --ss flag to execute the ss command directly with custom flags:
# Listen on all TCP ports
uss --ss tnl
# Show all TCP and UDP sockets with process information
uss --ss tunap
# Get UNIX sockets with all details
uss --ss peanutlx
# With leading dash (also works)
uss --ss -tnlThe --ss flag automatically parses output even when the Netid column is missing (e.g., from ss -tnl), inferring it as tcp by default. The leading dash in ss flags is optional.
Human-readable (default):
ss -tunap | ussJSON:
ss -tunap | uss --jsonPretty JSON:
ss -tunap | uss --json --prettyYAML:
ss -peanutlx | uss --yamlCSV:
ss -tunap | uss --csv > sockets.csvYou can also read from a file instead of stdin:
ss -tunap > sockets.txt
uss --json sockets.txtBy default, uss hides raw/unparsed fields for cleaner output. These include:
processRaw- The unparsed process metadata stringlocal- The raw local address:port token (before parsing)peer- The raw peer address:port token (before parsing)
To include these raw fields in the output, use the -r or --raw flag:
# Include raw fields in JSON output
ss -tunap | uss --json --raw
# Include raw fields in CSV output
ss -tunap | uss --csv --raw
# Include raw fields in YAML output
ss -tunap | uss --yaml --raw
# Include raw fields in human-readable output
ss -tunap | uss --rawThis is useful when you need to see the original unparsed data or for debugging parsing issues.
Filter socket entries using the -q or --query flag:
- Format:
field=value1,value2[,valueN] - Multiple values per field: comma-separated (OR condition)
- Multiple fields: space or semicolon separated (AND condition)
Single field:
# Find all TCP sockets
ss -tunap | uss -q "netid=tcp"
# Find sockets on port 22 or 80
ss -tunap | uss -q "port=22,80"Multiple fields (AND):
# TCP sockets on port 22 or 80 with uid 0 or 1000
ss -tunap | uss -q "netid=tcp port=22,80 uid=0,1000"
# Using semicolon separator
ss -tunap | uss -q "netid=tcp;port=22,80;uid=0,1000"Wildcards:
# All LISTEN sockets
ss -tunap | uss -q "state=LISTEN*"
# Unix sockets in /run directory
ss -peanutlx | uss -q "unixpath=*/run/*"Ranges:
# Ports between 1000 and 2000
ss -tunap | uss -q "port=1000-2000"
# UID between 1000 and 2000
ss -tunap | uss -q "uid=1000-2000"Substring matching:
# Find sockets with "docker" in path
ss -peanutlx | uss -q "unixpath=docker"
# Find sockets with specific cgroup
ss -tunap | uss -q "cgroup=systemd"Using --ss flag with output formats:
# Execute ss directly and output as JSON
uss --ss tunap --json
# Execute ss directly and output as YAML with filtering
uss --ss peanutlx --yaml -q unixpath=/run/*
# Execute ss and export as CSV
uss --ss tunap --csv > all_sockets.csv
# Execute ss and output as pretty JSON
uss --ss tnl --json --prettyCombined queries with output format:
# TCP LISTEN sockets on ports 22 or 80 with uid 0, output as JSON
ss -tunap | uss -q "netid=tcp state=LISTEN port=22,80 uid=0" --json
# Filter and output as CSV
ss -tunap | uss -q "netid=tcp" --csv > tcp_sockets.csvCommon:
netid- Socket type (tcp, udp, u_str, etc.)state- Socket state (LISTEN, ESTAB, UNCONN, etc.)recvq- Receive queue sizesendq- Send queue size
INET-specific:
port- Local or peer port (convenience field, matches both)address- Local or peer address (convenience field, matches both)localport- Local portpeerport- Peer portlocaladdress- Local addresspeeraddress- Peer addressinterface- Network interface
UNIX-specific:
unixtype- Unix socket type (u_str, u_dgr, u_seq)unixpath- Unix socket path (supports wildcards/substring)unixid- Unix socket IDunixpeer- Unix peer socketunixpeerid- Unix peer socket ID
Metadata:
uid- User ID (numeric)inode- Inode number (numeric)cgroup- Control group pathv6only- IPv6-only flag (0 or 1)fwmark- Firewall marksk- Socket kernel identifierdev- Device identifier
- Exact match: Default for most values (
port=22) - Wildcard: Use
*(any sequence) or?(single char) (state=LISTEN*) - Range: Use
min-maxfor numeric fields (port=1000-2000) - Substring: For string fields, partial matches work (
unixpath=docker) - Case-insensitive: All string matching is case-insensitive (
state=listenmatchesLISTEN)
Parse INET sockets:
ss -tunap | ussParse UNIX sockets:
ss -peanutlx | ussGenerate CSV report:
ss -tunap | uss --csv > sockets.csvYou can combine uss with jq for powerful JSON-based filtering and querying:
Filter by port:
# Find all sockets listening on port 80
ss -tunap | uss --json | jq '.[] | select(.localPort == "80")'Filter by state and protocol:
# Find all LISTEN TCP sockets
ss -tunap | uss --json | jq '.[] | select(.state == "LISTEN" and .netid == "tcp")'Filter by UID and extract specific fields:
# Find sockets owned by user 1000, show only address and port
ss -tunap | uss --json | jq '.[] | select(.uid == 1000) | {addr: .localAddress, port: .localPort, state: .state}'Count sockets by state:
# How many sockets in each state
ss -tunap | uss --json | jq 'group_by(.state) | map({state: .[0].state, count: length})'Find listening ports:
# Get all listening ports
ss -tunap | uss --json | jq -r '.[] | select(.state == "LISTEN") | "\(.localAddress):\(.localPort)"'Filter by cgroup:
# Find all sockets in systemd cgroups
ss -tunap | uss --json | jq '.[] | select(.cgroup | startswith("/system.slice"))'Complex filtering:
# TCP sockets that are either LISTEN or ESTAB, and not owned by root
ss -tunap | uss --json | jq '.[] | select((.state == "LISTEN" or .state == "ESTAB") and (.uid // 0) != 0)'Extract process information:
# Get process names and PIDs for each socket
ss -tunap | uss --json | jq '.[] | select(.users | length > 0) | {port: .localPort, processes: .users[].name}'Format as table:
# Create a custom table output
ss -tunap | uss --json | jq -r '.[] | [.netid, .state, .localAddress, .localPort, (.users[0].name // "unknown")] | @tsv'Find sockets by pattern:
# Find all UNIX sockets in /run directory
ss -peanutlx | uss --json | jq '.[] | select(.unixPath | startswith("/run"))'
# Find abstract sockets
ss -peanutlx | uss --json | jq '.[] | select(.unixPath | startswith("@"))'Export filtered results:
# Export only LISTEN TCP sockets to CSV-like format
ss -tunap | uss --json | jq -r '.[] | select(.state == "LISTEN" and .netid == "tcp") | [.netid, .state, .localAddress, .localPort, .uid] | @csv'import "github.com/mobydeck/uss"package main
import (
"fmt"
"os"
"strings"
"github.com/mobydeck/uss"
)
func main() {
// Sample ss output
input := `Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
`
// Parse the input
entries, err := uss.Parse(strings.NewReader(input), uss.Options{Strict: false})
if err != nil {
fmt.Fprintf(os.Stderr, "Parse error: %v\n", err)
return
}
// Print results
for _, entry := range entries {
fmt.Printf("%s %s %s:%s\n",
entry.Netid, entry.State, entry.LocalAddress, entry.LocalPort)
}
}package main
import (
"os"
"strings"
"github.com/mobydeck/uss"
)
func main() {
input := `...` // ss output
entries, _ := uss.Parse(strings.NewReader(input), uss.Options{})
// Render as pretty JSON (without raw fields)
uss.RenderJSON(os.Stdout, entries, true, false)
// Or render as JSON with raw fields
// uss.RenderJSON(os.Stdout, entries, true, true)
// Or render as YAML
// uss.RenderYAML(os.Stdout, entries, false)
// Or render as CSV
// uss.RenderCSV(os.Stdout, entries, false)
// Or render as human-readable
// uss.RenderHuman(os.Stdout, entries, false)
}package main
import (
"fmt"
"os"
"strings"
"github.com/mobydeck/uss"
)
func main() {
input := `...` // ss output
entries, _ := uss.Parse(strings.NewReader(input), uss.Options{})
// Filter for TCP sockets on port 22 or 80
filtered, err := uss.Filter(entries, "netid=tcp port=22,80")
if err != nil {
fmt.Fprintf(os.Stderr, "Filter error: %v\n", err)
return
}
// Render filtered results as JSON
uss.RenderJSON(os.Stdout, filtered, true, false)
}For programmatic filtering where you build conditions dynamically, use FilterByConditions:
package main
import (
"fmt"
"os"
"strings"
"github.com/mobydeck/uss"
)
func main() {
input := `...` // ss output
entries, _ := uss.Parse(strings.NewReader(input), uss.Options{})
// Build conditions dynamically
conditions := []string{
"netid=tcp",
"state=LISTEN",
"port=22,80,443",
}
// Filter using conditions slice
filtered, err := uss.FilterByConditions(entries, conditions)
if err != nil {
fmt.Fprintf(os.Stderr, "Filter error: %v\n", err)
return
}
// Use filtered results
for _, entry := range filtered {
fmt.Printf("%s:%s (%s)\n", entry.LocalAddress, entry.LocalPort, entry.State)
}
}The Entry struct represents a single socket entry:
type Entry struct {
// Common fields
Netid string
State string
RecvQ int
SendQ int
ProcessRaw string
// Extracted metadata
Users []User
UID *int
Inode *uint64
CGroup *string
V6Only *int
FWMark *string
Sk *string
Dev *string
Peers *string
// INET-specific
LocalAddress string
LocalPort string
PeerAddress string
PeerPort string
Local string
Peer string
Interface string
// UNIX-specific
UnixType string
UnixPath string
UnixID string
UnixPeer string
UnixPeerID string
}The User struct represents a process using a socket:
type User struct {
Name string
PID int
FD int
}Control parsing behavior:
type Options struct {
Strict bool // if true, fail on malformed lines; if false, skip and continue
}- IPv4:
0.0.0.0:111,127.0.0.1:8080 - IPv4 with interface:
127.0.0.53%lo:53,0.0.0.0%incusbr0:67 - IPv6:
[::]:22,[fd42:2eff:9d69:efec::1]:53 - IPv6 with interface:
[fe80::1266:6aff:fecf:96d]%incusbr0:53 - Wildcard:
*:989,*:8090 - Special formats:
_:8090_,:*
- Filesystem paths:
/run/systemd/private,/var/run/docker.sock - Abstract sockets:
@/var/lib/incus/containers/satis/command,@5cbfc - Socket types:
u_str(stream),u_dgr(datagram),u_seq(seqpacket)
Extracts from the process field:
users:(("name",pid=N,fd=M),...)→ User listuid:<num>→ UIDino:<num>→ Inodecgroup:<path>→ CGroupv6only:<0|1>→ V6Only flagfwmark:<value>→ Firewall marksk:<value>→ Socket kernel identifierdev:<maj/min>→ Devicepeers:→ Peers marker
Run the test suite:
go test ./...Run with verbose output:
go test -v ./...MIT
Contributions are welcome! Please open an issue or submit a pull request.
Built for parsing Linux socket statistics efficiently in Go.