Proxychains Configuration: Force Any Linux Tool via Proxy
Proxychains configuration for proxychains-ng 4.17: strict vs dynamic chain, proxy_dns, auth lines, Go and static binary workarounds, and a DNS leak test.
A working proxychains configuration comes down to five lines, but three behaviours catch nearly everyone: the first proxy entry must be a numeric IP, a mistyped -f path silently loads a different config, and a Go program can print the proxychains banner and still connect direct. This guide builds a per-job config for proxychains-ng 4.17 against a SparkProxy SOCKS5 endpoint, runs single commands through it, handles the static-binary and Go cases where LD_PRELOAD does nothing, and proves there's no IP or DNS leak. Every log line quoted below was produced on Ubuntu 24.04 on 2026-10-30, unless it is marked as coming from the project's source.
What proxychains-ng Hooks, and What It Can't Reach
proxychains-ng is a shared library plus a small launcher. The launcher, proxychains4, sets LD_PRELOAD to libproxychains.so.4 and then executes your command. The dynamic linker loads that library first, so when the program calls connect(), it lands in proxychains code, which opens a connection to your proxy instead and asks the proxy to reach the real destination.
Reading src/libproxychains.c on the master branch, the hooked functions are connect, __xnet_connect, gethostbyname, getaddrinfo, freeaddrinfo, getnameinfo, gethostbyaddr, sendto (only to handle MSG_FASTOPEN), close and close_range. Everything else in libc passes through untouched. That list explains every limitation you'll hit:
| Situation | Hooked? | Result |
|---|---|---|
| Dynamically linked C/C++ tool using libc `connect()` (curl, wget, ssh, git) | Yes | Proxied |
| TCP connection from any hooked program | Yes | Proxied |
| UDP traffic, including QUIC and plain DNS queries over UDP | No | Goes direct. The README states it "supports TCP only (no UDP/ICMP etc)" |
| Name lookup through `gethostbyname2()` or `res_query()` | No | Resolved locally, a DNS leak |
| Statically linked binary | No | The library is never loaded |
| Go program, even a dynamically linked one | Library loads, but Go doesn't call libc `connect()` | Goes direct |
If you need the concept of chaining several hops rather than forcing one tool through one proxy, the explainer on proxy chaining covers latency and trust trade-offs. This guide stays on the single-proxy job.
Install proxychains-ng and Confirm the Version
On Debian and Ubuntu the package is proxychains4. Ubuntu 24.04's candidate is 4.17-1, which matches the latest upstream release.
sudo apt-get install -y proxychains4 # Debian/Ubuntu package name
proxychains4 true 2>&1 | grep 'DLL init' # prints: [proxychains] DLL init: proxychains-ng 4.17
Is it maintained? On 2026-10-30 we read the repository at github.com/rofl0r/proxychains-ng directly. The newest tag is v4.17, created 2024-01-21. The default branch is still moving: the latest commit, "fix endian-specific read of SOCKS5 domain length", is dated 2026-10-06, and four more fixes landed on 2026-08-27 (including "fix proxy_getaddrinfo crash when both node and hints are NULL"). So releases are infrequent but the code is actively fixed. If one of those post-4.17 fixes matters to you, build from the repo with ./configure --prefix=/usr --sysconfdir=/etc && make, as the README describes.
On other distributions, check the package name before installing: the binary you want is proxychains4, and the version line above is how you confirm you have the ng branch rather than the original 3.x proxychains.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Write a Per-Job proxychains Configuration File
Don't edit /etc/proxychains4.conf for one job. The README lists the lookup order: a path from -f or the PROXYCHAINS_CONF_FILE variable, then ./proxychains.conf, then $HOME/.proxychains/proxychains.conf, then the system file. A per-job file keeps the default config (which on Ubuntu points at Tor on 127.0.0.1:9050) out of the picture.
Trap one: a typo in -f is not an error. The function get_config_path() in src/common.c checks each candidate and moves on if it doesn't exist. We typed dsn.conf instead of dns.conf and got this:
$ proxychains4 -f dsn.conf curl -s https://www.sparkproxy.io/
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] Strict chain ... 127.0.0.1:9050 ... timeout
Always read the config file found: line once before trusting a run.
Trap two: the first proxy must be a numeric IP. We put the SparkProxy hostname straight into [ProxyList] and proxychains exited before running curl:
proxy gateway.sparkproxy.io has invalid value or is not numeric
non-numeric ips are only allowed under the following circumstances:
chaintype == strict (true), proxy is not first in list (false), proxy_dns active (thread)
Hostnames are accepted only for the second and later entries of a strict chain with proxy_dns on, because those can be looked up remotely through the earlier hop. For a single proxy, resolve the hostname when you generate the file. This script writes a config for SparkProxy's SOCKS5 port:
#!/usr/bin/env bash
# spx-conf: write a one-proxy proxychains config for SparkProxy SOCKS5
set -euo pipefail
GW_IP=$(getent ahostsv4 gateway.sparkproxy.io | awk '{print $1; exit}')
CONF="${HOME}/.proxychains/spark-socks5.conf"
mkdir -p "$(dirname "$CONF")"
umask 077 # the file holds a password
cat > "$CONF" <<EOF
strict_chain
proxy_dns
remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
socks5 ${GW_IP} 13000 ${SPARK_USER} ${SPARK_PASS}
EOF
echo "wrote $CONF using ${GW_IP}"
Re-run it from cron or before each job if the gateway's address ever changes. The umask 077 matters: the credentials sit in plain text.
Auth lines. The format is type ip port [user pass], separated by spaces or tabs. The shipped config comment says the supported auth types are "basic"-http and "user/pass"-socks. Both forms are shown below. There's also a URL form, socks5://user:pass@ip:port, added in 4.14, but the source comment above proxy_from_string() warns that "user:pass authentication is currently not implemented for http proxies" in that form, so stick with the space-separated line for HTTP.
[ProxyList]
# SOCKS5 with username/password (RFC 1929)
socks5 203.0.113.10 13000 SPARK_USER SPARK_PASS
# HTTP CONNECT with Basic auth
http 203.0.113.10 11000 SPARK_USER SPARK_PASS
# No credentials: works only if your server IP is whitelisted in the dashboard
socks5 203.0.113.10 13000
The IP addresses above are documentation placeholders; use the one your script resolved. Usernames or passwords containing spaces will break the space-separated parser, so pick credentials without them. If your server has a fixed IP, SparkProxy's IP whitelist lets you drop credentials from the file entirely.
strict_chain or dynamic_chain With One Gateway
Four chain types exist: strict_chain, dynamic_chain, round_robin_chain and random_chain. Only one should be uncommented; the shipped file warns that "otherwise the last appearing option will be accepted". With a single proxy, round_robin_chain and random_chain have nothing to choose between, so the real decision is strict versus dynamic.
The difference shows up when a hop is unreachable. With strict_chain and a dead proxy, each connection attempt fails and the tool sees a refusal:
[proxychains] Strict chain ... 127.0.0.1:1099 ... timeout
curl: (7) Failed to connect to www.sparkproxy.io port 443 after 1 ms: Couldn't connect to server
With dynamic_chain and a list of a dead entry plus a live one, the dead entry is skipped and the request succeeds:
[proxychains] Dynamic chain ... 127.0.0.1:1099 ... timeout
[proxychains] Dynamic chain ... 127.0.0.1:1080 ... 104.21.76.82:443 ... OK
For one gateway, choose strict_chain. It fails closed: if the proxy is down, the tool errors instead of quietly finding another path. dynamic_chain earns its place only when you list a backup proxy, and even then it never falls back to a direct connection; it fails once every entry is dead.
proxy_dns, the 224.x Mapping, and the Lookups It Misses
With proxy_dns on, a hooked lookup never reaches your resolver. proxychains hands the program a fake address from the remote_dns_subnet range (224.x.x.x by default) and remembers which hostname it stands for. When the program connects to that fake address, proxychains sends the hostname to the proxy, and the proxy resolves it.
We confirmed this with a logging SOCKS5 server (the script is in the leak section). With proxy_dns on, the server received a domain-name request, address type 3 in SOCKS5 terms:
CONNECT atyp=DOMAIN target=www.sparkproxy.io:443
With proxy_dns commented out, the same curl command resolved locally first, and the proxy only saw an IP:
CONNECT atyp=IPV4 target=104.21.76.82:443
The second case still reaches the target through the proxy. The leak is that your local resolver, and whoever runs it, saw every hostname. The concept of resolving at the proxy end is covered in the SOCKS5 setup guide for Python, Node.js and browsers, where the equivalent switch is the socks5h:// scheme.
The lookups it misses. Only gethostbyname, getaddrinfo and their reverse counterparts are hooked. We ran two commands under the same proxy_dns config:
$ proxychains4 -q -f l.conf getent ahostsv4 api.ipify.org
224.0.0.1 0 api.ipify.org # mapped: lookup stayed inside proxychains
$ proxychains4 -q -f l.conf getent hosts api.ipify.org
104.26.12.205 api.ipify.org # real answer: lookup went to the local resolver
getent hosts goes through a lookup path proxychains doesn't intercept, so it got a real answer from the system resolver. Tools with their own DNS client, such as dig, host and nslookup, send UDP queries directly and are never covered. If a tool you depend on resolves names that way, proxy_dns can't help it; see the static-binary section for routing that works below libc.
Two side effects of the fake range. A program that validates the address it gets back, or tries to pick a network interface for it, may reject a 224.x address. The README names newer nmap as one example and suggests disabling proxy_dns, using a numeric IP, or using nmap's own SOCKS support. Also, the shipped config notes that localnet exclusions only work for plain IPs or names from /etc/hosts when proxy_dns is on, so don't expect localnet 10.0.0.0/255.0.0.0 to catch db.internal.sparkproxy.io.
Running a Single Command Through the Proxy
The launcher takes two flags. -q silences the [proxychains] log lines, and -f names the config. Everything after is your command and its arguments:
CONF=~/.proxychains/spark-socks5.conf
proxychains4 -f "$CONF" curl -s https://api.ipify.org # check the exit IP once, with logs
proxychains4 -q -f "$CONF" wget -q https://www.sparkproxy.io/robots.txt
proxychains4 -q -f "$CONF" nc -zv sparkproxy.io 443 # netcat has no proxy flag of its own
proxychains4 -q -f "$CONF" ssh deploy@203.0.113.50 # any TCP port the proxy permits
proxychains4 -q -f "$CONF" bash # every child of this shell is proxied
OpenSSH needed a dedicated fix: the 4.17 changelog lists "add hook for close_range function, fixing newer versions of openssh", so use 4.17 or later if ssh misbehaves under an older package. The bash line is handy for an interactive session: LD_PRELOAD and PROXYCHAINS_CONF_FILE are inherited by child processes, so anything you start from that shell is proxied until you exit. A program that clears its environment before spawning helpers breaks that inheritance.
For scripts, a two-line wrapper keeps call sites short and makes the config path impossible to typo:
# ~/.bashrc
spx() { proxychains4 -q -f "$HOME/.proxychains/spark-socks5.conf" "$@"; }
# usage: spx curl -s https://api.ipify.org
Scripting languages. The README warns that glibc keeps dlopen()ed modules from seeing the hooks, which "mainly affects scripting languages such as perl or python". On our Ubuntu 24.04 box, Python 3.12's standard-library urllib was hooked without trouble: the logging server recorded the domain-name request. A C extension loaded at runtime is the risky case. Test your own interpreter with the logging server before relying on it, and prefer the library's native proxy setting when it has one.
Random Rotation, Sticky Sessions and Ports
proxychains has no idea how a provider rotates IPs; it opens one TCP connection to the proxy per outbound connection. What the exit IP does is decided by the port you point it at. SparkProxy's gateway at gateway.sparkproxy.io exposes these, all of which accepted TCP connections in our check on 2026-10-30:
| Port | Protocol | Exit IP behaviour | Use it for |
|---|---|---|---|
| 11000 | HTTP/HTTPS | Random Rotation | Stateless fetches where each connection can have a new IP |
| 11002 | HTTP/HTTPS | Sticky Sessions (holds an IP for 5 minutes) | Logins, multi-step flows, tools that open several connections to one site |
| 13000 | SOCKS5 | Not covered in this guide | Non-HTTP TCP protocols, where permitted, and the default in this guide |
Rotation interacts with how a tool uses connections. curl fetching one URL makes one connection, so rotation is invisible. A tool that logs in on one connection and downloads on another can see two different exit IPs under Random Rotation, and the site may reject the second request. Point that tool at 11002 with an http line:
[ProxyList]
http 203.0.113.10 11002 SPARK_USER SPARK_PASS
proxychains speaks HTTP CONNECT to that port, so HTTPS and other TCP traffic is tunnelled the same way.
Concurrency is where cost enters. SparkProxy plans are sold as packs "Starting from $75/month" (USD, as listed on sparkproxy.io on 2026-10-09) with unlimited bandwidth, so a proxied tool that pulls large files doesn't add traffic charges; the plan and its concurrency are the billing unit. Running many proxychains processes in parallel is fine as long as their combined connection count stays within your plan. A 24-hour free trial with no credit card is enough to check whether a stubborn tool works before you commit.
Static Binaries and Go Programs: When LD_PRELOAD Fails
First check what you're dealing with. file and ldd answer it in two seconds:
file ./tool # "statically linked" means proxychains cannot load into it
ldd ./tool # "not a dynamic executable" means the same
go version -m ./tool # prints the Go version if it's a Go binary, an error otherwise
Static binaries. There's no dynamic linker, so LD_PRELOAD is ignored and nothing is hooked. The tell is in the log: proxychains prints preloading ... but never DLL init.
Go binaries are the subtle case. We built the same short Go 1.22 program (one http.Get call) twice, once with cgo (dynamically linked per file) and once with CGO_ENABLED=0 (statically linked), and ran both under a strict config pointing at the logging server:
$ proxychains4 -f dns.conf ./gofetch https://www.sparkproxy.io/
[proxychains] DLL init: proxychains-ng 4.17
200 117951
$ proxychains4 -f dns.conf ./gofetch-static https://www.sparkproxy.io/
200 117951
The dynamic build loaded the library, printed DLL init, and still fetched the page with no chain line and no entry in the proxy log. It went direct. The reason is in Go's own source: net/hook_unix.go sets connectFunc = syscall.Connect, and on Linux syscall.Connect issues the SYS_CONNECT system call itself rather than calling libc. A banner that says the library loaded proves nothing for Go.
You have three ways to route a Go program or static binary, in order of effort:
1. Environment variables, if the program uses Go's default HTTP client. Go's net/http documentation says ProxyFromEnvironment reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY, and the Transport.Proxy docs list http, https, socks5 and socks5h schemes, with socks5 "treated the same as socks5h", meaning the hostname goes to the proxy. Our static build, which ignored proxychains, went through the proxy with a domain-name request this way:
HTTPS_PROXY="socks5://${SPARK_USER}:${SPARK_PASS}@gateway.sparkproxy.io:13000" ./gofetch-static https://www.sparkproxy.io/
This only works when the program uses http.DefaultTransport or copies its Proxy field. Raw net.Dial callers ignore the variables.
2. graftcp, which traces syscalls instead of preloading. Its README says it "redirects TCP connections from an arbitrary Linux process to SOCKS5 or HTTP proxies by tracing socket syscalls with ptrace(2)" and that "statically linked binaries such as most Go programs can be traced as well". We have not benchmarked it. The repository's newest tag is v0.8.3 (2026-07-15) and its last commit is dated 2026-08-25, both read on 2026-10-30. Note its README states "DNS proxying is disabled by default", so add --enable-dns:
GW_IP=$(getent ahostsv4 gateway.sparkproxy.io | awk '{print $1; exit}')
graftcp --socks5 "${GW_IP}:13000" \
--socks5_username "$SPARK_USER" --socks5_password "$SPARK_PASS" \
--enable-dns ./gofetch-static https://www.sparkproxy.io/
Its --http_proxy flag takes only an address, with no username field in the help text, so use the SOCKS5 flags or whitelist your IP. ptrace restrictions such as Yama's ptrace_scope can block it inside containers.
3. A TUN device for everything else. tun2socks (github.com/xjasonlyu/tun2socks, newest tag v2.7.0 from 2026-07-12, last commit 2026-10-28) describes itself as transparently routing "all network traffic from any application through a proxy" via a TUN interface, and policy routing sends a user's or a network namespace's traffic into it. That works for any binary because it operates at the IP layer, but it's a routing project, not a one-liner, and a misconfigured route can loop the proxy's own connection into the tunnel. Use it when options 1 and 2 don't fit.
Prove There Is No IP or DNS Leak
Compare exit IPs first; it catches the gross failure in one step:
curl -s https://api.ipify.org; echo # your server's own IP
spx curl -s https://api.ipify.org; echo # should be a SparkProxy IP
That proves curl is proxied. It doesn't prove your actual tool is, and it says nothing about DNS. Two checks cover the rest.
Watch port 53 while the tool runs. In a second terminal, run sudo tcpdump -ni any 'port 53'. With a hooked tool and proxy_dns on, a run should produce no queries for the target hostnames. A burst of queries means something resolved locally: a static binary, a Go program, or a lookup path like getent hosts.
Point the tool at a logging SOCKS5 stub. This is the test we used for every result in this guide. It accepts SOCKS5 with or without a password, prints how each request names its target, then relays the traffic. DOMAIN means the proxy is resolving; IPV4 means your machine already looked the name up; no line at all means the tool bypassed proxychains.
# socks_log.py: SOCKS5 stub that logs whether a client sends hostnames or IPs
import socket
import struct
import threading
def relay(src, dst):
try:
while data := src.recv(65536):
dst.sendall(data)
except OSError:
pass
finally:
src.close()
dst.close()
def handle(client):
try:
_ver, n_methods = client.recv(2)
methods = client.recv(n_methods)
if 2 in methods: # username/password offered
client.sendall(b"\x05\x02")
client.recv(1)
user = client.recv(client.recv(1)[0]).decode()
client.recv(client.recv(1)[0]) # password, discarded
client.sendall(b"\x01\x00")
else:
user = "-"
client.sendall(b"\x05\x00")
_v, _cmd, _rsv, atyp = client.recv(4)
if atyp == 3:
host, kind = client.recv(client.recv(1)[0]).decode(), "DOMAIN"
elif atyp == 1:
host, kind = socket.inet_ntoa(client.recv(4)), "IPV4"
else:
host, kind = socket.inet_ntop(socket.AF_INET6, client.recv(16)), "IPV6"
port = struct.unpack(">H", client.recv(2))[0]
print(f"user={user} atyp={kind} target={host}:{port}", flush=True)
upstream = socket.create_connection((host, port), timeout=10)
client.sendall(b"\x05\x00\x00\x01" + b"\x00" * 6)
threading.Thread(target=relay, args=(client, upstream), daemon=True).start()
relay(upstream, client)
except (OSError, ValueError) as exc:
print("error:", exc, flush=True)
client.close()
server = socket.create_server(("127.0.0.1", 1080))
while True:
conn, _ = server.accept()
threading.Thread(target=handle, args=(conn,), daemon=True).start()
Run it with python3 socks_log.py, add socks5 127.0.0.1 1080 test test to a scratch config, and run your real tool against it. It binds to localhost only and has no access control, so don't expose it. Once the tool shows DOMAIN lines, swap the scratch config for the SparkProxy one. For browser-side leaks such as WebRTC and DoH, the DNS leak testing and mitigation guide goes further than a terminal can.
Log Lines Decoded: What Failed and Whether a Proxy Helps
All strings below were produced on 2026-10-30, either against SparkProxy's gateway with deliberately wrong credentials or against local test servers.
| You see | Cause | Fix | Can a different proxy fix it? |
|---|---|---|---|
| `[proxychains] config file found: /etc/proxychains4.conf` when you passed `-f` | Your `-f` path doesn't exist; lookup fell through | Fix the path, use an absolute one | No |
| `proxy ... has invalid value or is not numeric` | Hostname as the first `[ProxyList]` entry | Resolve to an IP when writing the file | No |
| `... 13000 ... api.ipify.org:443 <--socket error or timeout!` | On SparkProxy's SOCKS5 port, this is what wrong or missing credentials produced, not a network fault | Check `user pass` on the line, or whitelist your IP | No, it's account config |
| `... 11000 ... api.ipify.org:443 <--denied` | HTTP port rejected the CONNECT (we got it with bad Basic credentials) | Same as above | No |
| `[proxychains] Strict chain ... | The proxy itself didn't accept a TCP connection | Check the IP is current and the port is right | Only if the proxy is genuinely down |
| `!!!need more proxies!!!` (from `src/core.c`) | Dynamic, random or round-robin mode ran out of live entries | Fix the dead entries or add working ones | Yes, if entries are genuinely down |
| `proxychains: can't load process './x'.` | Target binary path is wrong | Fix the path | No |
| `preloading ...` with no `DLL init` | Static binary | Section 8 options | No |
| `DLL init` but no chain lines | Go program or tool not using libc `connect()` | Environment variables or graftcp | No |
| Tool reaches the site but gets blocked or served a CAPTCHA | Routing works; the target dislikes the exit IP or request | Try another country, Sticky Sessions, or slower request rate | Yes, this is the one a proxy addresses |
The SOCKS5 row is the one that costs people hours. proxychains reports an authentication refusal on SOCKS5 with the same text it uses for a dead socket, so "socket error or timeout" on a gateway you can otherwise reach with nc -zv almost always means credentials. Proxy-side status codes in general are covered in proxy error codes explained.
If your tool only needs to fetch web pages and you control how it's called, you can skip local routing entirely: SparkProxy's Scraping API at https://scrape.sparkproxy.io/api/v1 takes the target URL with an X-API-Key header, and render_js=false keeps a plain fetch at 1 credit. New accounts get 1,000 free credits.
Frequently asked questions
FAQ
Usually not. Go's net package calls the connect system call directly instead of libc, so even a dynamically linked Go binary that loads proxychains connects direct. Use HTTPS_PROXY=socks5://... if the program uses Go's default HTTP client, or a syscall tracer such as graftcp.
strict_chain uses every listed proxy in order and fails if any is down. dynamic_chain uses them in order but skips dead ones, and fails only when none respond. With one proxy, strict_chain is the right choice because it fails closed.
The first entry in [ProxyList] must be an IP address. Hostnames are allowed only for later entries in a strict chain with proxy_dns enabled, so resolve your provider's hostname with getent ahostsv4 when you write the file.
No. It covers lookups made through getaddrinfo and gethostbyname, which is most programs. Tools that use gethostbyname2, call the resolver library directly, or send their own UDP queries (like dig) still resolve locally. Watch tcpdump port 53 to check.
Add the credentials after the port on the same line: socks5 203.0.113.10 13000 USER PASS. The same layout works for HTTP proxies with Basic auth. Avoid spaces in credentials, and keep the file mode at 600.
Yes. Pass a file with -f, set PROXYCHAINS_CONF_FILE, or drop a proxychains.conf in the working directory. Check the config file found: log line, because a missing -f file silently falls back to the next location.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
Scrapy: Could Not Open CONNECT Tunnel With Proxy (Fixed)
Scrapy could not open CONNECT tunnel with proxy? Read the status inside the TunnelError, then fix 407s, dropped Proxy-Authorization, timeouts and concurrency.
Proxy Showing Wrong Country? Align Every Geo Signal
Proxy showing wrong country content even though the IP checks out? Test cookies, Accept-Language, timezone, CDN edge and geo-IP lag in order, then fix each one.
aiohttp ClientProxyConnectionError: Fix Every Proxy Error
Fix aiohttp ClientProxyConnectionError, ClientHttpProxyError 407 and ServerDisconnectedError with proxies: verbatim errors, a diagnosis order and tested fixes.
