跪拜 Guibai
← Back to the summary

A Local TLS Proxy That Reroutes LM Studio Downloads Through China's Hugging Face Mirror

LM Studio Hugging Face Local Hijack Tutorial

Use a local TLS termination proxy to rewrite LM Studio's Hugging Face downloads to the domestic mirror hf-mirror.com, then connect directly to the CDN returned by the mirror. The goals are:

This tutorial is based on actual testing on Windows 10/11 + LM Studio 0.4.x. The core ideas, specific domain names, path rewriting, and certificate SAN must all be implemented exactly as described here; missing any one item will cause it to continue connecting to the official Cloudflare.

It is recommended to let an agent operate directly.


0. First, understand why it's slow

LM Studio is an Electron application, and model downloads go through Node / undici, not the browser.

Common misconceptions:

Misconception Actual Situation
Turning on the system proxy makes LM Studio use the proxy The Electron / Node downloader does not read the Windows system proxy
useHFProxy=true switches to hf-mirror.com It only rewrites the URL to https://search.lmstudio.ai/v1/hf-proxy/<repo>/...
Modifying hosts to hijack huggingface.co is enough When useHFProxy=true, the first hop is search.lmstudio.ai
DIRECT in Clash rules equals a direct broadband connection When Clash is on, mihomo's DNS / TUN can override hosts
The mirror can be used directly with Host: huggingface.co Later, the mirror checks the client IP; non-domestic IPs will be rejected or redirected back to the official site

The actual path tested:

LM Studio
  └─ https://search.lmstudio.ai/v1/hf-proxy/<org>/<repo>/resolve/main/<file>
        └─ Local 127.0.0.1:443 hijack
              ├─ Remove prefix /v1/hf-proxy
              ├─ Change Host to hf-mirror.com
              └─ Direct connect to 160.16.86.14:443 using domestic egress
                    └─ 302
                          └─ https://us.aws.cdn.hf.co/...
                                └─ Directly reachable CloudFront edge (this article uses 13.214.85.108)

Therefore, you must hijack simultaneously:


1. Prerequisites

  1. Your broadband itself can directly connect to hf-mirror.com. hf-mirror blocks non-domestic IPs. First, completely exit Clash / turn off TUN and other proxies, then visit https://hf-mirror.com.

If this step fails, this solution is invalid.

  1. Keep Clash / TUN off during downloads. TUN's DNS hijacking will override hosts.
  2. Administrator privileges are required: install local CA, modify hosts, listen on 443.
  3. Python 3.10+ is required (standard library is sufficient, no extra packages needed).
  4. OpenSSL is required (the one included with Git for Windows is fine).
  5. If your machine's IPv6 is enabled, it must be hijacked together. LM Studio will prioritize Cloudflare IPv6; only modifying IPv4 will be bypassed.

First, confirm the mirror IP and CDN IP are still reachable. These two addresses will change; do not blindly copy when replicating:

# Check the mirror's current IP
nslookup hf-mirror.com

# After turning off Clash, test TLS one by one
curl --noproxy "*" -I --resolve hf-mirror.com:443:<mirror IP> https://hf-mirror.com/

# Check CDN, then test one by one
nslookup us.aws.cdn.hf.co

A set verified to work in this article:

hf-mirror.com     -> 160.16.86.14
us.aws.cdn.hf.co  -> 13.214.85.108

If the reachable IPs you test are different, change all subsequent scripts and hosts to your own.


2. Directory Structure

It is recommended to place everything uniformly in the user directory. All subsequent paths should be modified according to this:

%USERPROFILE%\hfproxy\
  ca.key
  ca.crt
  server.key
  server.crt
  server.csr
  san.cnf
  hf_hijack.py
  start_lms_hijack.ps1          # Optional: Start LM Studio with DNS mapping
  start_proxy_hidden.vbs        # Auto-start on login

PowerShell:

New-Item -ItemType Directory -Force "$env:USERPROFILE\hfproxy" | Out-Null
cd $env:USERPROFILE\hfproxy

3. Generate Local CA and Server Certificate

