EncodeBatch packs zero or more frames into a base64-encoded HTTP body. Wire format (before base64): nonce (12 bytes) || AES-GCM ciphertext+tag over: flags (1 byte) — 0x00 raw | 0x01 DEFLATE-compressed body client_id (16 bytes) u16 frame_count for each frame: u32 marshaled_len
(c *Crypto, clientID [ClientIDLen]byte, frames []*Frame)
| 145 | // not survive the hop. Sealing it under AES-GCM also means a passive observer |
| 146 | // of the relay traffic cannot tell two clients apart by their IDs. |
| 147 | func EncodeBatch(c *Crypto, clientID [ClientIDLen]byte, frames []*Frame) ([]byte, error) { |
| 148 | if len(frames) > 0xFFFF { |
| 149 | return nil, fmt.Errorf("batch: too many frames: %d", len(frames)) |
| 150 | } |
| 151 | |
| 152 | // Compute the exact plaintext size up front so the pooled buffer can be |
| 153 | // grown once and frames appended directly into it (no per-frame alloc). |
| 154 | plainSize := 1 + ClientIDLen + 2 // flags byte + client_id + u16 frame count |
| 155 | for _, f := range frames { |
| 156 | plainSize += 4 + f.EncodedLen() // u32 length prefix + frame bytes |
| 157 | } |
| 158 | |
| 159 | // Pull a plaintext scratch buffer from the pool; grow if needed. |
| 160 | plainP := encPlainPool.Get().(*[]byte) |
| 161 | plain := (*plainP)[:0] |
| 162 | if cap(plain) < plainSize { |
| 163 | plain = make([]byte, 0, plainSize) |
| 164 | } |
| 165 | defer func() { |
| 166 | // Reset and return to pool. The capacity is preserved so the next |
| 167 | // EncodeBatch reuses the same underlying allocation. |
| 168 | plain = plain[:0] |
| 169 | *plainP = plain |
| 170 | encPlainPool.Put(plainP) |
| 171 | }() |
| 172 | |
| 173 | plain = append(plain, 0x00) // flags placeholder at index 0 |
| 174 | plain = append(plain, clientID[:]...) |
| 175 | plain = append(plain, byte(len(frames)>>8), byte(len(frames))) |
| 176 | for _, f := range frames { |
| 177 | n := f.EncodedLen() |
| 178 | plain = append(plain, byte(n>>24), byte(n>>16), byte(n>>8), byte(n)) |
| 179 | var err error |
| 180 | plain, err = f.AppendMarshal(plain) |
| 181 | if err != nil { |
| 182 | return nil, fmt.Errorf("batch: marshal frame: %w", err) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | // Attempt Zstandard compression on the payload section (everything after |
| 187 | // the flags byte at index 0). Only worthwhile for batches large enough that |
| 188 | // the overhead is amortised; small control batches (SYN/FIN/keepalive) are |
| 189 | // sent raw. If compression does not shrink the data (e.g. already-encrypted |
| 190 | // TLS payloads) we fall back to raw transparently. |
| 191 | sealInput := plain // default: raw, flags byte already 0x00 |
| 192 | if len(plain)-1 >= compressMinSize { |
| 193 | enc := zstdEncPool.Get().(*zstd.Encoder) |
| 194 | // EncodeAll appends compressed bytes to dst. The [:1:1] cap trick gives |
| 195 | // us a fresh backing array with the flags placeholder at [0], so the |
| 196 | // pool-owned plain buffer is never modified. |
| 197 | compressed := enc.EncodeAll(plain[1:], plain[:1:1]) |
| 198 | zstdEncPool.Put(enc) |
| 199 | if len(compressed)-1 < len(plain)-1 { |
| 200 | compressed[0] = batchFlagZstd |
| 201 | sealInput = compressed |
| 202 | } else { |
| 203 | plain[0] = batchFlagRaw |
| 204 | } |