Why WSL2 Ubuntu Is a Different Beast Than “Linux Clash on Ubuntu”

If you landed here after reading a generic Ubuntu proxy tutorial, you probably noticed the instructions assume the proxy daemon and your shell share one network namespace. On WSL2, that assumption breaks: your Ubuntu distribution runs inside a lightweight virtual machine with its own virtual Ethernet adapter, while Windows Clash binds ports on the actual Windows host stack. Copying export http_proxy=http://127.0.0.1:7890 from a native Linux guide therefore fails in the least interesting way possible—the packet never leaves the Linux guest because 127.0.0.1 inside WSL2 is the guest itself, not the Windows process that owns mixed-port.

The happy path is smaller than it looks: prove Clash is listening where you think it is, learn the stable Windows host address as seen from WSL2, point HTTP(S)_PROXY at that address plus the correct port, then chase down the last mile where DNS still resolves through a resolver that cannot see the same split rules as Clash. This article stays focused on command-line workflows—apt, git, curl, and friends—because those are the tools that ignore Windows’ system proxy toggle entirely. For contrast, our native Ubuntu systemd guide covers running the core directly on Linux, which is the other half of the story when you eventually move Clash inside WSL or onto bare metal.

Before touching WSL, make sure the Windows side is not a mystery: the Clash for Windows walkthrough explains importing a subscription, enabling the local listener, and understanding what “System Proxy” does for Win32 apps versus what it does not do for WSL. You will reuse that mental model here, just aimed across the hypervisor boundary instead of at a single OS.

The Two Localhosts and Why mixed-port Matters

Every troubleshooting thread about WSL2 and proxies eventually rediscovers the same diagram: two parallel loopback interfaces that happen to share a keyboard. When documentation says “open http://127.0.0.1:9090 for the dashboard,” that URL is true for a browser running on Windows. Inside WSL2, the same numeric address refers to the Linux init namespace, so your dashboard request hits nothing unless you have an unrelated listener inside the distro. The fix is not to “try harder” with localhost—it is to target the Windows host’s IP on the virtual switch, or another supported alias, with the port your Clash profile actually exposes.

Modern Clash-class cores typically expose a single mixed-port that accepts both HTTP CONNECT and SOCKS-style sessions, which is why you will see one number (for example 7890) repeated in screenshots even when older guides listed separate port and socks-port values. Treat mixed-port as the primary front door unless you have a deliberate reason to split protocols. If your YAML still sets legacy keys, align your environment variables with whichever listener is truly bound in the running process—guessing breaks curl fast and wastes an hour on firewall rules that were never the problem.

Prerequisites on the Windows Side

Start with observable facts on Windows. Clash must be running, the profile should load without parse errors, and the local inbound must bind to an interface WSL2 can reach. Many users leave the listener on loopback only; that works for Windows browsers but not for another virtual machine. Enable allow-lan (or the equivalent “Allow LAN” toggle in your GUI) and set bind-address to * or an explicit adapter address if your build requires it, then restart the core so the change is not just a line in a file on disk.

Windows Defender Firewall will happily drop the first SYN packet from WSL2 if there is no inbound rule for your TCP port. Rather than disabling the firewall, add a narrow allow rule for the Clash executable or the specific port on private networks. The LAN sharing article walks through the same class of problem for phones on Wi‑Fi; the underlying idea matches what WSL2 traffic looks like from Windows—an internal client on a virtual subnet rather than loopback. See Share Clash Proxy on LAN: allow-lan and Firewall Step-by-Step for a firewall-first checklist you can reuse here.

Finding the Windows Host IP From Inside WSL2

There is no shortage of recipes; pick one you can remember under stress. On recent WSL2 builds, printing /etc/resolv.conf often shows a single nameserver line pointing at the Windows host’s internal address on the virtual switch—quick to type and good enough for many setups. Another classic is ip route show default: the gateway field of the default route is frequently the same host IP. Some users prefer $(hostname).local mDNS resolution; when it works it reads nicely in scripts, but it is not as universally reliable as the resolver-derived address across every corporate image or Insider build.

