Show HN: Netstat for TLS Traffic

yuedongze1 pts0 comments

GitHub - yuedongze/tlstat: Netstat for TLS · GitHub

/" data-turbo-transient="true" />

Skip to content

Search or jump to...

Search code, repositories, users, issues, pull requests...

-->

Search

Clear

Search syntax tips

Provide feedback

--><br>We read every piece of feedback, and take your input very seriously.

Include my email address so I can be contacted

Cancel

Submit feedback

Saved searches

Use saved searches to filter your results more quickly

-->

Name

Query

To see all available qualifiers, see our documentation.

Cancel

Create saved search

Sign in

/;ref_cta:Sign up;ref_loc:header logged out"}"<br>Sign up

Appearance settings

Resetting focus

You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.

Dismiss alert

{{ message }}

yuedongze

tlstat

Public

Notifications<br>You must be signed in to change notification settings

Fork

Star

main

BranchesTags

Go to file

CodeOpen more actions menu

Folders and files<br>NameNameLast commit message<br>Last commit date<br>Latest commit

History<br>6 Commits<br>6 Commits

bpf

bpf

images

images

internal

internal

.gitignore

.gitignore

Makefile

Makefile

README.md

README.md

design-doc.md

design-doc.md

go.mod

go.mod

go.sum

go.sum

main.go

main.go

View all files

Repository files navigation

tlstat

A live, htop-style terminal monitor for TLS handshakes and connections on a<br>Linux host, built with eBPF. It discovers TLS sessions (new and pre-existing),<br>parses the handshake for endpoints, server identity, and negotiated crypto,<br>counts bytes in/out live, and — for OpenSSL applications — captures the<br>cleartext before encryption , the one vantage point where plaintext exists.

tlstat — 6 connections, 3 TLS sort:recent<br>PID COMM REMOTE / SNI VER CIPHER WIRE ↑/↓ PLAIN ↑/↓ ST<br>423491 openssl example.com TLS 1.3 TLS_AES_256_GCM_SHA384 1.6K/6.4K 37B/874B TLS<br>...<br>process openssl pid=423491<br>flow 10.0.0.5:51234 → 93.184.x.x:443 outbound (client)<br>server example.com<br>version TLS 1.3<br>cipher TLS_AES_256_GCM_SHA384<br>symmetric AES-256-GCM<br>key exch x25519<br>signature offered: ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256<br>─────────────────────────────────────────────────────────<br>plaintext SSL_write → (cleartext sent)<br>0000 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..|

How it works

Two eBPF attachment strategies, fused into one connection table:

Wire observation — kprobes on tcp_sendmsg/tcp_recvmsg count bytes and<br>capture the first chunks of each direction; a tracepoint on<br>inet_sock_set_state tracks connection lifecycle. The captured bytes are<br>reassembled and parsed in userspace to recover the cleartext ClientHello<br>(SNI, offered ciphers, signature algorithms, ALPN) and ServerHello<br>(negotiated version, cipher, key-exchange group). On TLS 1.2 the<br>Certificate message is also cleartext, so the server cert (CN, SANs,<br>signature algorithm) is extracted too.

Library observation — uprobes on SSL_write/SSL_read in libssl.so<br>capture the plaintext an application hands to (or receives from) OpenSSL.<br>This is the only way to see decrypted content; it works for dynamically<br>linked OpenSSL programs and attaches to already-running processes.

Everything is keyed by the kernel struct sock *; plaintext is correlated back<br>to a connection via SSL_set_fd and /proc//fd → /proc/net/tcp.

Build & run

Requires a Linux kernel with BTF (/sys/kernel/btf/vmlinux), ~5.8+.

make deps # clang, llvm, libelf-dev, libbpf-dev (Debian/Ubuntu)<br>make build # compile eBPF (CO-RE) + Go binary<br>sudo ./tlstat # must be root: eBPF needs CAP_BPF/CAP_SYS_ADMIN

Keys: ↑/↓ select · enter peek plaintext · s cycle sort · q quit.

Flags: --libssl PATH (override the OpenSSL library), --interval DUR (poll<br>rate), --dump DUR (headless text mode, e.g. --dump 10s, for scripting/CI).

What it can and can't see

TLS 1.3 hides the server certificate on the wire. SNI (always cleartext)<br>is used as the primary server identity; the full certificate is recovered<br>only on TLS 1.2. TLS 1.3 rows show cert: encrypted.

Cleartext capture is OpenSSL-only in this version. GnuTLS, NSS, BoringSSL,<br>and Go's in-binary crypto/tls are not yet instrumented.

Pre-existing connections (established before tlstat started) missed their<br>handshake, so crypto/SNI show as unknown / pre-existing. Byte counts still<br>work, and plaintext works from the moment the uprobe attaches.

Handshake reassembly is best-effort from the first ~16 KB per direction;<br>fields past a truncation point may be unknown.

Encrypted ClientHello (ECH) would hide SNI — rare today.

Layout

bpf/tlstat.bpf.c eBPF programs (kprobes, tracepoint, uprobes)<br>bpf/tlstat.h structs shared with Go<br>internal/loader/ eBPF load/attach + ring buffer + map snapshots<br>internal/tlsparse/ TLS record/handshake parser + IANA name tables<br>internal/model/ connection table, correlation, plaintext join<br>internal/ui/ bubbletea...

tlstat openssl plaintext internal ebpf cleartext

Related Articles