starnet/proxy_local_helpers_test.go
starainrt 732e81316c
fix(starnet): 重构请求执行链路并补齐代理/重试/trace边界
- 分离 Request 的配置态与执行态,修复二次 Do、raw 模式网络配置失效和 body 来源互斥问题
  - 新增 starnet trace 抽象,补齐 DNS/连接/TLS/重试事件,并优化动态 transport 缓存与代理解析路径
  - 收紧非法代理为 fail-fast,多目标目标回退仅限幂等请求,修复 Host/TLS/SNI 等语义边界
  - 补充防御性拷贝、专项回归测试、本地代理/TLS 用例与 README 行为说明
2026-04-19 15:39:51 +08:00

332 lines
6.9 KiB
Go

package starnet
import (
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
type connectProxyServer struct {
*httptest.Server
mu sync.Mutex
targets []string
}
func newIPv4Server(t testing.TB, handler http.Handler) *httptest.Server {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen tcp4: %v", err)
}
server := httptest.NewUnstartedServer(handler)
server.Listener = listener
server.Start()
return server
}
func newIPv4TLSServer(t testing.TB, handler http.Handler) *httptest.Server {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen tcp4: %v", err)
}
server := httptest.NewUnstartedServer(handler)
server.Listener = listener
server.StartTLS()
return server
}
func newTrustedIPv4TLSServer(t testing.TB, dnsName string, handler http.Handler) (*httptest.Server, *x509.CertPool) {
t.Helper()
testT, ok := t.(*testing.T)
if !ok {
t.Fatal("newTrustedIPv4TLSServer requires *testing.T")
}
certPEM, keyPEM := genSelfSignedCertPEM(testT, dnsName)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf("X509KeyPair: %v", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(certPEM) {
t.Fatal("AppendCertsFromPEM returned false")
}
server := httptest.NewUnstartedServer(handler)
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen tcp4: %v", err)
}
server.Listener = listener
server.TLS = &tls.Config{
Certificates: []tls.Certificate{cert},
}
server.StartTLS()
return server, pool
}
func httpsURLForHost(t testing.TB, server *httptest.Server, host string) string {
t.Helper()
_, port, err := net.SplitHostPort(server.Listener.Addr().String())
if err != nil {
t.Fatalf("split host port: %v", err)
}
return fmt.Sprintf("https://%s:%s", host, port)
}
func newIPv4ConnectProxyServer(t testing.TB, dialTarget func(target string) (net.Conn, error)) *connectProxyServer {
t.Helper()
proxy := &connectProxyServer{}
if dialTarget == nil {
dialTarget = func(target string) (net.Conn, error) {
return net.Dial("tcp", target)
}
}
proxy.Server = newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodConnect {
http.Error(w, "connect required", http.StatusMethodNotAllowed)
return
}
proxy.mu.Lock()
proxy.targets = append(proxy.targets, r.Host)
proxy.mu.Unlock()
targetConn, err := dialTarget(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
hijacker, ok := w.(http.Hijacker)
if !ok {
targetConn.Close()
t.Fatal("proxy response writer is not a hijacker")
}
clientConn, rw, err := hijacker.Hijack()
if err != nil {
targetConn.Close()
t.Fatalf("hijack proxy conn: %v", err)
}
if _, err := rw.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
clientConn.Close()
targetConn.Close()
t.Fatalf("write connect response: %v", err)
}
if err := rw.Flush(); err != nil {
clientConn.Close()
targetConn.Close()
t.Fatalf("flush connect response: %v", err)
}
relayProxyConns(clientConn, targetConn)
}))
return proxy
}
func (p *connectProxyServer) Targets() []string {
p.mu.Lock()
defer p.mu.Unlock()
return append([]string(nil), p.targets...)
}
type socks5ProxyServer struct {
ln net.Listener
addr string
dial func(target string) (net.Conn, error)
stopCh chan struct{}
wg sync.WaitGroup
mu sync.Mutex
targets []string
}
func newSOCKS5ProxyServer(t testing.TB, dialTarget func(target string) (net.Conn, error)) *socks5ProxyServer {
t.Helper()
if dialTarget == nil {
dialTarget = func(target string) (net.Conn, error) {
return net.Dial("tcp", target)
}
}
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen tcp4 socks5: %v", err)
}
proxy := &socks5ProxyServer{
ln: ln,
addr: ln.Addr().String(),
dial: dialTarget,
stopCh: make(chan struct{}),
}
proxy.wg.Add(1)
go func() {
defer proxy.wg.Done()
for {
conn, err := ln.Accept()
if err != nil {
select {
case <-proxy.stopCh:
return
default:
return
}
}
proxy.wg.Add(1)
go func(c net.Conn) {
defer proxy.wg.Done()
proxy.handleConn(t, c)
}(conn)
}
}()
return proxy
}
func (p *socks5ProxyServer) URL() string {
return "socks5://" + p.addr
}
func (p *socks5ProxyServer) Targets() []string {
p.mu.Lock()
defer p.mu.Unlock()
return append([]string(nil), p.targets...)
}
func (p *socks5ProxyServer) Close() {
close(p.stopCh)
_ = p.ln.Close()
p.wg.Wait()
}
func (p *socks5ProxyServer) handleConn(t testing.TB, conn net.Conn) {
t.Helper()
closeConn := true
defer func() {
if closeConn {
_ = conn.Close()
}
}()
header := make([]byte, 2)
if _, err := io.ReadFull(conn, header); err != nil {
return
}
if header[0] != 0x05 {
return
}
methods := make([]byte, int(header[1]))
if _, err := io.ReadFull(conn, methods); err != nil {
return
}
if _, err := conn.Write([]byte{0x05, 0x00}); err != nil {
return
}
reqHeader := make([]byte, 4)
if _, err := io.ReadFull(conn, reqHeader); err != nil {
return
}
if reqHeader[0] != 0x05 || reqHeader[1] != 0x01 {
_, _ = conn.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
host, err := readSOCKS5Addr(conn, reqHeader[3])
if err != nil {
_, _ = conn.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
portBytes := make([]byte, 2)
if _, err := io.ReadFull(conn, portBytes); err != nil {
return
}
target := net.JoinHostPort(host, fmt.Sprintf("%d", binary.BigEndian.Uint16(portBytes)))
p.mu.Lock()
p.targets = append(p.targets, target)
p.mu.Unlock()
targetConn, err := p.dial(target)
if err != nil {
_, _ = conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
if _, err := conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
targetConn.Close()
return
}
closeConn = false
relayProxyConns(conn, targetConn)
}
func readSOCKS5Addr(r io.Reader, atyp byte) (string, error) {
switch atyp {
case 0x01:
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return net.IP(buf).String(), nil
case 0x03:
var size [1]byte
if _, err := io.ReadFull(r, size[:]); err != nil {
return "", err
}
buf := make([]byte, int(size[0]))
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
case 0x04:
buf := make([]byte, 16)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return net.IP(buf).String(), nil
default:
return "", fmt.Errorf("unsupported atyp: %d", atyp)
}
}
func relayProxyConns(left, right net.Conn) {
var once sync.Once
closeBoth := func() {
_ = left.Close()
_ = right.Close()
}
go func() {
_, _ = io.Copy(left, right)
once.Do(closeBoth)
}()
go func() {
_, _ = io.Copy(right, left)
once.Do(closeBoth)
}()
}