Operations 8 min read

How to Use tcpw with eBPF to Capture Curl’s Five‑Tuple Information

This article introduces tcpw, a small eBPF‑based utility that traces TCP, UDP, and Unix‑domain sockets to display the five‑tuple of commands like curl or telnet, explains its command‑line options, shows concrete usage examples, and details the underlying BPF and Go implementation, including connect, accept, and fork tracing.

Linux Kernel Journey
Linux Kernel Journey
Linux Kernel Journey
How to Use tcpw with eBPF to Capture Curl’s Five‑Tuple Information

When troubleshooting tools such as curl or telnet, obtaining the socket five‑tuple (source IP, source port, destination IP, destination port, protocol) is not directly supported by standard utilities. The tiny program tcpw uses eBPF to capture socket‑level information and print the five‑tuple of the target process.

Usage

Running ./tcpw -h prints the help message:

Usage: tcpw [options] <command> args...
Options:
  --udp, -U       Trace UDP sockets
  --unix, -X      Trace Unix domain sockets
  --help, -h      Print this help message

By default tcpw traces TCP sockets. The --udp (or -U) flag enables UDP tracing, and --unix (or -X) enables tracing of Unix‑domain sockets.

TCP example

$ ./tcpw curl https://google.com
2024/12/21 14:42:15 tcpw: pid=97849 comm=curl af=AF_INET proto=TCP 192.168.241.133:46182 -> 142.251.10.101:443
<HTML>...</HTML>

UDP example

$ ./tcpw -U nslookup google.com
2024/12/21 14:44:36 tcpw: pid=98464 comm=isc-net-0000 af=AF_INET proto=UDP 127.0.0.1:37324 -- 127.0.0.53:53
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
...

Unix‑domain socket example

$ ./tcpw -X ../sockdump/sockdump-example
2024/12/21 14:45:24 serving
2024/12/21 14:45:24 tcpw: pid=98505 comm=sockdump-exampl af=AF_UNIX proto=UNIX-STREAM path=/tmp/uskdump.sock
2024/12/21 14:45:24 got response
...
tcpw

also supports IPv6 sockets and can trace the traffic direction.

Implementation details

The core uses eBPF fexit probes on the kernel connect and accept functions. The BPF snippets are:

SEC("fexit/connect")
int BPF_PROG(fexit_connect, struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags, int retval) {
    return trace_sock(sock, true);
}

SEC("fexit/accept")
int BPF_PROG(fexit_accept, struct socket *sock, struct socket *newsock, struct proto_accept_arg *arg, int retval) {
    return trace_sock(newsock, false);
}

Instead of hard‑coding the exact signatures, the Go side inspects kernel BTF information to locate the connect and accept functions. Signature checks are performed with helper functions such as:

func isConnectFunc(fn *btf.Func) bool {
    fnProto := fn.Type.(*btf.FuncProto)
    if len(fnProto.Params) != 4 { return false }
    return mybtf.IsStructPointer(fnProto.Params[0].Type, "socket") &&
           mybtf.IsStructPointer(fnProto.Params[1].Type, "sockaddr") &&
           isInt(fnProto.Params[2].Type) &&
           isInt(fnProto.Params[3].Type)
}

func isAcceptFunc(fn *btf.Func) bool {
    fnProto := fn.Type.(*btf.FuncProto)
    if len(fnProto.Params) != 4 { return false }
    return mybtf.IsStructPointer(fnProto.Params[0].Type, "socket") &&
           mybtf.IsStructPointer(fnProto.Params[1].Type, "socket") &&
           isInt(fnProto.Params[2].Type) &&
           isBool(fnProto.Params[3].Type)
}

When a matching function is found, the BPF program is attached, enabling capture of socket parameters at function exit.

Tracing the fork system call

To record child PIDs when tcpw launches commands (e.g., curl or telnet), the tracepoint sched_process_fork is used because it provides both parent and child PIDs.

SEC("tp/sched/sched_process_fork")
int tp_sched_process_fork(struct trace_event_raw_sched_process_fork *ctx) {
    __u32 parent_pid = ctx->parent_pid;
    __u32 child_pid  = ctx->child_pid;
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    if (bpf_map_lookup_elem(&tcpw_pids, &pid)) {
        bpf_map_update_elem(&tcpw_pids, &parent_pid, &pid, BPF_ANY);
        bpf_map_update_elem(&tcpw_pids, &child_pid, &parent_pid, BPF_ANY);
    }
    return BPF_OK;
}

The tracepoint fields parent_pid and child_pid are not guaranteed to be the current PID, so bpf_get_current_pid_tgid() is used to obtain the accurate PID. Before attaching the tracepoint, the tool inserts its own PID into the map so that all processes spawned under its tree are traced:

pid := os.Getpid()
err = pids.Put(uint32(pid), uint32(pid))
assert.NoErr(err, "Failed to put pid: %v")

Consequently, a command such as the following traces every child curl invocation under the same process tree:

./tcpw bash -c "for i in {1..10}; do curl -s https://google.com -o /dev/null; echo done $i; done"

Summary

tcpw

is a lightweight utility that prints the five‑tuple of network connections made by tools like curl and telnet. It also serves as a concrete example for learning eBPF, demonstrating how to trace connect, accept, and fork calls.

Source code:

https://github.com/Asphaltt/tcpw
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoeBPFacceptconnectforkLinux tracingsocket five-tupletcpw
Linux Kernel Journey
Written by

Linux Kernel Journey

Linux Kernel Journey

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.