In Git Bash, openssl -subj /CN=... will be converted to a path by MSYS; you must first turn off path conversion.

cd "$USERPROFILE/hfproxy"
export MSYS_NO_PATHCONV=1

# 1. Local CA
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout ca.key -out ca.crt -days 3650 \
  -subj "/CN=HfHijack Local CA"

# 2. Server private key
openssl genrsa -out server.key 2048

# 3. SAN: Must include all hostnames LM Studio will access
cat > san.cnf <<'EOF'
subjectAltName=DNS:huggingface.co,DNS:*.huggingface.co,DNS:cdn-lfs.huggingface.co,DNS:hf.co,DNS:us.aws.cdn.hf.co,DNS:search.lmstudio.ai
EOF

# 4. CSR + Sign with local CA
openssl req -new -key server.key -subj "/CN=huggingface.co" -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 3650 -sha256 -extfile san.cnf

# 5. Verify
openssl verify -CAfile ca.crt server.crt
openssl x509 -in server.crt -noout -ext subjectAltName

search.lmstudio.ai must be in the SAN. If only huggingface.co is signed, LM Studio's connection to the local proxy will fail directly due to a hostname mismatch.


4. Install the CA into the System and Make Node Trust It

Windows' built-in curl / Schannel recognizes system root certificates; LM Studio's Node downloader does not read system root certificates by default. Both sides must be handled.

Administrator CMD / PowerShell:

certutil -addstore -f Root "%USERPROFILE%\hfproxy\ca.crt"

Current user environment variable (no admin required):

reg add "HKCU\Environment" /v NODE_EXTRA_CA_CERTS /t REG_SZ /d "%USERPROFILE%\hfproxy\ca.crt" /f

Take effect immediately for the current session:

$env:NODE_EXTRA_CA_CERTS = "$env:USERPROFILE\hfproxy\ca.crt"

Also, clear invalid local proxy variables. If you previously set HTTP_PROXY=http://127.0.0.1:7897 for Clash, Node will connect to a dead port and timeout after Clash is closed:

reg delete "HKCU\Environment" /v HTTP_PROXY /f
reg delete "HKCU\Environment" /v HTTPS_PROXY /f
reg delete "HKCU\Environment" /v ALL_PROXY /f

Note:


5. Write the Hijack Proxy

Save as %USERPROFILE%\hfproxy\hf_hijack.py. Change the path and mirror IP to your own.

# -*- coding: utf-8 -*-
"""
Local TLS termination proxy:
1. Masquerade as huggingface.co / search.lmstudio.ai
2. Remove the /v1/hf-proxy prefix
3. Change the Host to hf-mirror.com
4. Connect directly to the mirror IP using domestic egress
"""
import re
import socket
import socketserver
import ssl
import threading
from pathlib import Path

BASE = Path(__file__).resolve().parent
CERT = str(BASE / "server.crt")
KEY = str(BASE / "server.key")

MIRROR_IP = "160.16.86.14"   # Change to the hf-mirror.com IP you tested as reachable
MIRROR_HOST = "hf-mirror.com"
LISTEN = ("::", 443)         # IPv6 dual-stack, accepts both 127.0.0.1 and ::1

server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_ctx.load_cert_chain(CERT, KEY)
server_ctx.set_alpn_protocols(["http/1.1"])  # Force HTTP/1.1 for easy Host modification

upstream_ctx = ssl.create_default_context()

_HOST_RE = re.compile(rb"^(Host:\s*)([^\r\n]*)", re.MULTILINE)
_REQUEST_RE = re.compile(rb"^([A-Z]+\s+)/v1/hf-proxy/", re.MULTILINE)


def rewrite_request(head: bytes) -> bytes:
    # LM Studio: GET /v1/hf-proxy/org/repo/...  ->  GET /org/repo/...
    head = _REQUEST_RE.sub(rb"\1/", head, count=1)

    def repl(m):
        host = m.group(2).decode("ascii", "ignore").strip()
        if host and host != MIRROR_HOST:
            return m.group(1) + MIRROR_HOST.encode()
        return m.group(0)

    return _HOST_RE.sub(repl, head)


