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

9 小时 23 分钟前
 GenServer

先上抓包 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
    }
}
410 次点击
所在节点    Go 编程语言
5 条回复
NouveauNom
9 小时 13 分钟前
一直开着内存会不会占用很高
GenServer
9 小时 10 分钟前
@NouveauNom 测试服基本没问题,因为只是针对某些具体域名。反正挺好用的也不用他们自己安装证书配置代理
lululau
8 小时 32 分钟前
通常来说 Nginx 和后端之间是 Plain HTTP ,所以这个事情完全可以通过 tcpdump | wireshark 来完成:

ssh my.server "tcpdump -i any -w - -U port 18080" | wireshark -k -i -
GenServer
7 小时 59 分钟前
@lululau 主要是可视化和友好问题,whistle 可以添加规则,可视化的重试,修改返回数据包等。调试还是挺好用的
relife
4 小时 11 分钟前
挺好

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/1141241

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX