Files
lou b55c0cb7bb 开源化: MIT LICENSE + 详细 README + deploy/watchdog + 全仓脱敏(云服IP/学校域/凭据 → RFC5737 占位)
- LICENSE: MIT © 2026 edgevoid
- README: 三层架构/逐字段伪装对照/部署/验证/安全边界/看门狗自启
- 脱敏(filter-repo 全历史): 8.136.202.225→203.0.113.10, .zjipc.cn→.internal.example, 学校DNS→192.0.2.53
- hook.sh 更新为安全形态(FORWARD-only 绑 WAN, 去掉 OUTPUT 全挂断网形态)
- router_deploy.sh: SSHPASS 改环境变量注入(明文密码清除)
- watchdog.sh 纳入 deploy/(接口变量化)
- 内部语境泛化(认证网关/学校DNS/寝室部署备注)
2026-09-09 18:14:10 +08:00

147 lines
4.6 KiB
Python

"""L2 本地 HTTP 透明代理(软路由侧, 监听 8080)。
iptables 把 LAN 出向 dport 80 的明文 HTTP REDIRECT 到这里:
1. recv 请求(头 + Content-Length body)
2. 原始字节打包走通道(云端负责 https 套壳)
3. 响应原样写回客户端后关闭连接(每个请求一条连接)
连接断/通道错误 -> 502。目标为内网段的流量被 hook 规则排除,
不会到本代理。
"""
from __future__ import annotations
import socket
import sys
import threading
from typing import Any
from .client import ChannelClient, ChannelError
from .httpmsg import parse_request_head
LISTEN_BACKLOG = 128
MAX_HEAD = 64 * 1024
class HttpTransparentProxy:
def __init__(self, client: ChannelClient, listen_port: int) -> None:
if listen_port < 1 or listen_port > 65535:
raise ValueError(f"bad listen port: {listen_port}")
self.client = client
self.port = listen_port
self.served = 0
self.failed = 0
def serve(self) -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", self.port))
srv.listen(LISTEN_BACKLOG)
print(f"[http-proxy] listen 0.0.0.0:{self.port}", flush=True)
while True:
conn, _addr = srv.accept()
threading.Thread(
target=self._handle,
args=(conn,),
name="http-proxy-conn",
daemon=True,
).start()
def _recv_request(self, conn: socket.socket) -> bytes | None:
buf = bytearray()
while len(buf) < MAX_HEAD:
chunk = conn.recv(8192)
if not chunk:
return None
buf += chunk
parsed = parse_request_head(bytes(buf))
if parsed is None:
continue
_req, head_len = parsed
content_length = 0
for key, value in _req.headers:
if key.lower() == "content-length":
try:
content_length = int(value)
except ValueError:
content_length = 0
total = head_len + content_length
if len(buf) >= total:
return bytes(buf[:total])
if content_length > MAX_HEAD:
return None
return None
def _handle(self, conn: socket.socket) -> None:
try:
conn.settimeout(30)
raw = self._recv_request(conn)
if raw is None:
return
resp = self._channel_send(raw)
if resp is None:
self._write_502(conn)
self.failed += 1
return
conn.sendall(resp)
self.served += 1
except OSError:
self.failed += 1
finally:
try:
conn.close()
except OSError:
pass
def _channel_send(self, raw: bytes) -> bytes | None:
try:
return self.client.send_http_request(raw)
except ChannelError:
try:
self.client.ensure_connected()
return self.client.send_http_request(raw)
except ChannelError:
return None
def _write_502(self, conn: socket.socket) -> None:
body = b"<html><body><h1>502 Bad Gateway</h1>"
body += b"<p>channel unavailable</p></body></html>"
head = (
b"HTTP/1.1 502 Bad Gateway\r\n"
b"Content-Type: text/html; charset=utf-8\r\n"
+ f"Content-Length: {len(body)}\r\n".encode("ascii")
+ b"Connection: close\r\n\r\n"
)
try:
conn.sendall(head + body)
except OSError:
pass
def run_http_proxy(cfg: dict[str, Any]) -> None:
cloud: dict[str, Any] = cfg["cloud"]
proxy_cfg: dict[str, Any] = cfg["http_proxy"]
client = ChannelClient(
host=str(cloud["host"]),
port=int(cloud["port"]),
token=str(cloud["token"]),
ca_path=str(cloud.get("ca_path", "")),
heartbeat=float(cfg.get("heartbeat", 30)),
)
client.connect()
HttpTransparentProxy(client, int(proxy_cfg["listen_port"])).serve()
def main() -> int:
if len(sys.argv) < 2:
print("usage: python -m channel.proxy_http <config.json>", file=sys.stderr)
return 2
import json
from pathlib import Path
cfg = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
run_http_proxy(cfg)
return 0
if __name__ == "__main__":
raise SystemExit(main())