Whatever you choose, capture the value in a shell variable for the rest of the session, for example export WIN_HOST=192.168.x.x after substituting the real address. Re-check after reboots or when Windows installs a hypervisor update, because these internal addresses can shift. If you hard-code the value into .bashrc, add a comment with the command you used to discover it so future-you does not treat the number as magic.

Confirming the Real Listener Port

Open your active profile on Windows and read the inbound section like you mean it. Note mixed-port first. If you run a fork that still separates HTTP and SOCKS, write both numbers down, but remember that most CLI tools want HTTP for http:// schemes and may need ALL_PROXY with a socks5h:// URL for hostname resolution inside the tunnel. Mixed mode exists precisely to reduce that friction: one port, fewer moving parts, fewer mistakes when you are tired.

Sanity-check from Windows itself with curl or the client’s built-in log panes before involving WSL2. If Windows cannot curl through localhost, WSL2 will not save you. Only after the listener is proven should you run a cross-namespace test like curl -v http://$WIN_HOST:$PORT from Ubuntu, expecting either an HTTP response from a metrics endpoint or a purposeful proxy error that still proves TCP connectivity.

HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and no_proxy

Most tutorials stop at uppercase HTTP_PROXY and HTTPS_PROXY. That is a decent baseline because many tools honor those names first. Lowercase variants exist for historical reasons; exporting both pairs is harmless redundancy when you are debugging stubborn binaries. For traffic that is not HTTP(S), ALL_PROXY fills gaps, especially when you choose a socks5h:// scheme so DNS for the target happens on the proxy side—critical when local DNS is flaky or polluted.

The no_proxy variable is where careful setups diverge from cargo-cult copies. Add your private subnets, the Windows host address itself, maybe localhost and 127.0.0.1, and any corporate registry hosts that must stay direct. A minimal list reduces surprises when git tries to reach an internal Git server that should not ride a residential exit. There is no universal template: paste your actual intranet suffixes, then iterate when something still tries to tunnel incorrectly.

Teaching apt and git to Use the Tunnel

apt does not read your shell exports in every code path; the reliable pattern on Debian-derived images is an Acquire::http::Proxy stanza in /etc/apt/apt.conf.d/ or a temporary -o Acquire::http::Proxy=... on the command line. Match the scheme to what Clash exposes—usually an http:// URL even for HTTPS repositories because the proxy speaks CONNECT. Keep the file permissions tight; world-readable proxy URLs leak credentials if you ever embed them (please do not embed credentials).

git respects http.proxy and https.proxy configuration entries. Global settings are convenient; per-repo settings are safer when you toggle between employer VPN and personal subscriptions. Remember that SSH remotes ignore HTTP proxy variables unless you tunnel differently; this article focuses on HTTPS Git remotes and HTTP tooling, which is what most WSL2 developers use with GitHub or GitLab HTTPS endpoints.

When DNS Is the Real Problem (Not the Proxy)

A frustrating failure mode looks like this: curl through the proxy times out with a hostname, but an IP literal works, or you see “Could not resolve host” before any TCP attempt. That means your DNS path still goes to a resolver that is blocked, filtered, or simply unaware of the split routing you expect from Clash. Inside WSL2, systemd-resolved may or may not be running depending on how Microsoft wired your image; /etc/resolv.conf might be a symlink you are not supposed to edit by hand. Understand which resolver you actually use before randomly pointing everything at 8.8.8.8.

If your Clash profile uses fake-ip, remember that illusion only holds for applications whose DNS queries pass through Clash’s DNS path. A bare dig from WSL2 to a public resolver will not magically apply your rule files. Align tools with the same DNS pipeline your Windows client uses, or temporarily test with curl -x plus socks5h so resolution happens remotely and you can separate “bad resolver” from “bad node.” For a deeper tour of TUN, DNS hijacking, and resolver alignment, keep the TUN mode guide open in another tab—but do not enable TUN inside WSL2 casually unless you know how it interacts with the virtual switch and Windows routes.

Corporate environments sometimes inject HTTP proxies or DNS filtering on the Windows host that do not replicate inside WSL2. When that happens, your fix might be forwarding WSL2 DNS to the Windows resolver explicitly, or running a small local forwarder. The key is to observe: capture where the NXDOMAIN or SERVFAIL originates before toggling random Clash switches.

