feralbureau/hush-go
GitHub: feralbureau/hush-go
一个基于 QUIC 和二进制 TLV 编码的隐身优先 Go API 协议框架,旨在抵御常规网络抓包与非官方客户端调用。
Stars: 0 | Forks: 0
# Hush 🔇
**专为 Go 设计的隐身优先 API 协议。**
Hush 是一个网络协议框架,能让你的 API 对标准工具隐形。没有可发现的 HTTP endpoint,没有可读的请求结构,也没有重放攻击。它运行在带有自定义 ALPN 的 QUIC 之上,将 payload 编码为紧凑的二进制 TLV 格式,并使用基于会话的 AES-256-GCM 密钥加密每一帧。
```
import "github.com/feralbureau/hush-go"
```
## 为什么选择 Hush
| REST 的问题 | Hush 的解决方案 |
|---|---|
| 任何人都能打开 DevTools 并复制请求 | 自定义 ALPN `hush/1` — HTTP 工具无法连接 |
| API 接口完全可观测 | 二进制 TLV + AEAD — 没有可读的结构 |
| 很容易被 fuzz 测试和渗透测试 | 会话级加密 + 序列号 |
| 很容易编写非官方客户端 | 每个会话的临时 ECDH 密钥让重放变得毫无意义 |
| gRPC 臃肿且难以忍受 | 使用 TLV 而非 protobuf,无需代码生成,无需 schema 文件 |
当你不需要这些保护时,可以将其中的任意项禁用(参见[配置](#configuration))。
## 快速开始
### 前置条件
- Go 1.26+
- 用于本地测试的 TLS 证书:
```
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout test-key.pem -out test-cert.pem -days 3650 -nodes \
-subj "/CN=hush.test" -addext "subjectAltName=DNS:hush.test,IP:127.0.0.1"
```
### 最小化服务端
```
package main
import (
"context"
"crypto/tls"
"log"
"os"
"os/signal"
"github.com/feralbureau/hush-go/frame"
"github.com/feralbureau/hush-go/server"
"github.com/feralbureau/hush-go/session"
"github.com/feralbureau/hush-go/tlv"
)
func main() {
apiKey, _ := session.GenerateAPIKey()
keyStore := session.MapKeyStore{apiKey.ID: apiKey.Secret}
cert, _ := tls.LoadX509KeyPair("test-cert.pem", "test-key.pem")
tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}}
srv, _ := server.NewServer(keyStore,
server.WithTLSConfig(tlsCfg),
server.WithLogger(log.Default()),
)
srv.HandleFunc(0x0001, func(ctx context.Context, r *server.Request) (*frame.Response, error) {
name, _ := r.Payload.GetString("name")
return server.NewResponse(tlv.NewMap().
Set("greeting", tlv.String("hello, "+name))), nil
})
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
log.Fatal(srv.ListenAndServe(ctx, ":443"))
}
```
### 最小化客户端
```
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"github.com/feralbureau/hush-go/client"
"github.com/feralbureau/hush-go/session"
"github.com/feralbureau/hush-go/tlv"
)
func main() {
key := &session.APIKey{ID: "", Secret: []byte("")}
tlsConf := &tls.Config{InsecureSkipVerify: true}
c, err := client.Dial(context.Background(), "127.0.0.1:443", key,
client.WithTLSConfig(tlsConf))
if err != nil {
log.Fatal(err)
}
defer c.Close()
resp, _ := c.Do(context.Background(), 0x0001,
tlv.NewMap().Set("name", tlv.String("world")))
greeting, _ := resp.Payload.GetString("greeting")
fmt.Println(greeting) // "hello, world"
}
```
## 包概览
```
hush-go/
├── transport/ QUIC dial/listen with configurable ALPN
├── session/ X25519 key exchange, AES-256-GCM, session store
├── frame/ Length-prefixed encrypted/plaintext wire frames
├── tlv/ Binary TLV serialization (string, ints, floats, maps, arrays)
├── client/ High-level client (connect, send, receive)
├── server/ High-level server (TLS, sessions, handler dispatch, streaming)
│ └── stream.go Built-in event pub/sub hub
└── media/ Session-bound media tokens for HTTP media delivery
```
### `transport` — QUIC 连接
```
// Override the ALPN for the protocol (default: "hush/1")
transport.DefaultALPN = "my-app/1"
conn, _ := transport.Dial(ctx, "127.0.0.1:443", tlsCfg)
listener, _ := transport.Listen(":443", tlsCfg)
```
传递给 Dial/Listen 的 TLS 配置用作证书和 ALPN 的来源。如果 `NextProtos` 为空,传输层将使用 `DefaultALPN`。
### `session` — 密钥交换、加密、配置
```
POST-QUIC HANDSHAKE:
Client ──► api_key_id + X25519_pub ──► Server
Client ◄── X25519_pub + session_id ◄── Server
Both: shared = ECDH(priv, peer_pub)
key = HKDF-SHA256(salt=shared, ikm=api_key_secret, info="hush-v1-key")
```
```
// Generate API keys
key, _ := session.GenerateAPIKey()
// Low-level handshake
priv, _ := session.GenerateKeyPair()
sess, _ := session.NegotiateClient(ctx, stream, key, priv)
// Key store interface
type APIKeyStore interface {
Get(id string) []byte
}
store := session.MapKeyStore{key.ID: key.Secret}
// Session store with configurable timeouts
store := session.NewSessionStore(session.SessionConfig{
IdleTimeout: 5 * time.Minute,
MaxLifetime: 24 * time.Hour,
GCInterval: 1 * time.Minute,
})
```
### `frame` — 传输格式
每个请求/响应都是一个包含单个帧的 QUIC stream:
```
4 bytes: frame_length (big-endian)
4 bytes: sequence_number (big-endian)
N bytes: frame_data
```
当**已加密**时 (`key != nil`):
```
frame_data = nonce (12) || AES-256-GCM ciphertext || tag (16)
```
当**明文**时 (`key == nil`):
```
frame_data = raw plaintext bytes
```
```
// Encrypted (default)
frame.WriteRequest(stream, key, seq, req)
req, seq, _ := frame.ReadRequest(stream, key)
// Plaintext (no encryption)
frame.WriteRequest(stream, nil, seq, req)
req, seq, _ := frame.ReadRequest(stream, nil)
```
#### 允许的 opcode 范围
opcode 是 `uint16`。约定如下:
| 范围 | 用途 |
|-------|-----|
| `0x0000` | 保留(服务器推送事件) |
| `0x0001`–`0x00FF` | 系统 |
| `0x0100`–`0x7FFF` | 应用程序 |
| `0x8000`–`0xFFFF` | 保留供未来 Hush 扩展使用 |
### `tlv` — 二进制 payload 序列化
紧凑,无 schema 文件,无代码生成。传输格式如下:
```
type (1 byte) || length (LEB128 varint) || value (length bytes)
```
**支持的类型:**
| 类型 | Go 构造函数 | Go 访问器 |
|------|---|--|
| String | `tlv.String(s)` | `v.String()` |
| Bytes | `tlv.Bytes(b)` | `v.Bytes()` |
| Uint8 | `tlv.Uint8(n)` | `v.Uint8()` |
| Uint16 | `tlv.Uint16(n)` | `v.Uint16()` |
| Uint32 | `tlv.Uint32(n)` | `v.Uint32()` |
| Uint64 | `tlv.Uint64(n)` | `v.Uint64()` |
| Int32 | `tlv.Int32(n)` | `v.Int32()` |
| Int64 | `tlv.Int64(n)` | `v.Int64()` |
| Float32 | `tlv.Float32(f)` | `v.Float32()` |
| Float64 | `tlv.Float64(f)` | `v.Float64()` |
| Bool | `tlv.Bool(b)` | `v.Bool()` |
| Array | `tlv.Array(vals)` | `v.Array()` |
| Map | `tlv.NewMap().Set(...)` | `v.Map()` |
| Timestamp | `tlv.Timestamp(t)` | `v.Timestamp()` |
| Null | `tlv.Null` | — |
**Map — 主要的 payload 结构:**
```
payload := tlv.NewMap().
Set("name", tlv.String("alice")).
Set("count", tlv.Uint64(42)).
Set("nested", tlv.MapValue(tlv.NewMap().
Set("key", tlv.Bool(true)),
)).
// Reading
name, _ := payload.GetString("name")
count, _ := payload.GetUint64("count")
nested, _ := payload.GetMap("nested")
```
### `client` — 高级客户端
```
c, err := client.Dial(ctx, addr, apiKey, opts...)
resp, err := c.Do(ctx, opcode, payload)
sid := c.SessionID()
c.Close()
```
### `server` — 高级服务端
```
srv, _ := server.NewServer(keyStore, opts...)
// Standard request-response handler
srv.HandleFunc(0x0001, func(ctx context.Context, r *server.Request) (*frame.Response, error) {
return server.NewResponse(tlv.NewMap().Set("ok", tlv.Bool(true))), nil
})
// Streaming handler (full stream control, e.g. event subscriptions)
srv.HandleStreamFunc(0x0002, func(ctx context.Context, r *server.Request,
stream io.ReadWriteCloser, key []byte) error {
// write frames to stream
return nil
})
srv.ListenAndServe(ctx, ":443")
// Or use an existing UDP socket
conn, _ := net.ListenUDP("udp", addr)
srv.ListenAndServeOnConn(ctx, conn)
```
**服务端选项:**
| 选项 | 用途 |
|--------|---------|
| `WithTLSConfig(cfg)` | TLS 证书(必填) |
| `WithLogger(l)` | 结构化 logger(nil = 静默) |
| `WithSessionConfig(cfg)` | 会话超时 |
| `WithMediaSupport(baseURL)` | 媒体 token 存储 |
### `media` — 媒体 token 管理
为了通过 HTTPS 提供大文件(图像、音频、HLS 流)服务,Hush 使用绑定到会话的媒体 token。QUIC 会话处理 API 调用;配套的 HTTPS 服务器处理媒体传输。
```
store := media.NewTokenStore(func(sid uint64) bool {
_, ok := sessionStore.Get(sid)
return ok
})
// Issue a token bound to a session
tok, _ := store.Issue(sessionID, "track-abc")
// Validate and extend (for initial access)
valid := store.Validate(tok.ID)
// Lightweight existence check (for HLS segment proxying)
exists := store.Exists(tok.ID)
// Absolute TTL (configurable)
store.MaxTokenTTL = 30 * time.Minute
// Build media URLs
builder := media.NewMediaURLBuilder("https://media.example.com", store)
url := builder.BuildURL(tok.ID, "track-abc")
// → "https://media.example.com/media/ab12.../track-abc"
```
## 事件流
Hush 包含一个内置的、基于内存主题的 pub/sub 中心。
```
hub := server.NewHub()
// Register handlers
srv.HandleFunc(0x0401, hub.PublishHandler())
srv.HandleStreamFunc(0x0402, hub.SubscribeHandler())
srv.HandleFunc(0x0403, hub.ListTopicsHandler())
// Publish from anywhere
hub.Publish("alerts", tlv.NewMap().Set("level", tlv.String("info")))
```
### 客户端
```
// Subscribe to a topic (streaming — stays open)
// Subscribe creates a long-lived stream that pushes events as they arrive.
// The client receives frames with status=0 and the event payload.
// Publish an event (standard request-response)
resp, _ := c.Do(ctx, 0x0401, tlv.NewMap().
Set("topic", tlv.String("alerts")).
Set("payload", tlv.MapValue(tlv.NewMap().
Set("level", tlv.String("info")),
)),
)
// List active topics
resp, _ := c.Do(ctx, 0x0403, nil)
```
## 配置
Hush 中的所有内容都是可配置的。以下是所有的调优点:
### 开启/关闭加密
传递 `nil` 而不是会话密钥来读/写明文帧:
```
frame.WriteRequest(stream, nil, seq, req) // no encryption
req, seq, _ := frame.ReadRequest(stream, nil) // no decryption
```
### 会话超时
```
srv, _ := server.NewServer(ks,
server.WithSessionConfig(session.SessionConfig{
IdleTimeout: 10 * time.Minute, // default: 5m
MaxLifetime: 48 * time.Hour, // default: 24h
GCInterval: 30 * time.Second, // default: 1m
}),
)
```
### ALPN
```
import "github.com/feralbureau/hush-go/transport"
transport.DefaultALPN = "my-custom-proto/1"
```
### 媒体 token TTL
```
store.MaxTokenTTL = 10 * time.Minute // default: 2h
```
### Logger
```
srv, _ := server.NewServer(ks,
server.WithLogger(log.New(os.Stdout, "hush: ", log.Ltime|log.Lmsgprefix)),
)
```
### 日志级别格式
服务器日志使用级别前缀:`[INF]`、`[WRN]`、`[ERR]`。
## 传输协议参考
### 帧格式
```
frame_length (uint32 BE) || frame_data
```
**加密的 frame_data:**
```
sequence_number (uint32 BE) || nonce (12 bytes) || ciphertext || AEAD tag (16 bytes)
```
**明文 frame_data:**
```
sequence_number (uint32 BE) || plaintext
```
### 请求明文
```
opcode (uint16 BE) || tlv_payload (optional)
```
### 响应明文
```
status_code (uint8) || tlv_payload (optional)
```
### 状态码
| 代码 | 名称 |
|------|------|
| `0x00` | 成功 |
| `0x01` | 错误请求 |
| `0x02` | 未认证 |
| `0x03` | 拒绝访问 |
| `0x04` | 未找到 |
| `0x05` | 会话已过期 |
| `0x06` | 限流 |
| `0x07` | 内部错误 |
| `0x80+` | 应用程序自定义 |
### 会话握手
```
Client → Server: api_key_id_len (uint16 BE) || api_key_id || X25519_pubkey (32 bytes)
Server → Client: X25519_pubkey (32 bytes) || session_id (uint64 BE)
Shared secret = ECDH(client_priv, server_pub)
Session key = HKDF-SHA256(ikm=api_key_secret, salt=shared_secret, info="hush-v1-key")
```
## 安全模型
| 威胁 | 缓解措施 |
|---|---|
| 窃听 | TLS 1.3 + 每帧 AES-256-GCM |
| 重放攻击 | 每帧序列号,基于会话的密钥 |
| API 密钥被盗 | 密钥是用于 ECDH 的 PSK — 握手后从不发送 |
| 可观测性 | 自定义 ALPN,二进制传输格式,无可读结构 |
| 模糊测试 | 无效帧会在传输层导致 AEAD 解密失败 |
| 会话劫持 | 会话 ID 绑定到 ECDH 派生密钥 |
### 权衡
- **浏览器支持**:Hush 使用原生 QUIC — 浏览器无法像 WebSocket 那样连接到它。对于 Web 客户端,请运行 HTTPS 或 WebSocket 桥接。
- **复杂性**:QUIC + 自定义加密比普通 HTTP 更重。你是在用简单性换取隐身性。
- **调试**:没有 curl,没有 Postman,没有 DevTools。请使用内置的客户端,或者在开发期间以明文模式 (`key == nil`) 运行。
## 项目结构
```
hush-go/
├── transport/ QUIC dial/listen, configurable ALPN
├── session/ X25519 key exchange, AES-256-GCM, session store, config
├── frame/ Length-prefixed encrypted/plaintext wire frames
├── tlv/ Binary TLV encode/decode, all types
├── client/ High-level client
├── server/ High-level server, handlers, event streaming hub
├── media/ Session-bound media token store
├── test-cert.pem TLS cert for local testing
└── test-key.pem TLS key for local testing
```
## 贡献
欢迎贡献。在提交 pull request 之前,请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 许可证
[MIT](LICENSE)
标签:API框架, EVTX分析, Go, QUIC, Ruby工具, 二进制协议, 内核驱动, 加密通信, 底层编程, 日志审计, 网络协议