Kali gives you three different ways to push traffic through a proxy, and they cover different tools. Getting them right matters more here than on a normal desktop, because the failure mode is not a broken download — it is a packet leaving from your real address while you believe it did not.
There is no single proxy switch on Kali. What exists is three mechanisms, and knowing which one covers a given tool is the whole skill:
| Layer | Reaches |
|---|---|
| Environment variables | Well-behaved HTTP clients: curl, wget, gobuster, ffuf, nikto, most Python tooling. |
| proxychains4 | Any dynamically linked program's TCP connections, whether or not it knows what a proxy is. |
| The tool's own setting | Burp Suite, ZAP, sqlmap, Metasploit, browsers — each has a proxy field, and it is always the most reliable option when it exists. |
The rule of thumb: use a tool's native proxy setting when it has one, environment variables for scripts, and proxychains only for programs that have neither.
Kali defaults to zsh, so the persistent home for these is ~/.zshrc (add them to
~/.bashrc too if you switch shells):
export http_proxy="http://USERNAME-country-us:PASSWORD@gw.pyproxy.com:1111" export https_proxy="$http_proxy" export no_proxy="localhost,127.0.0.1,::1,10.10.10.0/24" export HTTP_PROXY="$http_proxy" HTTPS_PROXY="$https_proxy" NO_PROXY="$no_proxy"
https_proxy correctly begins with http:// — the scheme describes the hop to
the proxy, not the destination, and HTTPS travels through a CONNECT tunnel over it.
Two Kali-specific traps. First, no_proxy must contain 127.0.0.1 as well as
localhost, or every request to a local listener — Burp on 8080, a local web shell handler —
is handed to the proxy and dies. Second, much of Kali is run under sudo, which strips the
environment: use sudo -E, or the variables silently do nothing.
Verify before you trust it:
curl -s https://httpbin.org/ip curl -s --noproxy '*' https://httpbin.org/ip
Different answers mean the proxy is live. Identical answers mean it is being ignored.
proxychains4 ships with Kali. It preloads a library that replaces the libc connect()
call, so a program it wraps makes its TCP connections through your proxy list without knowing anything
about it. The global config is /etc/proxychains4.conf:
dynamic_chain # strict_chain # random_chain # chain_len = 2 proxy_dns remote_dns_subnet 224 tcp_read_time_out 15000 tcp_connect_time_out 8000 quiet_mode [ProxyList] http gw.pyproxy.com 1111 USERNAME-country-us PASSWORD
Then wrap a command:
proxychains4 curl -s https://httpbin.org/ip proxychains4 -f ~/pyproxy.conf gobuster dir -u https://target.example -w /usr/share/wordlists/dirb/common.txt
The -f flag points at a config of your own, which is much better practice than editing
the system file — credentials in /etc/proxychains4.conf are world-readable by default.
chmod 600 your copy.
[ProxyList] entry must therefore start with http. A
socks5 gw.pyproxy.com 1111 … line points proxychains at a protocol the gateway does not
speak, and every connection fails with a denied or timed-out chain — which reads like a credentials
problem and is not one.This costs less than it sounds: proxychains drives HTTP proxies with CONNECT, which
tunnels arbitrary TCP, so wrapped tools behave the same. What you lose is UDP, which SOCKS5 can carry
and CONNECT cannot — so UDP tools stay unproxied either way.
Exactly one of the chain modes must be uncommented, and the stock file ships with
strict_chain active — which is the wrong default for most people.
dynamic_chain walks the list in order and skips entries that are down, using
whatever is alive. With a single gateway entry, this is what you want: it is the same path, but a
transient failure does not abort the run.strict_chain requires every proxy in the list to be up and uses all of them in
the exact order written. Choose it when the route itself is the point — a pivot through a jump host,
say — and you would rather the connection fail than take a different path.random_chain picks entries at random, honouring chain_len. Only
meaningful with several independent proxies, and it makes debugging harder.DNS is UDP, so proxychains cannot tunnel it the ordinary way. Left alone, your machine resolves every target hostname through your own resolver — a complete record of what you were looking at, sitting in your ISP's or the client network's logs, from your real address, while the HTTP traffic went through the proxy.
proxy_dns fixes this by handing the program a fake address from the
remote_dns_subnet range (224.x.x.x by default) and resolving the real name over the chain
when the connection is made. Keep it enabled. That fake address is why tools sometimes print a
nonsensical IP for a target, and resolution over a CONNECT chain is slower than local
DNS — raise tcp_connect_time_out if lookups time out. Confirm nothing leaked by watching
port 53 in another terminal:
sudo tcpdump -n -i any port 53
Run your proxychains command and that capture should stay quiet.
Do not run Burp under proxychains. Burp is a JVM application with its own network stack and its own listener, and wrapping it produces confusing breakage. Configure the upstream proxy inside Burp instead — the chain becomes browser → Burp on 127.0.0.1:8080 → gateway → target, with Burp still decrypting and editing everything in the middle.
In Settings → Network → Connections → Upstream Proxy Servers, click Add and set:
* for everything, or a pattern such as
*.target.example to send only that scope through the proxy.gw.pyproxy.com, Proxy port 1111.USERNAME-country-us chooses the exit country and adding a session token holds
one exit address for about thirty minutes — which is what you want when a target's session must appear
to come from one visitor.Add a second rule above it for internal destinations with the proxy fields left blank, so
127.0.0.1 and any lab range go direct. Rules are matched top-down. Test with a request to
an address-echo endpoint through Burp's own browser and check the reply shows the gateway's exit
address, not yours.
This is the part that gets people caught, and it follows directly from how proxychains works: it hooks a library call. Anything that does not use that library call is not proxied, and is not blocked either — it simply goes out directly.
nmap -sS builds raw packets and
writes them to a raw socket, bypassing connect() entirely. Running
proxychains4 nmap -sS target produces a scan from your real IP address with no warning
that the proxy was skipped.The same applies to -sU, -sA and -sF scans, to -O
OS detection, to the ICMP host discovery nmap does by default, and to ping,
masscan, hping3, arp-scan and the wireless tools. Raw-socket
scans also need root, and root plus proxychains is exactly the combination that silently produces
direct packets.
If a scan must go through the proxy, use the TCP connect scan and disable everything that uses raw sockets or local DNS:
proxychains4 nmap -sT -Pn -n -p 80,443,8080 target.example
-sT makes nmap call connect() so proxychains can catch it,
-Pn skips host discovery and -n skips name resolution. It is far slower and
noisier at the target, and aggressive timing such as -T5 produces false results through a
proxy, so scan a short port list. nmap's own --proxies http://host:port is worth knowing
about too, though it covers a limited set of scan types and scripts.
The honest summary: proxy your HTTP-layer work, and route port scanning through a real network path — a VPN or a pivot host — rather than proxychains.
All three layers above point at the same thing: a hostname, a port and a username that carries the options. That is why a single line proves the whole setup:
curl -x http://USERNAME-country-us:PASSWORD@gw.pyproxy.com:1111 https://httpbin.org/ip
PyProxy sells residential, ISP, mobile and datacenter proxies on one balance, billed per gigabyte
rather than per address, and the traffic does not expire — which suits security work, where a box sits
idle between engagements. Residential is HTTP and HTTPS, so configure it as an http entry
everywhere, including in proxychains4.conf.
Only a TCP connect scan. proxychains works by intercepting the libc connect() call, and nmap's default SYN scan builds raw packets that never go through it, so those packets leave from your real address. Run nmap -sT -Pn -n under proxychains, or do not proxy it at all.
dynamic_chain for everyday work, because it skips dead entries in the list and keeps going. strict_chain requires every proxy in the list to be alive and used in the exact order given, which is what you want when the routing path itself must be guaranteed.
Because proxy_dns is commented out in the config. DNS goes over UDP, which proxychains cannot tunnel, so without proxy_dns your own resolver looks up every target name and leaks it. Enable proxy_dns so names are resolved through the chain instead.
Yes, as an http entry. PyProxy residential is HTTP and HTTPS only and does not support SOCKS5, so the ProxyList line must read http gw.pyproxy.com 1111 USERNAME PASSWORD. A socks5 entry pointed at the same gateway will simply fail.