痛点:内网服务多、公网端口贵、安全要求高?Nginx 只懂 HTTP,数据库/SSH 需要单独端口映射,还要防扫描、防渗透。
这篇实战文章从架构设计、源码解析到 Docker 部署,完整拆解一款基于 Netty4 的单端口协议嗅探 TCP 网关——一个端口搞定 HTTP、HTTPS、数据库、SSH 全流量转发,还带前置 token 鉴权,生产可用的那种。

目录

一、为什么要做单端口网关?

先抛一个运维和开发都会遇到的场景:

  • 公网 IP 只有一个,端口却要开放 Nginx(80/443)、数据库(1521/3306/5236)、SSH(22)……安全和运维成本都很高;
  • 直接暴露数据库端口到公网,等于把数据库密码送到黑客嘴边,扫描器 24 小时在打;
  • 一个协议一个端口,防火墙策略、证书、监控全都翻倍。

单端口网关(单端口多协议转发) 的思路是:公网只开放一个 TCP 端口,网关读取连接的第一个数据包做协议嗅探,然后按协议把连接转发到不同的内网后端:

检测到的协议转发目标行为
HTTP / HTTPS(TLS)内网 Nginx透明透传(不解析、不解密、不改写)
其他普通 TCP 流量(数据库、SSH、自定义协议)可配置目标 IP+端口透明透传,可选前置 magic-token 鉴权

由于网关只"嗅探 + 搬运"原始字节,它对协议零认知,所以 MySQL、PostgreSQL、Oracle、达梦、SSH、RDP、自定义二进制协议都能长连接透传。

二、项目简介与核心能力

项目 jquick-hub 是一个基于 Netty 4 的高性能协议嗅探 TCP 网关,核心特性:

  • 单端口多协议路由:首包嗅探分流 HTTP/HTTPS 与普通 TCP 流量;
  • 零拷贝透明透传:数据只搬运、不解析、不解密、不改写;
  • 长连接友好
    • 后端建连期间数据零丢包缓冲,就绪后原子刷出;
    • 任一端断开,另一端联动关闭;
    • 背压控制:对端写缓冲满时暂停本端读取(setAutoRead),内存不失控;
    • 引用计数安全,无缓冲泄漏;
  • 前置 magic-token 鉴权(针对普通 TCP 流量):
    • 定长 token 拼在首个业务包前,网关校验后剥离再转发;
    • MessageDigest.isEqual 常量时间比较,防时序侧信道;
    • 鉴权失败/超时静默关闭,不返回任何应答,防信息泄露;
    • 日志只输出掩码 前4****后2,绝不打印明文 token;
  • 优雅停机:SIGTERM/SIGINT 触发 JVM shutdown hook;
  • 可配置缓冲区泄漏检测(SIMPLE/ADVANCED/PARANOID/DISABLED);
  • 内置迷你代理客户端:Navicat、DBeaver、psql、ssh 等原生工具零改造接入。

环境要求:JDK 1.8+、Maven 3.x;Docker 部署需要 Docker 19.03+(多阶段构建)。

三、架构设计与协议识别原理

                   公网                                     内网
+----------------+  单端口      +------------------+  透传     +----------------+
|  HTTP/HTTPS    +------------->+                  +----------->+   Nginx        |
|  客户端         |              |   jquick-hub     |            |   (:52000)     |
+----------------+              |  TCP 网关        |            +----------------+
                                 |                  |
+----------------+  token+数据   |                  |  剥离token  +----------------+
| DB/SSH 工具     |              |                  |            | 其他后端        |
|    |           |  本地代理     |  (前置token鉴权)  |----------->+  (:5236)       |
|    v           +------------->+                  |            +----------------+
|  本地端口       |              +------------------+
+----------------+

每条连接的处理器管线(Pipeline):

JQuickProtocolDetectHandler → JQuickAuthPrefixHandler → JQuickTcpRelayHandler

协议探测规则ProtocolDetectHandler,只窥视不消费,getXXX 接口不改 readerIndex):

  1. HTTPS/TLS:首字节 0x16(TLS Handshake,RFC 5246)→ 1 个字节即可判定;
  2. HTTP:前缀匹配已知方法(GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH/CONNECT/TRACE),且方法名后紧跟空格 0x20,大小写不敏感;
  3. 其他流量:累积满 16 个探测字节仍不匹配任何特征 → 判定为普通 TCP 流量;
  4. 探测超时:超时后若已收到数据 → 按普通 TCP 流量转发;零数据 → 关闭连接。

