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.
Overview
Section titled “Overview”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 packageproduces the fat-jartarget/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).
✨ Core Features
Section titled “✨ Core Features”- Single-port multi-protocol sniffing: one listening port carries HTTP, HTTPS-TLS and other TCP traffic at once. A first byte of
0x16detects 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.isEqualfor 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).
🏗️ Architecture
Section titled “🏗️ Architecture”Topology & traffic split
Section titled “Topology & traffic split” ┌─────────────┐ HTTP/HTTPS ┌──────────────────────────────┐ ┌───────────────┐ │ Web 客户端 │─────────────▶│ │───────────▶│ 内网 Nginx │ └─────────────┘ │ JQuick Hub 单端口(:18080) │ │ nginx.target │ ┌─────────────┐ 其他 TCP │ 协议嗅探 → 可选Token认证→透传 │ └───────────────┘ │ TCP 客户端 │─────────────▶│ │───────────▶┌───────────────┐ └─────────────┘ └──────────────────────────────┘ │ 指定后端服务 │ │ other.target │ └───────────────┘Processing pipeline
Section titled “Processing pipeline”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 时加入流水线)Protocol detection rules
Section titled “Protocol detection rules”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):
- First byte ==
0x16→ classified as TLS (HTTPS); a single byte is enough; - 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; - ≥ 16 bytes accumulated with no match → classified as OTHER (plain TCP traffic).
Probe timeout
Section titled “Probe timeout”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.
Where routing & auth apply
Section titled “Where routing & auth apply”- HTTP / TLS →
nginx.target(code default127.0.0.1:80); - OTHER →
other.traffic.target(code default127.0.0.1:9000); when authentication is enabled, traffic passesJQuickAuthPrefixHandlerfirst; - Prefix-token authentication only applies to OTHER traffic and is never triggered for HTTP/HTTPS; with
enable=false, OTHER traffic is relayed fully transparently.
🚀 Deployment
Section titled “🚀 Deployment”Option 1: build from source
Section titled “Option 1: build from source”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):
- Command-line argument
arg - Environment variable
GATEWAY_CONFIG ./gateway.yamlin the working directory- Built-in
/gateway.yamlon the classpath
Option 2: Docker
Section titled “Option 2: Docker”Build the image:
docker build -t jquick-hub:1.0.0 .Start the container (environment variables override the configuration):
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.0Mount a custom configuration file:
docker run -d --name jquick-hub -p 18080:18080 -v /path/to/gateway.yaml:/app/gateway.yaml:ro jquick-hub:1.0.0Port mapping rules:
- The format is
-p <host port>:<container port>; the container port must equalJQUICK_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"].
Option 3: Docker Compose
Section titled “Option 3: Docker Compose”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:roCommon commands:
docker compose up -d --build # 构建并后台启动docker compose logs -f jquick-hub # 跟踪网关日志docker compose down # 停止并移除Verify it is running
Section titled “Verify it is running”docker logs -f jquick-hubWhen relaying succeeds, the log shows the keyword: 后端连接成功 <host>:<port>,开始透传.
⚙️ Configuration reference
Section titled “⚙️ Configuration reference”Complete gateway.yaml
Section titled “Complete gateway.yaml”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.
🔐 Authentication in depth
Section titled “🔐 Authentication in depth”How it works
Section titled “How it works”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.
Security design
Section titled “Security design”- Constant-time comparison:
MessageDigest.isEqualcompares 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.
Toggle behavior
Section titled “Toggle behavior”other.traffic.auth.enable=false→ OTHER traffic is relayed fully transparently (same behavior as the old version);- Default-on condition:
enabledefaults totrueonly when the YAML has anauthsection; without anauthsection 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-lenexactly (32 bytes by default), otherwise startup is rejected.
🔌 Local proxy client
Section titled “🔌 Local proxy client”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.
The client.properties file
Section titled “The client.properties file”Original content of client/client.properties (4 lines):
local.ports=15236gw.host=127.0.0.1gw.port=18080token=0123456789abcdef0123456789abcdefConfiguration 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 |
Starting the client
Section titled “Starting the client”javac -encoding UTF-8 GatewayClient.javajava -Dfile.encoding=UTF-8 GatewayClient # 读 ./client.propertiesjava -Dfile.encoding=UTF-8 GatewayClient -config d:\xx\client.propertiesjava -Dfile.encoding=UTF-8 GatewayClient -ports 5236 -host x.x.x.x -port 8080 -token xxxxxxxxOn Windows you can simply double-click start-client.bat (it compiles automatically on first run).
Client behavior
Section titled “Client behavior”- 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.
❓ FAQ & troubleshooting
Section titled “❓ FAQ & troubleshooting”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.
HTTP requests routed to the other target
Section titled “HTTP requests routed to the other target”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.
Open source
Section titled “Open source”- GitHub repository: https://github.com/paohaijiao/jquick-hub
- Issue tracker: https://github.com/paohaijiao/jquick-hub/issues
- License: Apache License 2.0