
0. 背景 : 流量特征分析与合规预警
在企业级网络策略中,标准版 frp 流量因具备明显的协议指纹,极易被防火墙或 DPI(深度包检测)设备识别并阻断。主要特征包括:
- TLS 指纹:固定的 ClientHello 首字节(0x17)及特定加密套件顺序。
- 明文特征:控制信道中包含
privilege_key等明文 JSON 字段。 - 行为特征:固定频率(默认 30s)且包大小恒定的心跳包。
本文基于 frp v0.65.0 源码,通过协议头注入、TLS 指纹模拟、载荷混淆及心跳随机化四个维度,将 frp 流量伪装为普通 HTTPS 浏览器访问行为。
⚠️ 核心警告
- 不兼容性:本文涉及的修改会导致客户端(frpc)与原版服务端(frps)无法互通,必须两端同时编译部署。
- 法律风险:此类修改属于网络防御规避(Defense Evasion)。在未经授权的网络环境中使用可能违反法律或公司合规政策,请仅用于安全测试或科研环境。
1. 协议头注入 : 伪装 HTTP 握手
原理:在 TCP 连接建立后、TLS 握手前,插入 HTTP 请求交互。防火墙会先识别到合法的 HTTP 流量(模拟访问百度),从而放行后续流量。
1.1 客户端修改
文件路径:client/connector.go
逻辑:在 realConnect 方法中,TCP 连接建立后立即发送 HTTP GET 请求,并校验服务端返回的 200 OK。
if protocol == "tcp" {
// 1. 发送伪造 HTTP 请求头 (模拟访问白名单域名)
fakeHeader := "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n"
if _, err := conn.Write([]byte(fakeHeader)); err != nil {
conn.Close()
return nil, fmt.Errorf("write fake header error: %v", err)
}
// 2. 读取并校验服务端响应
buf := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf)
conn.SetReadDeadline(time.Time{}) // 重置超时
if err != nil {
conn.Close()
return nil, fmt.Errorf("read fake response error: %v", err)
}
// 必须包含 200 OK 才能继续
if !strings.Contains(string(buf[:n]), "200 OK") {
conn.Close()
return nil, fmt.Errorf("invalid fake response")
}
// ... 后续标准 TLS 握手逻辑 ...
}
1.2 服务端修改
文件路径:server/service.go
逻辑:引入中间层 FakeHandshakeListener,拦截并“吞掉”伪造的 HTTP 流量,仅将后续真实流量透传给 frp 逻辑。
// 自定义 Listener 包装器
type FakeHandshakeListener struct {
net.Listener
}
func (l *FakeHandshakeListener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil { return nil, err }
return &FakeHandshakeConn{Conn: c}, nil
}
// 自定义 Conn 包装器
type FakeHandshakeConn struct {
net.Conn
handshakeDone bool
}
func (c *FakeHandshakeConn) Read(b []byte) (n int, err error) {
// 握手完成后直接透传
if c.handshakeDone {
return c.Conn.Read(b)
}
// 1. 读取客户端伪造头
buf := make([]byte, 1024)
n, err = c.Conn.Read(buf)
if err != nil { return 0, err }
data := string(buf[:n])
// 2. 校验特征 (Host: www.baidu.com)
if strings.Contains(data, "Host: www.baidu.com") {
// 3. 回复伪造 200 OK
_, _ = c.Conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"))
c.handshakeDone = true
return 0, nil // 返回 0 字节,向上层隐藏此次交互
}
c.Conn.Close()
return 0, fmt.Errorf("invalid handshake")
}
2. TLS 指纹伪装 : 模拟 Chrome
原理:Go 标准库 crypto/tls 的指纹特征固定。使用 utls 库完全模拟 Chrome 浏览器的 JA3 指纹,并移除 frp 特有的魔数。
2.1 引入 utls 依赖
go get github.com/refraction-networking/utls
2.2 客户端集成 utls
文件路径:client/connector.go
import utls "github.com/refraction-networking/utls"
// ... 在 realConnect 中替换原有 TLS 逻辑 ...
tlsEnable := true // 强制开启 TLS
if tlsConfig != nil {
uConfig := &utls.Config{
ServerName: tlsConfig.ServerName,
InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
RootCAs: tlsConfig.RootCAs,
NextProtos: tlsConfig.NextProtos,
}
// 关键:使用 HelloChrome_Auto 模拟 Chrome 指纹
uConn := utls.UClient(conn, uConfig, utls.HelloChrome_Auto)
if err := uConn.Handshake(); err != nil {
conn.Close()
return nil, err
}
conn = uConn // 替换连接对象
}
2.3 移除特征字节
文件路径:pkg/util/net/tls.go
逻辑:FRP 默认在 TLS 前发送 0x17,这是极强特征,需修改为标准 TLS ClientHello 首字节 0x16。
// var FRPTLSHeadByte = 0x17 // 原版
var FRPTLSHeadByte = 0x16 // 修改后
注:需同步注释掉 pkg/util/net/dial.go 中的 DialHookCustomTLSHeadByte 写入逻辑。
3. 载荷特征混淆 : JSON 字段简化
原理:缩短并混淆 JSON 字段名,规避针对 privilege_key 等关键词的正则匹配。
文件路径:pkg/msg/msg.go
type Login struct {
Version string `json:"v,omitempty"` // version -> v
Hostname string `json:"h,omitempty"` // hostname -> h
PrivilegeKey string `json:"pk,omitempty"` // privilege_key -> pk
Timestamp int64 `json:"ts,omitempty"` // timestamp -> ts
RunID string `json:"ri,omitempty"` // run_id -> ri
}
type Ping struct {
PrivilegeKey string `json:"pk,omitempty"`
Timestamp int64 `json:"ts,omitempty"`
Padding string `json:"p,omitempty"` // 新增填充字段
}
4. 心跳逻辑重构 : 随机化与抗分析
原理:打破固定时间间隔和包大小的统计特征。
文件路径:client/control.go
修改点:
- 立即启动:连接建立后立即发送首个心跳。
- 随机抖动:心跳间隔增加 ±20% 随机值。
- 垃圾填充:在
Padding字段填充随机长度字符。
go func() {
// 1. 立即发送首个心跳
if _, err := sendHeartBeat(); err != nil {
xl.Warnf("send first heartbeat error: %v", err)
}
for {
// 2. 计算基准时间 + 随机抖动 (30s ± 20%)
baseInterval := time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second
jitter := time.Duration(rand.Intn(int(baseInterval)/5*2) - int(baseInterval)/5)
select {
case <-time.After(baseInterval + jitter):
// 3. 发送带 Padding 的心跳
if _, err := sendHeartBeat(); err != nil {
xl.Warnf("send heartbeat error: %v", err)
}
case <-ctl.doneCh:
return
}
}
}()
5. 构建与部署 : 编译配置
5.1 编译命令 (PowerShell)
必须禁用 CGO 以确保静态链接。
# Linux 服务端/客户端
$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"
go build -ldflags "-s -w" -o frps ./cmd/frps
go build -ldflags "-s -w" -o frpc_linux ./cmd/frpc
# Windows 客户端
$env:CGO_ENABLED="0"; $env:GOOS="windows"; $env:GOARCH="amd64"
go build -ldflags "-s -w" -o frpc.exe ./cmd/frpc
5.2 配置文件关键项
| 配置文件 | 参数项 | 建议值 | 说明 |
|---|---|---|---|
| frps.ini | tls_only | true | 强制走修改后的 TLS 逻辑 |
| frps.ini | heartbeat_timeout | 90 | 放宽超时时间以适应随机心跳 |
| frpc.ini | tls_enable | true | 客户端显式开启 TLS |
| frpc.ini | heartbeat_interval | 30 | 设置基准值触发随机逻辑 |
6. 验证 : 流量表现与局限性
完成修改并部署后,预期效果如下:
- Wireshark 抓包:
- 连接初期显示为标准的
HTTP GET Host: www.baidu.com及200 OK。 - 后续 TLS ClientHello 与 Chrome 浏览器完全一致(无
0x17前缀)。 - 载荷全加密,无明文 JSON 关键字。
- 连接初期显示为标准的
- 流量统计:心跳包大小动态变化,时间间隔无明显周期性。
局限性说明:
尽管上述手段消除了协议指纹,但在高安全等级环境中(如 H800 计算集群),高级的 UEBA(用户实体行为分析) 仍可能通过以下特征发现异常:
- 非工作时间的持续长连接。
- 单 IP 出现异常的大流量吞吐。
- 目标服务器 IP 信誉度低(如家用宽带 IP)。

发表回复