这里有个很关键的细节:TCP 拆包。首包可能只收到 "GET" 三个字节(没有空格),此时不能否定 HTTP 可能,探测器会返回 UNKNOWN 继续等待,只有收到 "GET "(带空格)才确认是 HTTP——这正是很多"半吊子"嗅探器误判的根源。

四、快速开始:本地 jar 五分钟跑起来

mvn clean package
java -jar target/tcp-gateway.jar

启动日志:

  TCP 流量网关启动成功
  公网监听: 0.0.0.0:18080 (backlog=1024, so_timeout=5000ms)
  Nginx 转发目标: 127.0.0.1:52000
  普通TCP转发目标: 127.0.0.1:5236
  普通TCP鉴权: 启用 (token长度=32, 超时=5000ms)

配置文件查找优先级:

命令行参数路径 → 环境变量 GATEWAY_CONFIG → 当前目录 ./gateway.yaml → classpath 内置默认

五、配置文件详解(YAML + 环境变量)

一份带注释的完整配置:

gateway:
  listen:
    host: 0.0.0.0               # 监听网卡 IP(默认所有网卡)
    port: 18080                 # 公网监听端口
    backlog: 1024              # TCP 半连接/全连接队列
    so_timeout: 5000           # 连接超时(ms)
    probe_timeout_ms: 5000     # 首包探测等待上限(ms)
nginx:
  target:
    host: 127.0.0.1            # 内网 Nginx IP
    port: 52000                # Nginx 端口
other:
  traffic:
    target:
      host: 127.0.0.1          # 其他流量转发目标
      port: 5236
    auth:
      enable: true             # 鉴权开关
      token-prefix-len: 32     # token 定长(字节)
      timeout_ms: 5000         # 鉴权超时,静默关闭
      valid-tokens:
        - "0123456789abcdef0123456789abcdef"
netty:
  boss:
    threads: 1                 # acceptor 线程
  worker:
    threads: 0                 # I/O 线程,0 = 自动(CPU核数*2)
  buffer:
    leak:
      detection: SIMPLE        # SIMPLE | ADVANCED | PARANOID | DISABLED

默认值速查表:

配置项默认值说明
gateway.listen.host0.0.0.0监听网卡
gateway.listen.port8080公网端口
gateway.listen.backlog1024必须为正整数
gateway.listen.so_timeout5000ms
gateway.listen.probe_timeout_msso_timeout≤0 时回退 5000
nginx.target.host/port127.0.0.1/80
other.traffic.target.host/port127.0.0.1/9000
other.traffic.auth.enabletrue仅当存在 auth 段时生效;老配置无 auth 段则默认关闭(向后兼容)
other.traffic.auth.token-prefix-len32字节
other.traffic.auth.timeout_ms5000ms
netty.boss.threads1正整数
netty.worker.threads0(自动)0 = CPU 核数 × 2
netty.buffer.leak.detectionSIMPLE

启动校验:鉴权开启时,若未配置任何合法 token,或 token 长度与 token-prefix-len 不一致,程序拒绝启动;端口必须在 1–65535。

六、客户端接入:数据库工具零改造过鉴权

数据库连接工具(Navicat、DBeaver、psql)和 ssh 客户端不能自定义请求头,也无法在应用层协议里塞 token。解决方案是本地起一个迷你代理,在连接的第一个业务包前自动拼 token:

DB工具 ──连接──▶ 本地代理(监听 local.ports) ──首包前拼token──▶ 公网网关 ──剥离token──▶ 内网数据库

client/GatewayClient.java(零第三方依赖)配置 client/client.properties

local.ports=15236                              # 本地监听端口,逗号分隔可多个
gw.host=127.0.0.1                              # 网关地址
gw.port=18080                                  # 网关端口
token=0123456789abcdef0123456789abcdef         # 必须与网关 valid-token 一致

启动:

javac -encoding UTF-8 GatewayClient.java
java -Dfile.encoding=UTF-8 GatewayClient

之后数据库工具只需连接本地端口 15236,无需任何改造。命令行参数 -ports/-host/-port/-token 可覆盖配置文件。

七、Docker 容器化部署与端口映射

7.1 镜像设计原则

  • 不写 EXPOSE:EXPOSE 只是文档声明,不固化任何端口,端口完全由运行时 -p 决定;
  • 不硬编码 IP/端口:镜像内置 gateway.yaml 仅为占位默认值,全部运行时参数通过环境变量传入;
  • JRE 基础镜像:Maven+JDK 构建阶段产出 fat-jar,运行时使用 Eclipse Temurin JRE,镜像更小更安全;
  • 优先级:环境变量 > YAML > 代码默认值。

