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工具, 二进制协议, 内核驱动, 加密通信, 底层编程, 日志审计, 网络协议