V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
GenServer
V2EX  ›  Go 编程语言

把服务器的 nginx 流量转发到 whistle 抓包工具,解决前后端沟通成本,友好工作

  •  
  •   GenServer · 5 小时 52 分钟前 · 347 次点击

    先上抓包 WebUI 图

    嗯很好抓到了 V2EX 的一个 404 的 bug ,把 webui 配置一个域名,然后往工作群一发,前端和 app 自己玩去。

    部署流程

    1. 编译 go_middle 代码,上传到服务器启动 go_middle
    2. 安装 Whistle 并启动
    3. 配置 nginx

    1. 直接上 go 代码

    package main
    
    import (
    	"bufio"
    	"bytes"
    	"crypto/tls"
    	"fmt"
    	"io"
    	"log"
    	"net"
    	"net/http"
    	"strings"
    )
    
    var whistleAddr = "127.0.0.1:8899" // Whistle 监听的代理端口
    
    func main() {
    	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    		targetHost := r.Header.Get("X-Target-Host")
    		if targetHost == "" {
    			http.Error(w, "Missing X-Target-Host header", http.StatusBadRequest)
    			return
    		}
    
    		if strings.HasSuffix(targetHost, ":443") {
    			handleTLSProxy(w, r, targetHost)
    			return
    		}
    
    		if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
    			// ws:// 直接 HTTP 请求升级
    			handleWebSocketProxy(w, r, targetHost)
    			return
    		}
    		// 普通 HTTP 请求
    		handleHTTP(w, r, targetHost)
    	})
    
    	log.Println("Proxy wrapper (for Whistle) listening on :8082")
    	log.Fatal( http.ListenAndServe(":8082", nil))
    }
    
    func handleHTTP(w http.ResponseWriter, r *http.Request, targetHost string) {
    	// 建立到 whistle 的连接
    	fmt.Println("Handling HTTP request for target:", targetHost)
    	conn, err := net.Dial("tcp", whistleAddr)
    	if err != nil {
    		http.Error(w, "Failed to connect to whistle: "+err.Error(), http.StatusBadGateway)
    		return
    	}
    	defer conn.Close()
    
    	// 构造代理协议的完整 URL
    	fullURL := fmt.Sprintf("http://%s%s", targetHost, r.URL.RequestURI())
    
    	// 创建新的请求
    	req, err := http.NewRequest(r.Method, fullURL, r.Body)
    	if err != nil {
    		http.Error(w, "Failed to create proxy request: "+err.Error(), http.StatusInternalServerError)
    		return
    	}
    
    	req.Header = r.Header.Clone()
    
    	// 写入代理请求
    	if err := req.Write(conn); err != nil {
    		http.Error(w, "Failed to write proxy request to whistle: "+err.Error(), http.StatusBadGateway)
    		return
    	}
    
    	// 从 whistle 读取响应
    	resp, err := http.ReadResponse(bufio.NewReader(conn), req)
    	if err != nil {
    		http.Error(w, "Failed to read response from whistle: "+err.Error(), http.StatusBadGateway)
    		return
    	}
    	defer resp.Body.Close()
    
    	// 转发响应头和状态码
    	for k, v := range resp.Header {
    		for _, vv := range v {
    			w.Header().Add(k, vv)
    		}
    	}
    	w.WriteHeader(resp.StatusCode)
    
    	// 转发响应体
    	io.Copy(w, resp.Body)
    }
    func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, targetHost string) {
    	hj, ok := w.( http.Hijacker)
    	if !ok {
    		http.Error(w, "Hijack not supported", http.StatusInternalServerError)
    		return
    	}
    
    	clientConn, _, err := hj.Hijack()
    	if err != nil {
    		http.Error(w, "Hijack failed: "+err.Error(), http.StatusInternalServerError)
    		return
    	}
    
    	backendConn, err := net.Dial("tcp", whistleAddr)
    	if err != nil {
    		clientConn.Close()
    		return
    	}
    
    	// 构造完整的 websocket 请求(代理格式)
    	var buf bytes.Buffer
    	fmt.Fprintf(&buf, "GET http://%s%s HTTP/1.1\r\n", targetHost, r.URL.RequestURI())
    	fmt.Fprintf(&buf, "Host: %s\r\n", targetHost)
    	fmt.Fprintf(&buf, "Connection: Upgrade\r\n")
    	fmt.Fprintf(&buf, "Upgrade: websocket\r\n")
    
    	for key, values := range r.Header {
    		if strings.EqualFold(key, "Host") || strings.EqualFold(key, "Connection") || strings.EqualFold(key, "Upgrade") {
    			continue
    		}
    		for _, v := range values {
    			fmt.Fprintf(&buf, "%s: %s\r\n", key, v)
    		}
    	}
    	buf.WriteString("\r\n")
    
    	// 直接转发请求内容
    	_, err = backendConn.Write(buf.Bytes())
    	if err != nil {
    		clientConn.Close()
    		backendConn.Close()
    		return
    	}
    	// 👇 直接把返回内容透传回客户端(不要使用 http.ReadResponse 解析,否则后续帧会丢)
    	go io.Copy(backendConn, clientConn)
    	io.Copy(clientConn, backendConn)
    }
    func handleTLSProxy(w http.ResponseWriter, r *http.Request, targetHost string) {
    	hj, ok := w.( http.Hijacker)
    	if !ok {
    		http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
    		return
    	}
    
    	clientConn, _, err := hj.Hijack()
    	if err != nil {
    		http.Error(w, "Hijack failed: "+err.Error(), http.StatusInternalServerError)
    		return
    	}
    
    	// 中间件连接 whistle ,发送 CONNECT 建立隧道
    	backendConn, err := net.Dial("tcp", whistleAddr)
    	if err != nil {
    		clientConn.Close()
    		return
    	}
    
    	// 发起 CONNECT 请求
    	connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetHost, targetHost)
    	_, err = backendConn.Write([]byte(connectReq))
    	if err != nil {
    		http.Error(w, "backendConn CONNECT failed: "+err.Error(), http.StatusInternalServerError)
    		clientConn.Close()
    		backendConn.Close()
    		return
    	}
    
    	// 读取 CONNECT 响应
    	resp, err := http.ReadResponse(bufio.NewReader(backendConn), r)
    	if err != nil || resp.StatusCode != 200 {
    		clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
    		clientConn.Close()
    		backendConn.Close()
    		return
    	}
    	// 🔐 2. TLS 握手
    	tlsConn := tls.Client(backendConn, &tls.Config{
    		ServerName: strings.Split(targetHost, ":")[0], // SNI 用于证书校验
    	})
    	if err := tlsConn.Handshake(); err != nil {
    		clientConn.Close()
    		backendConn.Close()
    		return
    	}
    
    	// 构造完整的 websocket 请求(代理格式)
    	var buf bytes.Buffer
    	fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", r.Method, r.URL.RequestURI())
    	fmt.Fprintf(&buf, "Host: %s\r\n", targetHost)
    	for key, values := range r.Header {
    		if strings.EqualFold(key, "Host") {
    			continue
    		}
    		for _, v := range values {
    			fmt.Fprintf(&buf, "%s: %s\r\n", key, v)
    		}
    	}
    	buf.WriteString("\r\n")
    	// 直接转发请求内容
    	_, err = tlsConn.Write(buf.Bytes())
    	if err != nil {
    		clientConn.Close()
    		tlsConn.Close()
    		return
    	}
    	// 🔄 3. 开始转发数据( WebSocket over TLS )
    	go io.Copy(tlsConn, clientConn)
    	io.Copy(clientConn, tlsConn)
    }
    
    

    交叉编译命令:GOOS=linux GOARCH=amd64 go build -o go_middle main.go 编译命令需要根据自己服务器进行调整我这里是 centos7 注意:go_middle 和 Whistle 要配置在一台机器上,如果需要配置到不同的服务器请修改代码中的whistleAddr

    2.安装 Whistle (忽略直接 google )

    启动命令: w2 start 自己有域名可以这样配置: w2 start -H 0.0.0.0 -l w2.xxx.com

    3.配置 nginx

    # go_middle 的地址
    upstream go_middle{server 192.168.31.148:8082}
    server{
        server_name wchat.dsmai.com
        localtion / {
           proxy_pass http://go_middle
           # 内网真实服务器的地址
           proxy_set_header X-Target-Host 192.168.31.1:80
        }
    }
    
    5 条回复    2025-06-26 21:18:06 +08:00
    NouveauNom
        1
    NouveauNom  
       5 小时 42 分钟前
    一直开着内存会不会占用很高
    GenServer
        2
    GenServer  
    OP
       5 小时 39 分钟前
    @NouveauNom 测试服基本没问题,因为只是针对某些具体域名。反正挺好用的也不用他们自己安装证书配置代理
    lululau
        3
    lululau  
       5 小时 1 分钟前
    通常来说 Nginx 和后端之间是 Plain HTTP ,所以这个事情完全可以通过 tcpdump | wireshark 来完成:

    ssh my.server "tcpdump -i any -w - -U port 18080" | wireshark -k -i -
    GenServer
        4
    GenServer  
    OP
       4 小时 28 分钟前
    @lululau 主要是可视化和友好问题,whistle 可以添加规则,可视化的重试,修改返回数据包等。调试还是挺好用的
    relife
        5
    relife  
       39 分钟前
    挺好
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2865 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 13:57 · PVG 21:57 · LAX 06:57 · JFK 09:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.