b55c0cb7bb
- 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/寝室部署备注)
147 lines
4.6 KiB
Python
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())
|