Windows Firewall and Hyper-V Edge Cases

If TCP to $WIN_HOST:$PORT never completes—curl hangs at “Trying…”—return to Windows and verify inbound rules. Public versus private network profiles matter: a laptop on café Wi‑Fi may classify the vEthernet adapter differently than Ethernet at home. Some security suites add another layer that filters WSL2 subnets. Fix the network profile first, then add explicit allow rules, then retest with Test-NetConnection in PowerShell targeting the same port from the Windows side to confirm the process is listening where you think it is.

curl Recipes You Can Trust

Start with verbose mode: curl -v -x http://$WIN_HOST:$PORT https://www.cloudflare.com/cdn-cgi/trace should show a CONNECT tunnel, a certificate handshake, and a short text body listing an exit IP that matches your expectation. If you need SOCKS, swap to curl --socks5-hostname $WIN_HOST:$PORT so the proxy performs DNS. Compare against a run without -x to see the delta in timing and IP.

For apt-like behavior without touching apt yet, fetch a small static URL over HTTP and HTTPS separately. Break the proxy intentionally once—wrong port, stopped Clash—to memorize the error signatures. That rehearsal saves time when you are offline at midnight and the failure mode is ambiguous.

When IPv6 enters the picture, some targets prefer AAAA records and your tunnel might not handle IPv6 end-to-end. If you see intermittent failures only on certain sites, try forcing IPv4 in curl with -4 during diagnosis, then adjust Clash or OS preferences once you understand the pattern.

Persisting Environment Variables Without Breaking Other Workflows

Many developers append exports to ~/.bashrc or ~/.profile. That is fine for interactive shells but easy to forget for non-interactive sessions like CI-like scripts. Document whether you want the proxy always on or gated behind a function such as proxy_on / proxy_off that toggles the same variables. Teams appreciate explicit toggles because corporate VPNs and personal subscriptions do not mix well when exports leak into Ansible runs.

If you use multiple WSL distributions, duplicate the snippet thoughtfully. Each distro maintains its own home directory; there is no single /etc/wsl.conf trick that exports proxy variables to every shell on earth. Keep the Windows host discovery command near the exports so you can refresh the IP when something looks stale.

Troubleshooting Cheat Sheet

Symptom: “Connection refused” to $WIN_HOST:$PORT. Likely causes: Clash not running, wrong port, listener bound only to loopback on Windows, or allow-lan disabled. Fix the Windows listener first, then retest from WSL2.

Symptom: TCP connects but TLS fails or you get an immediate 403 from a middlebox. Likely causes: transparent HTTPS inspection on the network, wrong protocol scheme in ALL_PROXY, or a node that blocks data-center IPs. Rotate nodes methodically instead of flipping every Clash toggle at once.

Symptom: Works in a Windows browser, fails in WSL2 with the same hostname. Likely causes: DNS split, missing proxy variables for the CLI tool, or the browser using a PAC file that WSL2 does not see. Align DNS or force SOCKS5h for resolution inside the tunnel while testing.

Symptom: apt still tries direct even though curl works. Likely causes: apt configuration not updated, or a stale proxy line pointing at the wrong schema. Re-read /etc/apt/apt.conf.d/* and remove conflicting fragments.

Closing the Loop

Routing WSL2 through Windows Clash is mostly geometry: pick the host IP that the virtual switch actually routes to, aim your proxy environment variables at the real mixed-port, teach each tool the same story, then prove connectivity with curl -v before you blame upstream nodes. The hard part is rarely YAML syntax—it is the invisible line between two loopback worlds and the DNS path that still thinks it lives on a different planet.

Compared with ad hoc port forwards or duplicate Clash instances fighting over profiles, reusing one well-maintained Windows core keeps subscription state and rule edits unified. When you outgrow that layout, the natural next step is either native Linux service management—as in our Ubuntu systemd article—or a router-level deployment if you want every device in the house to share the same policy.

→ Download Clash for free and experience the difference when you want matching clients across Windows, Linux, and mobile without hunting stray binaries.