Skip to content

Latest commit

 

History

History
601 lines (449 loc) · 13.2 KB

File metadata and controls

601 lines (449 loc) · 13.2 KB

uss - Unix Socket Statistics Parser

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.

Features

  • Parse ss output: 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

Installation

From Source

go install github.com/mobydeck/uss/cmd/uss@latest

Build Locally

git clone https://github.com/mobydeck/uss
cd uss
go build -o uss ./cmd/uss

CLI Usage

Basic Usage

By default, uss reads from stdin and outputs human-readable format:

ss -tunap | uss

Executing ss Directly

Use 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 -tnl

The --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.

Output Formats

Human-readable (default):

ss -tunap | uss

JSON:

ss -tunap | uss --json

Pretty JSON:

ss -tunap | uss --json --pretty

YAML:

ss -peanutlx | uss --yaml

CSV:

ss -tunap | uss --csv > sockets.csv

Reading from File

You can also read from a file instead of stdin:

ss -tunap > sockets.txt
uss --json sockets.txt

Raw Fields

By default, uss hides raw/unparsed fields for cleaner output. These include:

  • processRaw - The unparsed process metadata string
  • local - 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 --raw

This is useful when you need to see the original unparsed data or for debugging parsing issues.

Filtering Results

Filter socket entries using the -q or --query flag:

Query Syntax

  • Format: field=value1,value2[,valueN]
  • Multiple values per field: comma-separated (OR condition)
  • Multiple fields: space or semicolon separated (AND condition)

Examples

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 --pretty

Combined 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.csv

Supported Fields

Common:

  • netid - Socket type (tcp, udp, u_str, etc.)
  • state - Socket state (LISTEN, ESTAB, UNCONN, etc.)
  • recvq - Receive queue size
  • sendq - 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 port
  • peerport - Peer port
  • localaddress - Local address
  • peeraddress - Peer address
  • interface - Network interface

UNIX-specific:

  • unixtype - Unix socket type (u_str, u_dgr, u_seq)
  • unixpath - Unix socket path (supports wildcards/substring)
  • unixid - Unix socket ID
  • unixpeer - Unix peer socket
  • unixpeerid - Unix peer socket ID

Metadata:

  • uid - User ID (numeric)
  • inode - Inode number (numeric)
  • cgroup - Control group path
  • v6only - IPv6-only flag (0 or 1)
  • fwmark - Firewall mark
  • sk - Socket kernel identifier
  • dev - Device identifier

Matching Rules

  • Exact match: Default for most values (port=22)
  • Wildcard: Use * (any sequence) or ? (single char) (state=LISTEN*)
  • Range: Use min-max for numeric fields (port=1000-2000)
  • Substring: For string fields, partial matches work (unixpath=docker)
  • Case-insensitive: All string matching is case-insensitive (state=listen matches LISTEN)

Examples

Parse INET sockets:

ss -tunap | uss

Parse UNIX sockets:

ss -peanutlx | uss

Generate CSV report:

ss -tunap | uss --csv > sockets.csv

Using jq for Advanced Filtering

You 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'

Library Usage

Import

import "github.com/mobydeck/uss"

Basic Example

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)
    }
}

Rendering Output

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)
}

Filtering Results

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)
}

Filtering with Conditions Slice

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)
    }
}

Data Structures

Entry

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
}

User

The User struct represents a process using a socket:

type User struct {
    Name string
    PID  int
    FD   int
}

Options

Control parsing behavior:

type Options struct {
    Strict bool // if true, fail on malformed lines; if false, skip and continue
}

Supported Formats

INET Sockets

  • 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_, :*

UNIX Sockets

  • 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)

Process Metadata

Extracts from the process field:

  • users:(("name",pid=N,fd=M),...) → User list
  • uid:<num> → UID
  • ino:<num> → Inode
  • cgroup:<path> → CGroup
  • v6only:<0|1> → V6Only flag
  • fwmark:<value> → Firewall mark
  • sk:<value> → Socket kernel identifier
  • dev:<maj/min> → Device
  • peers: → Peers marker

Testing

Run the test suite:

go test ./...

Run with verbose output:

go test -v ./...

License

MIT

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Author

Built for parsing Linux socket statistics efficiently in Go.