def relay(src, dst):
    try:
        while True:
            data = src.recv(65536)
            if not data:
                break
            dst.sendall(data)
    except Exception:
        pass
    finally:
        for s in (src, dst):
            try:
                s.shutdown(socket.SHUT_RDWR)
            except Exception:
                pass
            try:
                s.close()
            except Exception:
                pass


class Handler(socketserver.BaseRequestHandler):
    def handle(self):
        client = self.request
        try:
            buf = b""
            while b"\r\n\r\n" not in buf:
                chunk = client.recv(4096)
                if not chunk:
                    return
                buf += chunk
                if len(buf) > 100 * 1024:
                    return
            head, rest = buf.split(b"\r\n\r\n", 1)
            head = rewrite_request(head)
            raw = socket.create_connection((MIRROR_IP, 443), timeout=15)
            upstream = upstream_ctx.wrap_socket(raw, server_hostname=MIRROR_HOST)
            upstream.sendall(head + b"\r\n\r\n" + rest)
            t = threading.Thread(target=relay, args=(client, upstream), daemon=True)
            t.start()
            relay(upstream, client)
            t.join(timeout=2)
        except Exception as e:
            print(f"[ERR] {e}", flush=True)


class Server(socketserver.ThreadingTCPServer):
    address_family = socket.AF_INET6
    daemon_threads = True
    allow_reuse_address = True

    def server_bind(self):
        # Windows default V6ONLY=0 can also dual-stack; explicitly turning it off is more stable
        self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
        super().server_bind()


if __name__ == "__main__":
    srv = Server(LISTEN, Handler)
    srv.socket = server_ctx.wrap_socket(srv.socket, server_side=True)
    print(f"HF hijack proxy listening on {LISTEN[0]}:{LISTEN[1]}", flush=True)
    srv.serve_forever()

Run it manually once to confirm no errors:

python "%USERPROFILE%\hfproxy\hf_hijack.py"

You should see:

HF hijack proxy listening on :::443

Open another terminal to check the listening status:

0.0.0.0:443    LISTENING
[::]:443       LISTENING

If 443 is occupied, first check who is using it:

netstat -ano | findstr ":443"

Common occupants: IIS, old proxy instances, some accelerators. This solution must occupy the local 443 because the client accesses the standard HTTPS port.


6. Modify hosts

Open with Notepad as administrator:

C:\Windows\System32\drivers\etc\hosts

Back it up first, then append. Change us.aws.cdn.hf.co to the CDN IP you tested as reachable:

# === HF hijack ===
127.0.0.1 huggingface.co
127.0.0.1 hf.co
127.0.0.1 cdn-lfs.huggingface.co
127.0.0.1 search.lmstudio.ai
13.214.85.108 us.aws.cdn.hf.co
::1 huggingface.co
::1 hf.co
::1 cdn-lfs.huggingface.co
::1 search.lmstudio.ai
# === end HF hijack ===

Then:

ipconfig /flushdns

Verify with ping, not nslookup. nslookup bypasses hosts and will show the real DNS.

ping huggingface.co
ping search.lmstudio.ai
ping us.aws.cdn.hf.co

Expected:

huggingface.co     -> 127.0.0.1
search.lmstudio.ai -> 127.0.0.1
us.aws.cdn.hf.co   -> 13.214.85.108   (or your own reachable IP)

The four ::1 lines cannot be omitted. If only IPv4 is written, LM Studio will connect directly to Cloudflare IPv6.


7. Verify the Proxy Itself

Clash must be in an exited state. Git Bash / PowerShell are both fine.

# 1. Metadata: should be 200
curl --ssl-no-revoke --noproxy "*" -s -m 20 -o /dev/null -w "HTTP %{http_code}\n" \
  "https://huggingface.co/gpt2/resolve/main/config.json"