7.2 环境变量覆盖清单

环境变量覆盖配置默认值
JQUICK_LISTEN_HOSTgateway.listen.host0.0.0.0
JQUICK_LISTEN_PORTgateway.listen.port8080
JQUICK_LISTEN_BACKLOGgateway.listen.backlog1024
JQUICK_LISTEN_SO_TIMEOUTgateway.listen.so_timeout5000
JQUICK_LISTEN_PROBE_TIMEOUTgateway.listen.probe_timeout_msso_timeout
JQUICK_NGINX_HOST / JQUICK_NGINX_PORTnginx.target.host/port127.0.0.1/80
JQUICK_OTHER_HOST / JQUICK_OTHER_PORTother.traffic.target.host/port127.0.0.1/9000
JQUICK_AUTH_ENABLEother.traffic.auth.enabletrue
JQUICK_AUTH_TOKEN_PREFIX_LENother.traffic.auth.token-prefix-len32
JQUICK_AUTH_TIMEOUT_MSother.traffic.auth.timeout_ms5000
JQUICK_AUTH_TOKENSother.traffic.auth.valid-tokens逗号分隔
JQUICK_NETTY_BOSS_THREADS / JQUICK_NETTY_WORKER_THREADSnetty.boss/worker.threads1/0
JQUICK_LEAK_DETECTIONnetty.buffer.leak.detectionSIMPLE

7.3 Dockerfile(多阶段构建)

# Stage 1: Maven + JDK 8 构建 fat-jar
FROM maven:3.9-eclipse-temurin-8 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn -q -DskipTests package

# Stage 2: JRE 运行时(无 EXPOSE,端口由运行时 -p 决定)
FROM eclipse-temurin:8-jre-jammy
WORKDIR /app
COPY --from=builder /build/target/tcp-gateway.jar /app/tcp-gateway.jar
COPY docker/gateway.yaml /app/gateway.yaml
ENV TZ=Asia/Shanghai LANG=C.UTF-8
ENTRYPOINT ["java", "-jar", "/app/tcp-gateway.jar"]
CMD ["/app/gateway.yaml"]

7.4 构建与启动(重点:host.docker.internal 与端口映射)

docker build -t jquick-hub:1.0.0 .

容器内访问宿主机服务必须用 host.docker.internal:容器内的 127.0.0.1 是容器自己,不是宿主机!Nginx、数据库如果跑在宿主机上,要这样配:

docker run -d --name jquick-hub --restart unless-stopped \
  -p 18080:18080 \
  -e JQUICK_LISTEN_PORT=18080 \
  -e JQUICK_NGINX_HOST=host.docker.internal -e JQUICK_NGINX_PORT=52000 \
  -e JQUICK_OTHER_HOST=host.docker.internal -e JQUICK_OTHER_PORT=5236 \
  -e JQUICK_AUTH_ENABLE=true -e JQUICK_AUTH_TOKEN_PREFIX_LEN=32 \
  -e JQUICK_AUTH_TOKENS=0123456789abcdef0123456789abcdef \
  jquick-hub:1.0.0

端口映射规则-p 宿主机端口:容器端口):

  • 容器端口必须等于 JQUICK_LISTEN_PORT(JVM 在容器内实际监听的端口);
  • 宿主机端口可自由选择,如 -p 8080:18080 表示宿主机 8080 → 容器 18080,客户端连宿主机 8080 即可;
  • 网关是单端口嗅探分流,一条映射就能同时暴露 HTTP/HTTPS 和普通 TCP 流量;多监听端口才需要多条 -p
  • Linux 下访问宿主机需要追加 --add-host=host.docker.internal:host-gateway

7.5 docker-compose 一键部署

services:
  jquick-hub:
    build:
      context: .
      dockerfile: Dockerfile
    image: jquick-hub:1.0.0
    container_name: jquick-hub
    restart: unless-stopped
    ports:
      - "18080:18080"
    environment:
      JQUICK_LISTEN_PORT: "18080"
      JQUICK_NGINX_HOST: "host.docker.internal"
      JQUICK_NGINX_PORT: "52000"
      JQUICK_OTHER_HOST: "host.docker.internal"
      JQUICK_OTHER_PORT: "5236"
      JQUICK_AUTH_ENABLE: "true"
      JQUICK_AUTH_TOKEN_PREFIX_LEN: "32"
      JQUICK_AUTH_TOKENS: "0123456789abcdef0123456789abcdef"
    # Linux 访问宿主机:
    # extra_hosts:
    #   - "host.docker.internal:host-gateway"
