From b89369cca13c3770f7da1afbaf328798fdb023e9 Mon Sep 17 00:00:00 2001 From: Hermes-1 Date: Wed, 28 Oct 2026 11:15:36 +0300 Subject: [PATCH] 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://: 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. --- .gitignore | 2 + Dockerfile.v100 | 29 +++--- proxyrun.sh | 57 ++++++++++++ socks2http.py | 236 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 311 insertions(+), 13 deletions(-) create mode 100644 .gitignore create mode 100755 proxyrun.sh create mode 100644 socks2http.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/Dockerfile.v100 b/Dockerfile.v100 index ed045c4..9818b6b 100644 --- a/Dockerfile.v100 +++ b/Dockerfile.v100 @@ -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://: 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://: +# 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 \ diff --git a/proxyrun.sh b/proxyrun.sh new file mode 100755 index 0000000..d6b587b --- /dev/null +++ b/proxyrun.sh @@ -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 -- [args...] +# sh proxyrun.sh "" -- # 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" diff --git a/socks2http.py b/socks2http.py new file mode 100644 index 0000000..1df2cbf --- /dev/null +++ b/socks2http.py @@ -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())