Skip to content

JQuick Hub

JQuick Hub is a single-public-port, protocol-sniffing TCP gateway built on Netty 4. It targets scenarios where public ports are scarce yet one port must serve both web traffic and TCP business traffic at the same time.

JQuick Hub listens on a single port and automatically distinguishes HTTP, HTTPS-TLS and other TCP traffic: HTTP/HTTPS traffic is relayed as-is to the intranet Nginx, while all remaining TCP traffic is forwarded to a configurable target, optionally guarded by a fixed-length prefix token.

The pain point it solves is straightforward: public ports are limited (or security policy allows only one open port), yet you need to expose both web services and TCP services such as databases. Traditional setups require multiple reverse proxies or negotiating a separate port per service; JQuick Hub converges every kind of traffic onto one port through protocol sniffing.

Technical foundation and project shape:

  • Java 1.8+, Netty 4.1.92.Final, snakeyaml 2.3, slf4j 1.7.36, lombok 1.18.30
  • Single-module Maven project, coordinates com.gateway:hub:1.0.0
  • mvn clean package produces the fat-jar target/tcp-gateway.jar (Main-Class=com.gateway.JQuickGatewayServer)

Typical scenarios:

Scenario Description
Single-port multiplexing Only one public port is open, carrying both web (HTTP/HTTPS) and TCP services such as databases
Unified intranet entry Acts as the single public entry in front of the intranet Nginx and internal TCP services, splitting traffic by protocol
Private TCP protocol access Non-HTTP private protocols reach intranet services after prefix-token authentication, without exposing plaintext business ports to the public

Requirements: JDK 8+ (to build and run from source); or Docker (the runtime image is based on eclipse-temurin:8-jre-jammy).

  • Single-port multi-protocol sniffing: one listening port carries HTTP, HTTPS-TLS and other TCP traffic at once. A first byte of 0x16 detects a TLS handshake (RFC 5246); an HTTP method name followed by a space detects HTTP; a 16-byte probe window is the fallback.
  • Zero-parsing byte relay: business protocol payloads are never parsed; the TCP byte stream is forwarded verbatim, fully transparent to your services.
  • Prefix-token authentication: a fixed-length token is prepended to the first packet; comparison uses MessageDigest.isEqual for constant-time matching against timing side channels, and tokens are masked in logs. It applies only to non-HTTP/HTTPS traffic.
  • Zero packet loss with backpressure: bytes arriving before the backend is ready are buffered and flushed once the connection is up; when the peer is not writable, local reads pause and resume automatically once it becomes writable again.
  • Linked shutdown: when either side goes inactive, fails a write or throws, both sides close together — no half-open connections.
  • Full environment-variable override: every configuration item can be overridden by a JQUICK_* environment variable, with the precedence “environment variables > YAML > code defaults”.
  • Cloud-native containerization: multi-stage Dockerfile (Maven build + pure JRE runtime) and a ready-to-use docker-compose.yaml; a single port mapping exposes HTTP/HTTPS and TCP at the same time.
  • Graceful shutdown and observability: a ShutdownHook gracefully closes bossGroup/workerGroup on SIGTERM/SIGINT; the Netty buffer leak-detection level is configurable (SIMPLE / ADVANCED / PARANOID / DISABLED).
┌─────────────┐ HTTP/HTTPS ┌──────────────────────────────┐ ┌───────────────┐
│ Web 客户端 │─────────────▶│ │───────────▶│ 内网 Nginx │
└─────────────┘ │ JQuick Hub 单端口(:18080) │ │ nginx.target │
┌─────────────┐ 其他 TCP │ 协议嗅探 → 可选Token认证→透传 │ └───────────────┘
│ TCP 客户端 │─────────────▶│ │───────────▶┌───────────────┐
└─────────────┘ └──────────────────────────────┘ │ 指定后端服务 │
│ other.target │
└───────────────┘

Every inbound connection passes through up to three handlers and is routed by the sniffing result:

入站连接
│
▼
JQuickProtocolDetectHandler # 协议嗅探:TLS(0x16) / HTTP(方法名+空格) / OTHER(≥16字节无匹配)
│
├── HTTP / TLS ─────────────────────────────▶ JQuickTcpRelayHandler ──▶ nginx.target
│
└── OTHER ──▶ JQuickAuthPrefixHandler ──▶ JQuickTcpRelayHandler ──▶ other.traffic.target
(仅 auth.enable=true 时加入流水线)

Key constants of JQuickProtocolDetectHandler:

Constant Value Description
TLS_HANDSHAKE_FIRST_BYTE 0x16 First byte of a TLS handshake (RFC 5246)
PROBE_MAX_BYTES 16 Protocol detection window (bytes)
MAX_PROBE_BUFFER 64KB Upper bound of the probe buffer
HTTP_METHODS GET / POST / PUT / DELETE / HEAD / OPTIONS / PATCH / CONNECT / TRACE Built-in set of HTTP method names

Decision rules (in priority order):

  1. First byte == 0x16 → classified as TLS (HTTPS); a single byte is enough;
  2. A “method-name prefix immediately followed by a space 0x20” appears → classified as HTTP, case-insensitive; if fragmentation leaves the method name incomplete, detection waits for more bytes;
  3. ≥ 16 bytes accumulated with no match → classified as OTHER (plain TCP traffic).

probe_timeout_ms (defaults to the value of so_timeout, 5000ms; falls back to 5000 when so_timeout ≤ 0):

  • Data already received before the timeout → forwarded as OTHER;
  • No data at all when the timeout fires → the connection is closed.
  • HTTP / TLS → nginx.target (code default 127.0.0.1:80);
  • OTHER → other.traffic.target (code default 127.0.0.1:9000); when authentication is enabled, traffic passes JQuickAuthPrefixHandler first;
  • Prefix-token authentication only applies to OTHER traffic and is never triggered for HTTP/HTTPS; with enable=false, OTHER traffic is relayed fully transparently.
Terminal window
mvn clean package # 产物:target/tcp-gateway.jar(fat-jar)
java -jar target/tcp-gateway.jar # 按默认查找顺序加载配置
java -jar target/tcp-gateway.jar /path/to/gateway.yaml # 命令行参数指定配置文件路径

Configuration file lookup order (highest priority first):

  1. Command-line argument arg
  2. Environment variable GATEWAY_CONFIG
  3. ./gateway.yaml in the working directory
  4. Built-in /gateway.yaml on the classpath

Build the image:

Terminal window
docker build -t jquick-hub:1.0.0 .

Start the container (environment variables override the configuration):

Terminal window
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

Mount a custom configuration file:

Terminal window
docker run -d --name jquick-hub -p 18080:18080 -v /path/to/gateway.yaml:/app/gateway.yaml:ro jquick-hub:1.0.0

Port mapping rules:

  • The format is -p <host port>:<container port>; the container port must equal JQUICK_LISTEN_PORT, while the host port is up to you;
  • Single-port sniffing means one mapping exposes HTTP/HTTPS and TCP at the same time;
  • The image declares no EXPOSE; published ports are entirely determined by -p.

Dockerfile highlights: multi-stage build — Stage 1 uses maven:3.9-eclipse-temurin-8 to compile and package, Stage 2 runs on the pure JRE eclipse-temurin:8-jre-jammy; built-in ENV TZ=Asia/Shanghai LANG=C.UTF-8; ENTRYPOINT ["java","-jar","/app/tcp-gateway.jar"], CMD ["/app/gateway.yaml"].

The repository ships a docker-compose.yaml (note the extension is .yaml, not .yml):

services:
jquick-hub:
build: .
image: jquick-hub:1.0.0
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 环境需取消注释下一项(Windows/macOS Docker Desktop 不需要)
# extra_hosts:
# - "host.docker.internal:host-gateway"
# 可选:挂载自定义配置文件
# volumes:
# - ./gateway.yaml:/app/gateway.yaml:ro

Common commands:

Terminal window
docker compose up -d --build # 构建并后台启动
docker compose logs -f jquick-hub # 跟踪网关日志
docker compose down # 停止并移除
Terminal window
docker logs -f jquick-hub

When relaying succeeds, the log shows the keyword: 后端连接成功 <host>:<port>,开始透传.

The following is the original content of src/main/resources/gateway.yaml:

gateway:
listen:
host: 0.0.0.0 # 监听网卡 IP(默认所有网卡), 可用环境变量 JQUICK_LISTEN_HOST 覆盖
port: 18080 # 监听端口, 可用环境变量 JQUICK_LISTEN_PORT 覆盖
backlog: 1024 # TCP backlog(未决连接队列大小)
so_timeout: 5000 # 连接超时毫秒: 用于后端建连超时与协议探测等待上限
probe_timeout_ms: 5000 # 协议探测等待上限(可选配置): 首包不足时最多等待的毫秒数
# 等待超时后, 若已收到数据则按"普通TCP流量"转发; 未收到任何数据则关闭连接
nginx:
target:
host: 127.0.0.1 # 内网 Nginx IP
port: 52000 # Nginx 端口
other:
traffic:
target:
host: 127.0.0.1 # 指定转发 IP
port: 5236 # 指定转发端口
auth:
enable: true # 鉴权开关,生产默认开启;false 时完全透传(行为与旧版一致)
token-prefix-len: 32 # 前置token固定长度(字节),必须与下方每个token长度一致
timeout_ms: 5000 # 鉴权超时: 连接建立后未在限定时间内收到完整token则静默关闭
valid-tokens: # 合法token列表(每个必须恰好 token-prefix-len 字节)
- "0123456789abcdef0123456789abcdef" # 示例32位token,生产环境务必更换为随机生成值
netty:
boss:
threads: 1 # boss 线程数(负责接受新连接)
worker:
threads: 0 # worker 线程数(处理 IO), 0 表示自动 = CPU核数*2
buffer:
leak:
detection: SIMPLE # 缓冲区泄漏检测级别: SIMPLE | ADVANCED | PARANOID | DISABLED
# 生产环境建议 SIMPLE; 调试内存泄漏可用 PARANOID(开销大)

Config keys × environment variables × defaults

Section titled “Config keys × environment variables × defaults”
YAML key Environment variable Code default Validation rule
gateway.listen.host JQUICK_LISTEN_HOST 0.0.0.0 —
gateway.listen.port JQUICK_LISTEN_PORT 8080 1–65535
gateway.listen.backlog JQUICK_LISTEN_BACKLOG 1024 positive integer
gateway.listen.so_timeout JQUICK_LISTEN_SO_TIMEOUT 5000ms (also the backend connect timeout) —
gateway.listen.probe_timeout_ms JQUICK_LISTEN_PROBE_TIMEOUT takes so_timeout; falls back to 5000 when so_timeout ≤ 0 —
nginx.target.host / nginx.target.port JQUICK_NGINX_HOST / JQUICK_NGINX_PORT 127.0.0.1 / 80 port 1–65535
other.traffic.target.host / other.traffic.target.port JQUICK_OTHER_HOST / JQUICK_OTHER_PORT 127.0.0.1 / 9000 port 1–65535
other.traffic.auth.enable JQUICK_AUTH_ENABLE true (only when the YAML has an auth section; off otherwise) —
other.traffic.auth.token-prefix-len JQUICK_AUTH_TOKEN_PREFIX_LEN 32 bytes positive integer
other.traffic.auth.timeout_ms JQUICK_AUTH_TIMEOUT_MS 5000ms —
other.traffic.auth.valid-tokens JQUICK_AUTH_TOKENS (comma-separated) none startup is rejected if empty while auth is enabled; the UTF-8 byte length of every token must equal token-prefix-len exactly
netty.boss.threads JQUICK_NETTY_BOSS_THREADS 1 positive integer
netty.worker.threads JQUICK_NETTY_WORKER_THREADS 0 (auto = CPU cores × 2) must not be negative
netty.buffer.leak.detection JQUICK_LEAK_DETECTION SIMPLE invalid values warn and fall back to SIMPLE

Configuration precedence: environment variables > YAML > code defaults.