docker compose up -d --build
docker compose logs -f jquick-hub

八、源码解析:三个 Handler 的硬核细节

8.1 ProtocolDetectHandler:只窥视不消费

探测期累积缓冲 pending,判定只读(getUnsignedByte/getByte),不改 readerIndex,判定完成后把整段原始字节原样移交,保证零丢包。几个容易踩坑的点:

  • 零拷贝接管:pending 为空时直接接管 ByteBuf in,不拷贝;
  • 拆包合并:pending 非空时合并到新缓冲,释放旧缓冲;
  • 防灌爆:累积超过 64KB 仍未判定,直接按普通 TCP 流量处理,防止恶意客户端疯狂灌数据拖垮内存。

8.2 AuthPrefixHandler:常量时间比较 + 静默关闭

// 只读窥视前 tokenLen 字节
byte[] head = new byte[tokenLen];
authBuffer.getBytes(authBuffer.readerIndex(), head);
// 常量时间比较,防时序侧信道
for (byte[] t : validTokens) {
    if (t.length == head.length && MessageDigest.isEqual(head, t)) {
        // 通过 → 剥离 token,剩余数据 slice+retain 零拷贝透传
    }
}
// 失败 → 静默关闭,不返回任何应答

边界处理完备:token 跨多个 TCP 包到达(拆包)统一累积;首包可能 token+业务数据粘连(粘包)统一处理;通过后剩余数据用 slice(tokenLen, remaining).retain() 零拷贝切片透传。

8.3 TcpRelayHandler:零丢包 + 背压 + 断连联动

一个类两种角色:前端侧(发起后端建连、暂存数据)与后端侧(把后端数据写回前端)。

  • 零丢包:后端未就绪期间前端数据全部暂存 buffered,就绪后一次性刷出;
  • 背压!backendChannel.isWritable()setAutoRead(false) 暂停从前端读,对端恢复可写(channelWritabilityChanged)再恢复读取;
  • 断连联动:前端断开关后端,后端断开关前端,写失败/异常双端关闭;
  • 后端连接失败 → 立即关闭前端(这个行为是排查秒断问题的关键,见下节)。

九、实战踩坑与问题排查

场景:客户端连上网关后 7 毫秒连接就断了,客户端日志一切正常。

排查顺序:

现象原因解决
客户端正常拼 token、转发,网关日志显示 鉴权失败客户端 token 与网关 valid-tokens 不一致两端 token 对齐;注意 JQUICK_AUTH_TOKENS 逗号分隔
网关日志显示 鉴权通过...透传N字节,随后 后端连接失败 127.0.0.1:5236 - Connection refused容器内 127.0.0.1 是容器自己,不是宿主机后端目标改 host.docker.internal
网关日志 后端连接成功,开始透传,但客户端仍秒断客户端工具配置的端口不对检查 -p 映射的宿主机端口
启动即退出鉴权开启但 token 长度与 token-prefix-len 不一致校验所有 token 长度

经验总结:排查这种"秒断"问题,第一件事看网关侧日志而不是客户端日志——网关的每个决策(协议判定、鉴权、后端建连、透传)都有日志,链路一目了然。

十、总结与适用场景

这款 Netty4 单端口协议嗅探网关解决的核心问题是:用一个公网端口,安全地暴露多种 TCP 服务

适合的场景:

  • 只有一个公网 IP/端口资源受限,却要同时对外提供 HTTP、数据库、SSH 服务;
  • 不想直接把数据库端口暴露公网,需要一层 token 鉴权兜底;
  • 内网服务多端口转发配置繁琐,希望统一收敛到单端口;
  • 已有 Nginx 但只懂 HTTP,普通 TCP 流量需要额外通道。

不适合的场景:对性能极致敏感(每连接多一跳)、协议需要深度解析/改造(网关是纯透传)。

安全性提示:token 是秘密,client.propertiesJQUICK_AUTH_TOKENS 切勿提交到 git;生产环境请用随机生成的强 token,并配合防火墙把网关端口限制在可信来源 IP。


项目地址:jquick-hub(Netty4 单端口协议嗅探 TCP 网关,含 GatewayClient 迷你代理客户端)

如果这篇文章对你有帮助,欢迎 点赞、收藏、评论 交流;关注我不迷路,后续会持续输出 Netty 网络编程、内网穿透、容器化部署等实战文章。你的支持是我更新的最大动力!

本文标签:#Netty #TCP网关 #协议嗅探 #内网穿透 #单端口多协议 #Docker部署 #端口映射 #数据库安全 #Java #后端架构

更多推荐