# 2. LM Studio real URL form: should 302 and then resume
curl --ssl-no-revoke --noproxy "*" -s -m 60 -L \
  -H "Range: bytes=0-8388607" -o /dev/null \
  -w "HTTP %{http_code} speed=%{speed_download}\n" \
  "https://search.lmstudio.ai/v1/hf-proxy/gpt2/resolve/main/pytorch_model.bin"

Normal results:

If HTTP 000 / exit 35: The proxy is not running, or the certificate SAN is wrong. If exit 60: The CA is not in the system root certificate store, or curl used Schannel and you didn't add --ssl-no-revoke. If it keeps connecting to a Cloudflare IP: hosts didn't take effect, or Clash TUN is still on.

Test once more with LM Studio's bundled Node, which is closer to the real download stack:

$env:NODE_EXTRA_CA_CERTS = "$env:USERPROFILE\hfproxy\ca.crt"
Remove-Item Env:HTTP_PROXY,Env:HTTPS_PROXY,Env:ALL_PROXY -ErrorAction SilentlyContinue
& "$env:USERPROFILE\.lmstudio\.internal\utils\node.exe" -e @"
const https = require('https');
const url = 'https://search.lmstudio.ai/v1/hf-proxy/gpt2/resolve/main/pytorch_model.bin';
const headers = { Range: 'bytes=0-8388607' };
const t0 = Date.now();
function go(u, n=0) {
  https.get(u, { headers }, res => {
    if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && n < 5) {
      console.log('redirect', res.statusCode, new URL(res.headers.location).hostname);
      res.resume();
      return go(res.headers.location, n+1);
    }
    let b = 0;
    res.on('data', c => b += c.length);
    res.on('end', () => {
      const s = (Date.now()-t0)/1000;
      console.log('HTTP', res.statusCode, 'bytes='+b, 'MB/s='+(b/s/1048576).toFixed(2));
    });
  }).on('error', e => { console.error(e); process.exit(1); });
}
go(url);
"@

Expected output similar to:

redirect 302 us.aws.cdn.hf.co
HTTP 206 bytes=8388608 MB/s=3.50

8. Configure LM Studio

8.1 Settings

%USERPROFILE%\.lmstudio\settings.json:

"useHFProxy": false

Explanation:

This proxy handles both. It's safer to turn off useHFProxy while continuing to hijack search.lmstudio.ai to prevent the app from rewriting old task URLs back.

8.2 Old Download Tasks

Already started tasks write their URLs into:

%USERPROFILE%\.lmstudio\.internal\download-jobs-info.json

It may contain both:

request.url  = https://huggingface.co/...
download.url = https://search.lmstudio.ai/v1/hf-proxy/...

When resuming a task, LM Studio might rewrite download.url back to the relay address. So don't just modify the JSON; the proxy must be able to handle /v1/hf-proxy.

Before changing the configuration, completely exit LM Studio and back up these two files.

8.3 Launch Method

A normal double-click also works, provided:

  1. The proxy is already listening on 443
  2. hosts has taken effect
  3. NODE_EXTRA_CA_CERTS has been written to the user environment
  4. There are no invalid HTTP_PROXY variables

Optional: Use Chromium host mapping as an extra layer. It is effective for the Chromium Network Service but not guaranteed for the main process Node fetch(), so it cannot replace hosts.

start_lms_hijack.ps1:

$rules = 'MAP search.lmstudio.ai 127.0.0.1, MAP huggingface.co 127.0.0.1, MAP hf.co 127.0.0.1, MAP cdn-lfs.huggingface.co 127.0.0.1, MAP us.aws.cdn.hf.co 13.214.85.108, EXCLUDE localhost'

Get-Process -Name 'LM Studio' -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2

$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = 'D:\AI\LLM\LM Studio\LM Studio.exe'   # Change to your installation path
$startInfo.UseShellExecute = $false
$startInfo.Arguments = '--host-resolver-rules="' + $rules + '" --disable-features=UseDnsHttpsSvcbAlpn,AsyncDns'
$startInfo.EnvironmentVariables['NODE_EXTRA_CA_CERTS'] = "$env:USERPROFILE\hfproxy\ca.crt"
$startInfo.EnvironmentVariables.Remove('HTTP_PROXY')
$startInfo.EnvironmentVariables.Remove('HTTPS_PROXY')
$startInfo.EnvironmentVariables.Remove('ALL_PROXY')
[System.Diagnostics.Process]::Start($startInfo) | Out-Null