Prefix-token authentication is implemented by JQuickAuthPrefixHandler and is only inserted into the pipeline of OTHER traffic. The client prepends a fixed-length token to the very first business packet; the gateway accumulates bytes and peeks at only the first token-prefix-len bytes for comparison:

  • Match: the token is stripped and the remaining business data is relayed zero-copy via slice + retain; everything afterwards passes through untouched;
  • Mismatch / timeout: ctx.close() closes the connection silently without any response (default timeout 5000ms);
  • TCP sticky-packet / fragmentation cases are unified by accumulating into authBuffer, so partial packets never cause a wrong verdict.
  • Constant-time comparison: MessageDigest.isEqual compares lengths first and then performs an equal-length comparison, defending against timing side-channel attacks;
  • Silent close: failures and timeouts return no error response at all, leaking no authentication detail to the public network;
  • Masked logging: mask() renders “first 4 chars + **** + last 2 chars”, or **** when the length ≤ 4 — tokens are never printed in plaintext under any circumstance.
  • other.traffic.auth.enable=false → OTHER traffic is relayed fully transparently (same behavior as the old version);
  • Default-on condition: enable defaults to true only when the YAML has an auth section; without an auth section it defaults to off;
  • Enabling auth with an empty valid-tokens → startup is rejected;
  • The UTF-8 byte length of every token must equal token-prefix-len exactly (32 bytes by default), otherwise startup is rejected.

GatewayClient (client/GatewayClient.java) is the standalone local proxy client shipped with the repository. It has zero third-party dependencies — a single .java file compiles and runs. It listens on one or more local ports, bridges local connections to the remote gateway, and automatically prepends the authentication token before the first packet. Key constants: GW_CONNECT_TIMEOUT_MS=5000, BUFFER_SIZE=16384, ServerSocket backlog=50.

Original content of client/client.properties (4 lines):

local.ports=15236
gw.host=127.0.0.1
gw.port=18080
token=0123456789abcdef0123456789abcdef

Configuration precedence: command-line arguments (-ports / -host / -port / -token) > the file given by -config > client.properties in the current directory.

Argument Description Default
-config <path> Path of the configuration file client.properties in the current directory
-ports Local listening ports, comma-separated for multiple ports 5236
-host Gateway address 127.0.0.1
-port Gateway port 8080
-token Authentication token (required; exit code 1 when missing) none
Terminal window
javac -encoding UTF-8 GatewayClient.java
java -Dfile.encoding=UTF-8 GatewayClient # 读 ./client.properties
java -Dfile.encoding=UTF-8 GatewayClient -config d:\xx\client.properties
java -Dfile.encoding=UTF-8 GatewayClient -ports 5236 -host x.x.x.x -port 8080 -token xxxxxxxx

On Windows you can simply double-click start-client.bat (it compiles automatically on first run).

  • Each local port gets its own listener thread;
  • In the local → gateway direction the token is prepended before the first packet (only once); the gateway → local direction is relayed verbatim;
  • When the gateway is unreachable, the local connection is closed silently;
  • Token logging uses the same “first 4 + last 2” masking and is never written to disk in plaintext.

Gateway refuses to start with auth enabled

Section titled “Gateway refuses to start with auth enabled”

With auth enabled (enable=true), an empty valid-tokens rejects startup. Fix: configure at least one valid token in other.traffic.auth.valid-tokens, or inject tokens via the JQUICK_AUTH_TOKENS environment variable (comma-separated).

Token length error at startup / auth always fails

Section titled “Token length error at startup / auth always fails”

The UTF-8 byte length of every token must equal token-prefix-len exactly (32 by default). Note that multi-byte characters such as Chinese count by UTF-8 bytes, not by character count; also make sure the client -token matches exactly one entry of the gateway’s valid-tokens.

The protocol detection window is 16 bytes (PROBE_MAX_BYTES): if no “method name + space” appears within the first 16 bytes, the traffic is classified as OTHER as soon as 16 bytes accumulate; data that has already arrived when the probe timeout (probe_timeout_ms) fires is likewise forwarded as OTHER. Make sure the client sends a standard HTTP method (GET / POST / …, case-insensitive) and that the first packet arrives completely and in time.

Container cannot reach host services on Linux

Section titled “Container cannot reach host services on Linux”

host.docker.internal does not exist by default in native Linux Docker. Append --add-host=host.docker.internal:host-gateway to docker run, or uncomment extra_hosts in compose; also make sure the container port in the -p mapping matches JQUICK_LISTEN_PORT.