Make BUILD_PROXY optional; ship the socks2http bridge with the repo

- BUILD_PROXY now defaults to empty: the three GitHub fetches (llama.cpp zip x2,
  pinned CPython tarball) go straight to GitHub, so the Dockerfile builds as-is on
  unrestricted networks. Pass --build-arg BUILD_PROXY=socks5://<host>:<port> to
  route them through a SOCKS5 proxy instead. The hard-coded cr1.mse:2000 address
  is gone from every file.
- socks2http.py + proxyrun.sh are now committed: the Dockerfile always shells out
  to proxyrun.sh (even in direct mode), so the bridge must be in the build context.
- proxyrun.sh resolves socks2http.py relative to its own location (was hard-coded
  /src/socks2http.py), so the wrapper also works outside the image.
- socks2http.py: --upstream defaults to empty (was the hard-coded address); empty
  upstream = plain TCP passthrough, still useful for testing.
This commit is contained in:
Hermes-1 committed 2026-10-28 11:15:59 +03:00
1 parent 9e27ada6ee
commit b89369cca1
4 files changed
+311 -13

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+16 -13
View File
@@ -9,16 +9,19 @@
# --build-arg CUDA_ARCHITECTURES="60;61;70;75;80;86;89" --build-arg BUILD_VISION=0 . # GTX 10 / P100 / V100 (driver 575+)
#
# Dockerfile.v100 - V100 (sm_70) host. Also compiles the DeepSeek-V4.1 engine (strata-dsv41) into the engine
# package, and routes the three GitHub fetches (llama.cpp source zip x2, the pinned CPython tarball x1) through
# a SOCKS5 proxy. apt (distro mirror) and pip (PyPI) are NOT proxied - they are not on GitHub. If your build
# host has no internet at all (only the SOCKS), you would need to proxy those too (that requires bootstrapping
# python3 first; not done here).
# package. The three GitHub fetches (llama.cpp source zip x2, the pinned CPython tarball x1) go straight to
# GitHub by default; pass --build-arg BUILD_PROXY=socks5://<host>:<port> to route them through a SOCKS5
# proxy (via the socks2http HTTP bridge, socks2http.py + proxyrun.sh at the build-context root). apt (distro
# mirror) and pip (PyPI) are NOT proxied - they are not on GitHub. If your build host has no internet at all
# (only the SOCKS), you would need to proxy those too (that requires bootstrapping python3 first; not done here).
# docker build -f Dockerfile.v100 -t strata:v100-dsv41 \
# --build-arg CUDA_IMAGE=nvidia/cuda:12.9.1-devel-ubuntu24.04 \
# --build-arg CUDA_ARCHITECTURES=70 --build-arg BUILD_DSV41=1 --build-arg BUILD_VISION=0 \
# --build-arg BUILD_PROXY=socks5://cr1.mse:2000 \
# --build-arg BUILD_ISA_FLOOR=none \
# --build-arg STRATA_COMMIT=$(git rev-parse HEAD) .
# BUILD_PROXY the SOCKS5 endpoint the GitHub fetches go through (default socks5://cr1.mse:2000); "" = direct
# # behind a SOCKS-only network, add:
# # --build-arg BUILD_PROXY=socks5://<host>:<port>
# BUILD_PROXY optional: the SOCKS5 endpoint the GitHub fetches go through; default "" = direct network access
# STRATA_COMMIT this checkout's commit. The build context has no .git (.dockerignore), so without it the
# terminal archive carries no COMMIT file and `strata bench run` on it is nonconforming.
# docker run --rm --gpus all -p 127.0.0.1:8080:8080 --ulimit memlock=-1 --shm-size 2g --stop-timeout 120 \
@@ -36,10 +39,10 @@ ARG RUNTIME_BASE=ubuntu:24.04
# ---- 1. compile the engine and make its component package
FROM ${CUDA_IMAGE} AS engine-build
# The GitHub fetches below (the llama.cpp source zip) go through the SOCKS5 proxy named by BUILD_PROXY, via the
# socks2http bridge (socks2http.py + proxyrun.sh, both copied by `COPY . .`). Only GitHub is proxied; apt and
# pip hit the distro mirror / PyPI directly. BUILD_PROXY="" runs the build with direct network access.
ARG BUILD_PROXY=socks5://cr1.mse:2000
# The GitHub fetches below (the llama.cpp source zip) go straight to GitHub by default. BUILD_PROXY=socks5://...
# routes them through the socks2http bridge (socks2http.py + proxyrun.sh, both copied by `COPY . .`). Only GitHub
# is ever proxied; apt and pip hit the distro mirror / PyPI directly.
ARG BUILD_PROXY=
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 PATH=/venv/bin:$PATH
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates git libgomp1 libatomic1 python3 python3-pip python3-venv unzip \
@@ -103,9 +106,9 @@ ARG STRATA_COMMIT=
# --build-arg STRATA_COMMIT=$(git rev-parse HEAD), so the terminal archive carries a COMMIT file
# (recorded by `strata bench run`; a standard benchmark without it is nonconforming).
ENV STRATA_BUILD_COMMIT=${STRATA_COMMIT}
# GitHub fetches in this stage (the pinned CPython tarball, the llama.cpp source zip) go through this SOCKS5
# proxy (socks2http bridge); apt (distro mirror) does not. BUILD_PROXY="" = direct access.
ARG BUILD_PROXY=socks5://cr1.mse:2000
# GitHub fetches in this stage (the pinned CPython tarball, the llama.cpp source zip) go straight to GitHub by
# default; BUILD_PROXY=socks5://... routes them through the socks2http bridge. apt (distro mirror) does not.
ARG BUILD_PROXY=
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
COPY . /src/
RUN sh /src/proxyrun.sh "$BUILD_PROXY" -- python3 /src/packaging/package_terminal.py --out /out --cache /cache \
Executable
+57
View File
@@ -0,0 +1,57 @@
#!/bin/sh
# proxyrun: run a command behind a local SOCKS5->HTTP forward proxy.
#
# The build container cannot reach the internet directly, but can reach a
# SOCKS5 endpoint. apt/pip/urllib/urllib3 all natively honour an HTTP forward
# proxy (http://host:port) but not a raw socks5:// URL, so this starts the
# socks2http bridge on loopback, points the *_proxy vars at it, runs the
# command, then stops the bridge.
#
# usage:
# sh proxyrun.sh <socks5://host:port> -- <command> [args...]
# sh proxyrun.sh "" -- <command> # upstream empty: run directly (no proxy)
set -eu
UP="${1:-}"; shift
[ "${1:-}" = "--" ] && shift
PORT="${PROXYRUN_PORT:-12808}"
LOG=/tmp/socks2http.$$.log
# No proxy needed: run the command directly.
if [ -z "$UP" ]; then
"$@"
exit $?
fi
# Start the bridge (output to a file so it does not hold the RUN's stdout pipe). The bridge script sits
# beside this one (both at the build-context root, so /src/socks2http.py in the image).
BRIDGE="$(dirname "$0")/socks2http.py"
python3 "$BRIDGE" --listen "127.0.0.1:$PORT" --upstream "$UP" >"$LOG" 2>&1 &
SPID=$!
# Wait for it to accept connections (up to ~15 s).
i=0
while ! python3 -c "import socket,sys; socket.create_connection(('127.0.0.1',$PORT),1).close()" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 30 ]; then
echo "proxyrun: socks2http did not start; log:" >&2
cat "$LOG" >&2 || true
kill "$SPID" 2>/dev/null || true
exit 1
fi
sleep 0.5
done
export http_proxy="http://127.0.0.1:$PORT"
export https_proxy="http://127.0.0.1:$PORT"
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export no_proxy="127.0.0.1,localhost"
export NO_PROXY="127.0.0.1,localhost"
rc=0
"$@" || rc=$?
kill "$SPID" 2>/dev/null || true
wait "$SPID" 2>/dev/null || true
rm -f "$LOG"
exit "$rc"
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""socks2http: a minimal HTTP forward proxy that tunnels to a SOCKS5 upstream.
Purpose: the Strata Docker build. The build container may not reach the public
internet directly, but may reach a SOCKS5 endpoint (e.g. socks5://myproxy:1080).
Standard clients in the build (apt, pip, Python's urllib) all natively honour an
HTTP/HTTPS *forward proxy* named in http_proxy / https_proxy - but they do NOT
natively speak a raw SOCKS5 URL. This process is that missing bridge:
apt / pip / urllib --http_proxy--> socks2http (127.0.0.1:PORT) --socks5--> upstream
Run it on loopback during a `docker build` RUN that does network I/O:
python3 socks2http.py --listen 127.0.0.1:12808 --upstream socks5://myproxy:1080 &
export http_proxy=http://127.0.0.1:12808 https_proxy=http://127.0.0.1:12808
... run the network command ...
kill %1
If --upstream is empty it runs as a plain TCP forward proxy (CONNECT still
tunnels, but the egress is a direct socket) - handy for testing / passthrough.
Standard library only. Binds to loopback by default; it does no authentication
of its own (it is only reachable on 127.0.0.1 inside the build container).
"""
from __future__ import annotations
import argparse
import socket
import socketserver
import sys
import threading
def parse_upstream(spec: str):
"""socks5://[user:pass@]host:port -> (host, port, user, pass). '' -> None (passthrough)."""
spec = (spec or "").strip()
if not spec:
return None
if "://" not in spec:
spec = "socks5://" + spec
scheme, rest = spec.split("://", 1)
if scheme not in ("socks5", "socks5h", "socks"):
raise SystemExit(f"socks2http: unsupported upstream scheme '{scheme}' (use socks5://host:port)")
user = pw = None
if "@" in rest:
auth, rest = rest.rsplit("@", 1)
if ":" in auth:
user, pw = auth.split(":", 1)
else:
user = auth
host, _, port = rest.rpartition(":")
if not host or not port.isdigit():
raise SystemExit(f"socks2http: bad upstream '{spec}' (want socks5://host:port)")
return host, int(port), user, pw
def socks5_connect(dest_host: str, dest_port: int, up) -> socket.socket:
"""Open a TCP socket to dest_host:dest_port through the SOCKS5 upstream (remote DNS)."""
up_host, up_port, user, pw = up
s = socket.create_connection((up_host, up_port), timeout=30)
try:
# greeting: VER 5, one method. 0x00 = no-auth, 0x02 = username/password.
methods = b"\x02" if user else b"\x00"
s.sendall(b"\x05\x01" + methods)
ver, rep = s.recv(2)
if ver != 5 or rep != 0:
raise OSError(f"SOCKS5 greeting rejected: {ver!r} {rep!r}")
if user:
u = user.encode(); p = (pw or "").encode()
s.sendall(b"\x01" + bytes([len(u)]) + u + bytes([len(p)]) + p)
ver, rep = s.recv(2)
if ver != 1 or rep != 0:
raise OSError(f"SOCKS5 auth rejected: {ver!r} {rep!r}")
# request: VER 5, CMD 1 (connect), RSV 0, ATYP 3 (domain, remote resolve)
h = dest_host.encode("idna") if any(ord(c) > 127 for c in dest_host) else dest_host.encode()
if len(h) > 255:
raise OSError("dest hostname too long for SOCKS5")
req = b"\x05\x01\x00\x03" + bytes([len(h)]) + h + dest_port.to_bytes(2, "big")
s.sendall(req)
resp = s.recv(4)
if len(resp) < 4:
raise OSError("short SOCKS5 response")
if resp[1] != 0:
raise OSError(f"SOCKS5 connect failed, rep={resp[1]}")
atyp = resp[3]
if atyp == 1:
s.recv(4)
elif atyp == 3:
s.recv(1 + s.recv(1)[0])
elif atyp == 4:
s.recv(16)
s.recv(2) # bound port
return s
except Exception:
s.close()
raise
def _copy(a: socket.socket, b: socket.socket) -> None:
"""Copy from socket a to socket b until EOF/error. One direction only."""
try:
while True:
data = a.recv(65536)
if not data:
break
b.sendall(data)
except OSError:
pass
finally:
# Half-close the write side of b so the peer sees EOF, without tearing
# down the socket the other direction is still using.
try:
b.shutdown(socket.SHUT_WR)
except OSError:
pass
def _relay(a: socket.socket, b: socket.socket) -> None:
"""Relay traffic between two sockets in both directions.
Returns when both directions have reached EOF / error. Closes both sockets
only after the relay is fully done (unlike a naive per-direction cleanup,
which would kill the upstream before its response is read back).
"""
t = threading.Thread(target=_copy, args=(b, a), daemon=True)
t.start()
_copy(a, b)
t.join(timeout=30)
for sk in (a, b):
try:
sk.shutdown(socket.SHUT_RDWR)
except OSError:
pass
try:
sk.close()
except OSError:
pass
class _Handler(socketserver.BaseRequestHandler):
def handle(self) -> None:
req = self.request
up = self.server.upstream
try:
# Read the request head (first blank line).
head = b""
while b"\r\n\r\n" not in head:
chunk = req.recv(65536)
if not chunk:
return
head += chunk
if len(head) > 1 << 20:
return
first = head.split(b"\r\n", 1)[0].decode("latin-1")
parts = first.split()
if len(parts) < 3:
return
if parts[0] == "CONNECT":
# CONNECT host:port HTTP/1.1
host, _, port = parts[1].rpartition(":")
port = int(port or 443)
if up:
egress = socks5_connect(host, port, up)
else:
egress = socket.create_connection((host, port), timeout=30)
req.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
_relay(req, egress)
return
# Plain HTTP proxy: GET/POST/HEAD ... absolute-URI ...
target = parts[1]
if target.startswith("http://") or target.startswith("https://"):
scheme, rest = target.split("://", 1)
hostport, _, path = rest.partition("/")
if ":" in hostport:
host, _, port = hostport.rpartition(":")
port = int(port)
else:
host, port = hostport, (443 if scheme == "https" else 80)
if up:
egress = socks5_connect(host, port, up)
else:
egress = socket.create_connection((host, port), timeout=30)
# Rewrite to origin-form and forward the rest of the head as-is.
path = "/" + path
rewritten = f"{parts[0]} {path} {parts[2]}\r\n".encode("latin-1")
body = head.split(b"\r\n", 1)[1]
egress.sendall(rewritten + body)
_relay(req, egress)
return
self.request.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
except (OSError, ValueError) as e:
try:
self.request.sendall(f"HTTP/1.1 502 Bad Gateway\r\n\r\n{e}\r\n".encode())
except OSError:
pass
finally:
try:
self.request.close()
except OSError:
pass
class _Server(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
upstream = None
def main() -> int:
ap = argparse.ArgumentParser(description="SOCKS5 -> HTTP forward proxy (loopback)")
ap.add_argument("--listen", default="127.0.0.1:12808", help="bind addr:port (default 127.0.0.1:12808)")
ap.add_argument("--upstream", default="",
help="socks5://host:port, or empty for a plain TCP forward proxy")
a = ap.parse_args()
host, _, port = a.listen.rpartition(":")
host = host or "127.0.0.1"
port = int(port)
if not (0 < port < 65536):
raise SystemExit(f"socks2http: --listen port {port} out of range (1-65535)")
_Server.upstream = parse_upstream(a.upstream)
srv = _Server((host, port), _Handler)
sys.stderr.write(f"socks2http: listening on {host}:{port}, upstream="
f"{a.upstream or '(passthrough)'}\n")
sys.stderr.flush()
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())