graftcp: redirecting a Linux process's TCP, UDP and DNS traffic through SOCKS5 or HTTP proxies
A flexible tool for redirecting a program's TCP, UDP, and DNS traffic to SOCKS5 or HTTP proxies.
At a glance
- What is it?
- graftcp traces socket syscalls with ptrace(2) instead of using LD_PRELOAD, so statically linked binaries can be proxied too. Here is how the token-IP mechanism works, how to build it, and where it stops being the right tool.
- Who is it for?
- Adopt graftcp when you need to force a statically linked binary, a Go program, or a whole shell session through a SOCKS5 or HTTP proxy on Linux and LD_PRELOAD-based tools cannot see the connections. Do not adopt it on non-Linux hosts, in containers where ptrace is blocked, or when you need a system-wide transparent redirect rather than a per-process one.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem graftcp solves: proxying binaries that ignore LD_PRELOAD
Most Unix proxy wrappers work by injecting a shared library that overrides the socket functions. That approach breaks the moment the target binary is statically linked. Go programs are the common example, since the Go toolchain links the runtime statically by default and the dynamic loader never gets a chance to interpose anything. The README states plainly that graftcp does not depend on LD_PRELOAD, so statically linked binaries such as most Go programs can be traced as well. That single property is the reason the project exists.
The audience is narrow and specific: engineers on Linux who need one command, or one shell session, to reach the network through a SOCKS5 or HTTP proxy, and who have already been defeated by proxychains or tsocks on a particular binary. It is a debugging and workflow tool rather than infrastructure. You run it in front of a command, and the command's outbound connections take a different path.
How the ptrace(2) interception and loopback token IPs fit together
The mechanism is more interesting than the usual wrapper. graftcp starts an embedded local TCP listener, then traces the target command with ptrace(2). For every intercepted connect(2), it records the original destination in an in-process route table and allocates a unique loopback token IP from 127.0.0.0/8. The tracee's destination sockaddr is rewritten to that token IP plus the embedded listener port.
When the listener accepts the connection, it reads the token from LocalAddr(), looks up the original destination in the route table, and dials the configured SOCKS5, HTTP, or direct path. After the syscall returns, graftcp restores the tracee's original sockaddr buffer on a best-effort basis. The word best-effort is doing real work there: the restore is not guaranteed, which matters for programs that inspect their own sockaddr buffers after connect.
For IPv6 connect(2), the destination is rewritten to an IPv4-mapped loopback address (::ffff:127.x.y.z) so the same token registry can be reused. The README notes that the old graftcp plus graftcp-local split has been merged into a single command, and that there is no standalone local daemon, no FIFO, no /proc scan, and no netlink socket reverse lookup. The embedded proxy resolves the token in-process, which removes a whole class of race conditions that the older architecture had to manage.
DNS and UDP take separate paths. When DNS proxying is enabled, graftcp starts an embedded UDP DNS listener, rewrites UDP connect() and sendto() calls to port 53 toward that listener, and forwards each DNS payload to the configured upstream over TCP through the same proxy selection path. DNS sockaddr rewrites are intentionally left in place after the syscall returns, because resolvers such as musl validate replies against the send-side sockaddr buffer. Generic UDP proxying is a third listener: connect(), sendto() and sendmsg() targets are rewritten to loopback token endpoints, and the listener forwards packets through SOCKS5 UDP ASSOCIATE when SOCKS5 is selected, or direct UDP in direct mode and fallback cases.
Building graftcp from source and running your first proxied command
graftcp runs on Linux only. The Makefile enforces this at parse time: if uname -s is not Linux, the build stops with the message "only support Linux now." Building requires Go and a C toolchain, because the tree mixes C sources (graftcp.c, ptrace-ops.c, cidr-trie.c, util.c) with a Go component under local/.
Clone and build:
git clone https://github.com/hmgle/graftcp.git
cd graftcp
makeThe build output is local/graftcp, and local/mgraftcp is created as a compatibility alias. Installing copies the binaries into place under /usr/local/bin by default:
sudo make installThe first real use is to put a single command behind a local SOCKS5 proxy. The README gives this exact example:
./local/graftcp --socks5 127.0.0.1:1080 curl https://example.comIf the proxy is a Unix socket rather than a TCP port, the address takes a unix: prefix, and you can pin the selection mode so only SOCKS5 is considered:
./local/graftcp --select_proxy_mode only_socks5 --socks5 unix:/path/tor.sock curl https://example.comDNS is off by default. To route UDP/53 queries through the DNS-over-TCP forwarder, enable it and name the upstream:
./local/graftcp --enable-dns --dns-server 1.1.1.1:53 curl https://example.comFor an interactive session, the README shows running a shell with a modified prompt, which makes it obvious which terminal is proxied:
./local/graftcp bash --rcfile <(echo 'PS1="(graftcp) $PS1"')Configuration can also live in a file. graftcp searches a fixed order of locations, starting with the path passed to --config, then $(dirname $0)/graftcp.conf, then the XDG and HOME config directories, and finally /etc/graftcp/graftcp.conf. Config keys mirror the flags, and CLI flags override config values:
dns_proxy = true
udp_proxy = true
ignore_local = falseThose three keys correspond to --enable-dns, --enable-udp, and --not-ignore-local respectively.
ptrace permissions, seccomp-BPF and the overhead you inherit
The first constraint is ptrace(2) itself. The README is direct about it: ptrace permissions still apply, and if tracing is blocked you should check Yama ptrace_scope, capabilities, or run as root when appropriate. On a hardened host or inside a container with a restrictive seccomp profile, the trace may simply never attach, and no amount of flag tuning will help.
The second constraint is syscall interception cost. On supported kernels graftcp installs a seccomp-BPF filter so only socket-related syscalls trap into the tracer, which reduces tracing overhead. On kernels without seccomp filtering it falls back to stopping on every syscall. This is automatic and needs no configuration, which is convenient, but it also means performance depends on the host kernel in a way you cannot see from the command line. If you are proxying a syscall-heavy workload on an old kernel, the fallback path is the one you get.
The third constraint is scope. Local destinations are ignored by default, so loopback and private-local connects bypass the proxy unless you pass --not-ignore-local. That default is sensible for the common case, and surprising for anyone who expected everything to be redirected. There is also the best-effort sockaddr restore mentioned earlier, and the deliberate exception for DNS, where rewrites are left in place. These are deliberate trade-offs, not oversights, but they are the kind of detail that only surfaces when a program behaves oddly under the tracer.
Where graftcp is the wrong tool
graftcp is not a system-wide transparent proxy. It wraps one process tree. If you need every connection from a machine or a container network to leave through a proxy without changing how each command is invoked, this is the wrong layer, and you want routing or a network namespace instead.
It is also not portable. The Makefile refuses to build anywhere but Linux, and the entire design rests on ptrace(2) and Linux syscall semantics. macOS and BSD users have no path here.
Finally, it is not a proxy server. graftcp consumes a SOCKS5 or HTTP proxy; it does not provide one. If you were hoping to run it standalone and have it accept client connections, you need to look elsewhere. The embedded listeners exist only to service the tracee, and the README describes no inbound service.
How graftcp differs from proxychains and tsocks
The README names the comparison directly: tsocks, proxychains, and proxychains-ng. The difference is the interception mechanism. proxychains and tsocks use LD_PRELOAD to replace the libc socket functions in the process. graftcp uses ptrace(2) to stop the process at syscall boundaries and rewrite the sockaddr in memory.
That distinction has a concrete consequence. A statically linked binary has no dynamic symbol table for the loader to interpose on, so LD_PRELOAD-based tools cannot see its connections. graftcp can, because it operates below libc. The same property means graftcp does not care which language runtime the target uses, only that it makes Linux socket syscalls.
The cost is that graftcp needs permission to trace, pays a syscall-stop cost, and lives with the ptrace attachment model. proxychains has none of those requirements and is available on more platforms. If your target is dynamically linked and your platform is not Linux, proxychains is the simpler answer. If your target is a static Go binary, graftcp is the one that will actually work.
Maintenance status, licence and upgrade cost
The repository is not archived, and the last push was on 2026-08-25. Recent releases are v0.8.1 on 2026-06-07, v0.8.2 on 2026-07-01, and v0.8.3 on 2026-07-15, so the project has shipped three tagged releases in the months before that push.
Upgrading has a specific cost worth understanding. The old graftcp plus graftcp-local split runtime has been merged into a single command. graftcp is now the primary entrypoint, and mgraftcp remains available as a compatibility alias. If your scripts or documentation reference the old two-process layout, the local daemon, the FIFO, or the /proc scan, those references are stale. The README also notes that example-mgraftcp.conf is kept for compatibility with the alias name, and that the config search order still includes mgraftcp paths, so existing configuration files in those locations continue to be found.
graftcp is licensed under GPL-3.0, and the Makefile carries the standard GPL header. The practical point for adopters is that this is a copyleft licence, not a permissive one. If you are considering linking graftcp's code into a product rather than invoking the binary, read COPYING and get your own legal advice. Running the binary as a separate process is a different question from incorporating its source, and the licence text is the authority, not this article.
Editorial conclusion
Adopt graftcp when you need to force a statically linked binary, a Go program, or a whole shell session through a SOCKS5 or HTTP proxy on Linux and LD_PRELOAD-based tools cannot see the connections. Do not adopt it on non-Linux hosts, in containers where ptrace is blocked, or when you need a system-wide transparent redirect rather than a per-process one. Before rolling it out, verify that your kernel allows ptrace on the target process (check Yama ptrace_scope) and confirm whether seccomp-BPF filtering is available, since without it graftcp stops on every syscall instead of only socket-related ones.
Frequently asked questions
Does graftcp work on macOS or Windows?
No. The Makefile checks uname -s and aborts the build with "only support Linux now." if the kernel is not Linux. The design depends on ptrace(2) and Linux socket syscall behavior throughout.
Why does graftcp work on Go binaries when proxychains does not?
Because graftcp does not depend on LD_PRELOAD. It traces socket syscalls with ptrace(2) instead of interposing on libc functions, so statically linked binaries such as most Go programs can be traced as well.
Is DNS proxying enabled by default in graftcp?
No, DNS proxying is disabled by default. Pass --enable-dns to enable the UDP/53 DNS-over-TCP path, and use --dns-server to choose the upstream resolver, for example --dns-server 1.1.1.1:53.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hmgle-graftcp)