9. Auto-start Proxy on Login

Save the following content as:

%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\HF Mirror Hijack Proxy.vbs

Use the absolute path for pythonw.exe to avoid login PATH order changes:

Set shell = CreateObject("WScript.Shell")
shell.Run """C:\Path\To\pythonw.exe"" ""%USERPROFILE%\hfproxy\hf_hijack.py""", 0, False

%USERPROFILE% will not automatically expand in VBS; write the real absolute path, for example:

Set shell = CreateObject("WScript.Shell")
shell.Run """G:\application\scoop\apps\python313\current\pythonw.exe"" ""C:\Users\YourUsername\hfproxy\hf_hijack.py""", 0, False

After restarting, use netstat -ano | findstr ":443" to confirm it is still listening.


10. How to Tell It's Working

Check three things after the download starts.

1. Connection Target

PowerShell:

Get-NetTCPConnection -State Established |
  Where-Object { (Get-Process -Id $_.OwningProcess -EA SilentlyContinue).ProcessName -eq 'LM Studio' } |
  Select-Object OwningProcess, LocalAddress, RemoteAddress, RemotePort

On success, you should see:

On failure, you will see Cloudflare:

172.67.x.x
104.26.x.x
2606:4700:...

2. Speed

After it's working, it's usually several MB/s to over ten MB/s. If it's still around 100 KB/s, it's basically still hitting the official Cloudflare.

3. Logs

%APPDATA%\LM Studio\logs\main.log

Certificate failures will report errors immediately. If it downloads a few percent and then Timed-out, it's mostly a slow relay link or a local proxy environment variable pointing to a dead port, not a CA issue.


11. Uninstall / Rollback

  1. Exit LM Studio.
  2. End hf_hijack.py in python / pythonw.
  3. Delete the entire # === HF hijack === section in hosts.
  4. ipconfig /flushdns.
  5. Delete HF Mirror Hijack Proxy.vbs from the startup folder.
  6. Optional: Remove HfHijack Local CA from "Trusted Root Certification Authorities".
  7. Optional: Delete the user environment variable NODE_EXTRA_CA_CERTS.
  8. Change useHFProxy back to your original value.

Generally, manually copying the hosts file once before modification is sufficient as a backup.


12. The Easiest Pitfalls When Replicating

  1. Only hijacked huggingface.co. When LM Studio has useHFProxy on, the real first hop is search.lmstudio.ai.

  2. Only modified IPv4 hosts. LM Studio will go to Cloudflare IPv6 at 2606:4700:....

  3. Clash / TUN is still on. TUN DNS will override hosts, making the mapping appear "invalid".

  4. Pointed us.aws.cdn.hf.co to 127.0.0.1. The CDN certificate won't match, and large files don't need to go through the local proxy anyway. It should point to a CDN IP your machine can reach directly.

  5. Certificate SAN missed search.lmstudio.ai. Node will reject it during the handshake phase.

  6. Only installed the CA into the Windows root certificate store. LM Studio's Node also needs NODE_EXTRA_CA_CERTS, and the application must be restarted.

  7. Residual HTTP_PROXY=http://127.0.0.1:7897. After Clash is closed, Node will connect to a dead port, and the logs will be full of Timed-out.

  8. Mirror IP / CDN IP expired. 160.16.86.14 and 13.214.85.108 were just the values tested at the time. Re-test on the day of replication.

  9. Using this solution on an overseas network. hf-mirror.com checks the client IP; non-domestic egress will be rejected. This solution relies on "the local broadband having a domestic IP".

  10. Using nslookup to verify hosts. It doesn't use hosts. Use ping or PowerShell [System.Net.Dns]::GetHostAddresses('huggingface.co').


13. Minimum Checklist

Check off in order:

After all items are checked, click download in LM Studio.