Signal9Signal9 — home
online
all lessons

Active Connections: Network Analysis

Intermediate35 min6 steps
What you'll learn
  • Read the full TCP connection table with ss -tanp and name the owning process behind each socket
  • Interpret connection STATES (LISTEN, SYN-SENT, ESTABLISHED, TIME-WAIT, CLOSE-WAIT) and what each implies during an incident
  • Filter by state and direction to surface the connections that matter and spot beaconing
  • Prioritise outbound ESTABLISHED links to unfamiliar external IPs, and document the table before you kill anything
  1. Why active connections, not just listening ports

    Frame the task: triage the live table of who is talking to whom right now.

    In the Process & Network Forensics lesson you learned to find what's listening — the open doors a backdoor holds with ss -tlnp — and to map a process to its sockets with lsof -i. This lesson is the other half of the picture: the live table of conversations already in progress.

    A listening port is a phone sitting on the hook waiting to ring. An active connection is a call that's actually happening — and during an incident, the calls in progress tell you what the attacker is doing right now: pulling data out, holding a command-and-control (C2) channel open, or trying to dial a server you've already blocked.

    Think of each connection as a stage in a phone call. The phone can be on-hook waiting (LISTEN), dialling but not yet answered (SYN-SENT), connected and talking (ESTABLISHED), or in the awkward moment after one side hangs up (TIME-WAIT, CLOSE-WAIT). The state tells you which stage you're looking at — and that single word reshapes how urgent the line is.

  2. The full table: ss -tanp

    List every TCP connection with its state and owning process in one view.

    ss (socket statistics, the modern replacement for netstat) gives you the whole TCP picture in one command. Where the forensics lesson used -l to show only listeners, here we drop -l and add `-a` to show all sockets — both listening and connected — so every conversation appears alongside its current state.

    sudo ss -tanp
    State       Recv-Q Send-Q   Local Address:Port      Peer Address:Port   Process
    LISTEN      0      128            0.0.0.0:22             0.0.0.0:*          users:(("sshd",pid=912,fd=3))
    LISTEN      0      511            0.0.0.0:80             0.0.0.0:*          users:(("nginx",pid=1001,fd=6))
    ESTAB       0      0           10.0.0.7:22           10.0.0.42:51188      users:(("sshd",pid=1340,fd=5))
    ESTAB       0      0           10.0.0.7:43990     203.0.113.66:443        users:(("updatecheck",pid=2207,fd=7))
    SYN-SENT    0      1           10.0.0.7:50122     203.0.113.66:8443       users:(("updatecheck",pid=2207,fd=8))
    TIME-WAIT   0      0           10.0.0.7:43002     203.0.113.66:443
    CLOSE-WAIT  1      0           10.0.0.7:43512        10.0.0.9:6379      users:(("redis-cli",pid=2290,fd=4))
    -t
    TCP sockets only — where interactive sessions and C2 channels live.
    -a
    All sockets: both listening AND connected. This is what surfaces active conversations, not just open doors.
    -n
    Numeric — show port 443, not "https", and the raw IP, so nothing hides behind a friendly name.
    -p
    Show the process (name + PID) owning each socket. Needs root to see other users' sockets.
    Read it as a table of phone calls. The first two LISTEN lines are sshd and nginx waiting for inbound calls — normal. The ESTAB on port 22 from 10.0.0.42 is your own SSH session. But updatecheck (pid 2207) is the outlier: it holds an ESTAB link out to 203.0.113.66:443, has a SYN-SENT to the same host on 8443, and there's a TIME-WAIT to it too. One process, repeatedly reaching the same external IP — that pattern is the rest of this lesson.

    Check yourself

    Compared to ss -tlnp from the forensics lesson, which flag makes ss show the listening sockets AND the connected ones together, in one table?

  3. Reading the states: the phases of the call

    Map each TCP state to what it implies, and which direction the connection runs.

    The State column is the most information-dense field in the table. Each value is a stage in the life of a TCP connection — the phone-call phases:

    • LISTEN — phone on the hook, waiting to ring. A server ready for inbound connections.
    • SYN-SENT — you've dialled and you're waiting for the other end to pick up. The connection is attempted but not yet established. A SYN-SENT to an external IP means something on your box is trying to reach out — and if it stays stuck in SYN-SENT, a firewall may be blocking it (the call isn't being answered).
    • ESTABLISHED (shown ESTAB) — connected and talking. Data can flow both ways. This is the state that matters most.
    • TIME-WAIT — you hung up; the kernel holds the line closed briefly to catch stray packets. A recently finished connection.
    • CLOSE-WAIT — the other end hung up but your local process hasn't closed its side yet. Often a bug (leaked socket); occasionally an implant that lost its server.

    Direction is read off the Local vs Peer columns: Local 10.0.0.7:43990 Peer 203.0.113.66:443 with a high random local port and a well-known remote port (443) is outbound — your box dialled out. The reverse (well-known local port, random remote port) is inbound — someone dialled you.

    Check yourself

    A connection sits in SYN-SENT to an external IP and never reaches ESTABLISHED. What is the most likely explanation, and why does it still matter?

  4. Filter to what matters: by state and direction

    Narrow the table to established conversations and surface repeated outbound contact.

    On a busy host ss -tanp can scroll for pages. ss filters server-side with a `state` keyword so you only render the phase you care about. During triage you almost always want the calls that are actually connected:

    sudo ss -tnp state established
    Recv-Q Send-Q   Local Address:Port      Peer Address:Port   Process
    0      0      10.0.0.7:22           10.0.0.42:51188      users:(("sshd",pid=1340,fd=5))
    0      0      10.0.0.7:43990     203.0.113.66:443        users:(("updatecheck",pid=2207,fd=7))
    state established
    Server-side filter: show only sockets in the ESTABLISHED state. (LISTEN rows vanish, so the column header drops the State field.)
    -t / -n / -p
    Same as before: TCP only, numeric addresses/ports, owning process.
    Two established calls remain. 10.0.0.42 on the local port 22 is an inbound SSH session to you — internal, expected. The second is updatecheck (pid 2207) talking out to 203.0.113.66:443: random high local port, well-known remote port, peer on a different network. That's an outbound link to an external host — the line to scrutinise.

    Beaconing — an implant phoning home on a schedule — shows up as the same Peer Address:Port appearing again and again across short-lived connections: a flicker of ESTAB, then TIME-WAIT, then a fresh SYN-SENT, repeating. You can count contacts to each remote endpoint to make the pattern jump out:

    sudo ss -tan | awk 'NR>1 {sub(/:[^:]*$/, "", $5); print $5}' | sort | uniq -c | sort -rn
         17 203.0.113.66
          2 0.0.0.0
          1 10.0.0.42
    Strip the :port off each peer address, then count. (The sub() removes only the LAST colon and what follows, so an IPv6 peer like [fdaa::2]:22 stays whole — cut -d: -f1 would chop it to [fdaa. The 0.0.0.0 rows are your two LISTEN sockets, whose peer is *: not a remote host.) 17 separate sockets to 203.0.113.66 versus 1 to your admin host is not normal background traffic — that repetition to a single external IP is the fingerprint of beaconing. Volume to one endpoint is the signal; a human-driven session looks nothing like it.

    Check yourself

    You see 17 short-lived connections to one external IP and 1 to everything else. What behaviour does that repetition most likely indicate?

  5. Prioritise, then document before you kill

    Rank connections by risk and capture evidence before destroying it.

    Not all connections deserve equal attention. Triage them in this order:

    1. ESTABLISHED outbound to an unfamiliar EXTERNAL IP — top priority. This is an active channel to someone you don't recognise: live data exfiltration or C2.

    2. SYN-SENT / repeated short contacts to an external IP — blocked or beaconing; high priority even though no data may be flowing yet.

    3. ESTABLISHED to internal/known hosts (your admin's SSH, a database peer) — context, usually benign.

    4. localhost (`127.0.0.1`) connections — lowest priority; traffic that never leaves the machine can't be exfiltration.

    The external/internal split is the axis that matters most: a conversation that crosses your network boundary to an unknown party is where the risk lives.

    Example — not runnable on your lab box

    sudo ss -tanp > /evidence/connections-$(date +%Y%m%dT%H%M%S).txt
    Silent success: the entire connection table — states, peer IPs, owning PIDs — is now frozen in a timestamped file before you touch anything. The timestamp ties the snapshot to your incident timeline. Only after this do you move on to containment (blocking the IP, then killing the process). This single habit separates an investigation from a clean-up that destroyed its own evidence.

    Check yourself

    Why must you capture ss -tanp output to a file BEFORE killing a suspicious process?

  6. Try it in your live terminal

    Run the triage workflow yourself on the real sandbox to your right.

    You have a real root shell. Run these in order — the exact sequence you'd use on a live "is this box talking to someone it shouldn't be?" call. There may be nothing malicious here; the goal is to make the moves reflex. Notice that you document before you would ever kill.

    sudo ss -tanp
    The full picture: every TCP socket, its state, and the owning process. Scan the State and Peer columns for anything outbound to an address you don't recognise.
    sudo ss -tnp state established
    Narrow to live conversations. For each: is the peer internal/known or external/unfamiliar? External + established is your top candidate.
    sudo ss -tan | awk 'NR>1 {sub(/:[^:]*$/, "", $5); print $5}' | sort | uniq -c | sort -rn | head
    Count connections per peer IP. A high count to a single external address is the beaconing fingerprint — chase it.
    sudo ss -tanp > /tmp/connections-$(date +%Y%m%dT%H%M%S).txt; ls -l /tmp/connections-*.txt
    Freeze the evidence with a timestamp BEFORE any containment. This is the habit that keeps an investigation defensible — capture the table while it's still live.
Recap
  • Listening ports show capability; active connections show activity. During an incident, the established table tells you what the attacker is doing right now.
  • The State column is the phases of a phone call — LISTEN, SYN-SENT, ESTABLISHED, TIME-WAIT, CLOSE-WAIT — and each reframes how urgent a line is.
  • Prioritise ESTABLISHED outbound to unfamiliar EXTERNAL IPs over internal and localhost traffic; repeated short contacts to one IP mean beaconing.
  • Document the full ss -tanp table to a timestamped file BEFORE you kill anything — killing destroys the peer IP, the state, and the PID mapping you need.
State → meaning → triage priority
StatePhone-call phaseWhat it impliesPriority
LISTENOn the hook, waitingA service ready for inbound connectionsLow (cross-ref ss -tlnp)
SYN-SENTDialling, no answer yetOutbound attempt; if stuck to an external IP, likely blocked beaconHigh
ESTABLISHEDConnected, talkingActive link; outbound to unknown external IP = live C2/exfilTop (external) / Low (localhost)
TIME-WAITYou hung up, line coolingA connection your side just closedContext
CLOSE-WAITThey hung up, you haven'tRemote closed; leaked socket or implant that lost its serverInvestigate if unexpected