master
go 651 lines 18 KB
Raw
1 // Package protocol implements the wire envelope and codec for the netipc
2 // protocol. Pure byte-layout encode/decode. No I/O, no transport, no
3 // allocation on decode. Localhost-only IPC — all multi-byte fields use
4 // host byte order.
5 //
6 // Decoded "View" types borrow the underlying buffer and are valid only while
7 // that buffer lives. Copy immediately if the data is needed later.
8 package protocol
9
10 import (
11 "encoding/binary"
12 "errors"
13 )
14
15 // ---------------------------------------------------------------------------
16 // Constants
17 // ---------------------------------------------------------------------------
18
19 const (
20 MagicMsg uint32 = 0x4e495043 // "NIPC"
21 MagicChunk uint32 = 0x4e43484b // "NCHK"
22 Version uint16 = 1
23 HeaderLen uint16 = 32
24 HeaderSize = 32
25
26 // Message kinds.
27 KindRequest uint16 = 1
28 KindResponse uint16 = 2
29 KindControl uint16 = 3
30
31 // Flags.
32 FlagBatch uint16 = 0x0001
33
34 // Transport status.
35 StatusOK uint16 = 0
36 StatusBadEnvelope uint16 = 1
37 StatusAuthFailed uint16 = 2
38 StatusIncompatible uint16 = 3
39 StatusUnsupported uint16 = 4
40 StatusLimitExceeded uint16 = 5
41 StatusInternalError uint16 = 6
42
43 // Control opcodes.
44 CodeHello uint16 = 1
45 CodeHelloAck uint16 = 2
46
47 // Method codes.
48 MethodIncrement uint16 = 1
49 MethodCgroupsSnapshot uint16 = 2
50 MethodStringReverse uint16 = 3
51 MethodCgroupsLookup uint16 = 4
52 MethodAppsLookup uint16 = 5
53
54 // Profile bits.
55 ProfileBaseline uint32 = 0x01
56 ProfileSHMHybrid uint32 = 0x02
57 ProfileSHMFutex uint32 = 0x04
58 ProfileSHMWaitAddr uint32 = 0x08
59
60 // Defaults.
61 MaxPayloadDefault uint32 = 1024
62
63 // MaxPayloadCap is the hard cap on negotiated request payload sizes
64 // (1 MiB) to prevent excessive memory allocation from a compromised peer.
65 MaxPayloadCap uint32 = 1024 * 1024
66
67 // Alignment for batch items and typed codec items.
68 Alignment = 8
69
70 // Payload sizes.
71 helloSize = 44
72 helloAckSize = 48
73 )
74
75 var ne = binary.NativeEndian
76
77 // ---------------------------------------------------------------------------
78 // Errors
79 // ---------------------------------------------------------------------------
80
81 var (
82 ErrTruncated = errors.New("buffer too short")
83 ErrBadMagic = errors.New("magic value mismatch")
84 ErrBadVersion = errors.New("unsupported version")
85 ErrBadHeaderLen = errors.New("header_len != 32")
86 ErrBadKind = errors.New("unknown message kind")
87 ErrBadLayout = errors.New("unknown layout_version")
88 ErrOutOfBounds = errors.New("offset+length exceeds data")
89 ErrMissingNul = errors.New("string not NUL-terminated")
90 ErrBadAlignment = errors.New("item not 8-byte aligned")
91 ErrBadItemCount = errors.New("item count inconsistent")
92 ErrOverflow = errors.New("builder out of space")
93 )
94
95 // ---------------------------------------------------------------------------
96 // Utility
97 // ---------------------------------------------------------------------------
98
99 // Align8 rounds v up to the next multiple of 8.
100 func Align8(v int) int {
101 return (v + 7) &^ 7
102 }
103
104 // ---------------------------------------------------------------------------
105 // Outer message header (32 bytes)
106 // ---------------------------------------------------------------------------
107
108 // Header is the outer message header (32 bytes on the wire).
109 type Header struct {
110 Magic uint32
111 Version uint16
112 HeaderLen uint16
113 Kind uint16
114 Flags uint16
115 Code uint16
116 TransportStatus uint16
117 PayloadLen uint32
118 ItemCount uint32
119 MessageID uint64
120 }
121
122 // Encode writes the header into buf. Returns 32 on success, 0 if buf is
123 // too small.
124 func (h *Header) Encode(buf []byte) int {
125 if len(buf) < HeaderSize {
126 return 0
127 }
128 ne.PutUint32(buf[0:4], h.Magic)
129 ne.PutUint16(buf[4:6], h.Version)
130 ne.PutUint16(buf[6:8], h.HeaderLen)
131 ne.PutUint16(buf[8:10], h.Kind)
132 ne.PutUint16(buf[10:12], h.Flags)
133 ne.PutUint16(buf[12:14], h.Code)
134 ne.PutUint16(buf[14:16], h.TransportStatus)
135 ne.PutUint32(buf[16:20], h.PayloadLen)
136 ne.PutUint32(buf[20:24], h.ItemCount)
137 ne.PutUint64(buf[24:32], h.MessageID)
138 return HeaderSize
139 }
140
141 // DecodeHeader decodes an outer message header from buf. Validates magic,
142 // version, header_len, and kind.
143 func DecodeHeader(buf []byte) (Header, error) {
144 if len(buf) < HeaderSize {
145 return Header{}, ErrTruncated
146 }
147 h := Header{
148 Magic: ne.Uint32(buf[0:4]),
149 Version: ne.Uint16(buf[4:6]),
150 HeaderLen: ne.Uint16(buf[6:8]),
151 Kind: ne.Uint16(buf[8:10]),
152 Flags: ne.Uint16(buf[10:12]),
153 Code: ne.Uint16(buf[12:14]),
154 TransportStatus: ne.Uint16(buf[14:16]),
155 PayloadLen: ne.Uint32(buf[16:20]),
156 ItemCount: ne.Uint32(buf[20:24]),
157 MessageID: ne.Uint64(buf[24:32]),
158 }
159 if h.Magic != MagicMsg {
160 return Header{}, ErrBadMagic
161 }
162 if h.Version != Version {
163 return Header{}, ErrBadVersion
164 }
165 if h.HeaderLen != HeaderLen {
166 return Header{}, ErrBadHeaderLen
167 }
168 if h.Kind < KindRequest || h.Kind > KindControl {
169 return Header{}, ErrBadKind
170 }
171 return h, nil
172 }
173
174 // ---------------------------------------------------------------------------
175 // Chunk continuation header (32 bytes)
176 // ---------------------------------------------------------------------------
177
178 // ChunkHeader is a chunk continuation header (32 bytes on the wire).
179 type ChunkHeader struct {
180 Magic uint32
181 Version uint16
182 Flags uint16
183 MessageID uint64
184 TotalMessageLen uint32
185 ChunkIndex uint32
186 ChunkCount uint32
187 ChunkPayloadLen uint32
188 }
189
190 // Encode writes the chunk header into buf. Returns 32 on success, 0 if
191 // buf is too small.
192 func (c *ChunkHeader) Encode(buf []byte) int {
193 if len(buf) < HeaderSize {
194 return 0
195 }
196 ne.PutUint32(buf[0:4], c.Magic)
197 ne.PutUint16(buf[4:6], c.Version)
198 ne.PutUint16(buf[6:8], c.Flags)
199 ne.PutUint64(buf[8:16], c.MessageID)
200 ne.PutUint32(buf[16:20], c.TotalMessageLen)
201 ne.PutUint32(buf[20:24], c.ChunkIndex)
202 ne.PutUint32(buf[24:28], c.ChunkCount)
203 ne.PutUint32(buf[28:32], c.ChunkPayloadLen)
204 return HeaderSize
205 }
206
207 // DecodeChunkHeader decodes a chunk continuation header from buf.
208 // Validates magic and version.
209 func DecodeChunkHeader(buf []byte) (ChunkHeader, error) {
210 if len(buf) < HeaderSize {
211 return ChunkHeader{}, ErrTruncated
212 }
213 c := ChunkHeader{
214 Magic: ne.Uint32(buf[0:4]),
215 Version: ne.Uint16(buf[4:6]),
216 Flags: ne.Uint16(buf[6:8]),
217 MessageID: ne.Uint64(buf[8:16]),
218 TotalMessageLen: ne.Uint32(buf[16:20]),
219 ChunkIndex: ne.Uint32(buf[20:24]),
220 ChunkCount: ne.Uint32(buf[24:28]),
221 ChunkPayloadLen: ne.Uint32(buf[28:32]),
222 }
223 if c.Magic != MagicChunk {
224 return ChunkHeader{}, ErrBadMagic
225 }
226 if c.Version != Version {
227 return ChunkHeader{}, ErrBadVersion
228 }
229 if c.Flags != 0 {
230 return ChunkHeader{}, ErrBadLayout
231 }
232 if c.ChunkPayloadLen == 0 {
233 return ChunkHeader{}, ErrBadLayout
234 }
235 return c, nil
236 }
237
238 // ---------------------------------------------------------------------------
239 // Batch item directory
240 // ---------------------------------------------------------------------------
241
242 // BatchEntry is one entry in a batch item directory (8 bytes on wire).
243 type BatchEntry struct {
244 Offset uint32
245 Length uint32
246 }
247
248 // BatchDirEncode encodes entries into buf. Returns total bytes written
249 // (len(entries) * 8), or 0 if buf is too small.
250 func BatchDirEncode(entries []BatchEntry, buf []byte) int {
251 need := len(entries) * 8
252 if len(buf) < need {
253 return 0
254 }
255 for i, e := range entries {
256 base := i * 8
257 ne.PutUint32(buf[base:base+4], e.Offset)
258 ne.PutUint32(buf[base+4:base+8], e.Length)
259 }
260 return need
261 }
262
263 // BatchDirDecode decodes itemCount directory entries from buf. Validates
264 // alignment and that each entry falls within packedAreaLen.
265 func BatchDirDecode(buf []byte, itemCount uint32, packedAreaLen uint32) ([]BatchEntry, error) {
266 count, ok := checkedInt(uint64(itemCount))
267 if !ok {
268 return nil, ErrBadItemCount
269 }
270 dirSize, ok := checkedMulInt(count, 8)
271 if !ok {
272 return nil, ErrBadItemCount
273 }
274 if len(buf) < dirSize {
275 return nil, ErrTruncated
276 }
277
278 out := make([]BatchEntry, count)
279 for i := range count {
280 base := i * 8
281 off := ne.Uint32(buf[base : base+4])
282 length := ne.Uint32(buf[base+4 : base+8])
283
284 if off%uint32(Alignment) != 0 {
285 return nil, ErrBadAlignment
286 }
287 if uint64(off)+uint64(length) > uint64(packedAreaLen) {
288 return nil, ErrOutOfBounds
289 }
290 out[i] = BatchEntry{Offset: off, Length: length}
291 }
292 return out, nil
293 }
294
295 // BatchDirValidate validates the batch directory without allocating.
296 // Checks alignment and that each entry falls within packedAreaLen.
297 func BatchDirValidate(buf []byte, itemCount uint32, packedAreaLen uint32) error {
298 count, ok := checkedInt(uint64(itemCount))
299 if !ok {
300 return ErrBadItemCount
301 }
302 dirSize, ok := checkedMulInt(count, 8)
303 if !ok {
304 return ErrBadItemCount
305 }
306 if len(buf) < dirSize {
307 return ErrTruncated
308 }
309 for i := range count {
310 base := i * 8
311 off := ne.Uint32(buf[base : base+4])
312 length := ne.Uint32(buf[base+4 : base+8])
313 if off%uint32(Alignment) != 0 {
314 return ErrBadAlignment
315 }
316 if uint64(off)+uint64(length) > uint64(packedAreaLen) {
317 return ErrOutOfBounds
318 }
319 }
320 return nil
321 }
322
323 // BatchItemGet extracts a single batch item by index from a complete batch
324 // payload. Returns the item slice on success.
325 func BatchItemGet(payload []byte, itemCount uint32, index uint32) ([]byte, error) {
326 if index >= itemCount {
327 return nil, ErrOutOfBounds
328 }
329
330 dirSize, ok := checkedInt(uint64(itemCount) * 8)
331 if !ok {
332 return nil, ErrBadItemCount
333 }
334 dirAligned, ok := checkedAlign8(dirSize)
335 if !ok {
336 return nil, ErrBadItemCount
337 }
338
339 if len(payload) < dirAligned {
340 return nil, ErrTruncated
341 }
342
343 idx, ok := checkedInt(uint64(index))
344 if !ok {
345 return nil, ErrOutOfBounds
346 }
347 base, ok := checkedMulInt(idx, 8)
348 if !ok {
349 return nil, ErrOutOfBounds
350 }
351 off, err := checkedWireU32Int(payload, base)
352 if err != nil {
353 return nil, err
354 }
355 length, err := checkedWireU32Int(payload, base+4)
356 if err != nil {
357 return nil, err
358 }
359
360 packedAreaStart := dirAligned
361 packedAreaLen := len(payload) - packedAreaStart
362
363 if off%Alignment != 0 {
364 return nil, ErrBadAlignment
365 }
366 relEnd, ok := checkedAddInt(off, length)
367 if !ok || relEnd > packedAreaLen {
368 return nil, ErrOutOfBounds
369 }
370
371 start, ok := checkedAddInt(packedAreaStart, off)
372 if !ok {
373 return nil, ErrOutOfBounds
374 }
375 end, ok := checkedAddInt(start, length)
376 if !ok {
377 return nil, ErrOutOfBounds
378 }
379 return payload[start:end], nil
380 }
381
382 // ---------------------------------------------------------------------------
383 // Batch builder
384 // ---------------------------------------------------------------------------
385
386 // BatchBuilder builds a batch payload: [directory] [align-pad] [packed items].
387 type BatchBuilder struct {
388 buf []byte
389 itemCount uint32
390 maxItems uint32
391 dirEnd int // byte offset where directory reservation ends
392 dataOffset int // current offset within the packed data area (relative)
393 }
394
395 // Reset reinitializes a batch builder against a caller-provided buffer.
396 // This lets hot paths reuse stack-allocated builders instead of allocating
397 // a fresh helper object for every request.
398 func (b *BatchBuilder) Reset(buf []byte, maxItems uint32) {
399 b.buf = buf
400 b.itemCount = 0
401 b.maxItems = maxItems
402 dirSize, ok := checkedInt(uint64(maxItems) * 8)
403 if !ok {
404 b.dirEnd = maxIntValue()
405 b.dataOffset = 0
406 return
407 }
408 dirEnd, ok := checkedAlign8(dirSize)
409 if !ok {
410 b.dirEnd = maxIntValue()
411 b.dataOffset = 0
412 return
413 }
414 b.dirEnd = dirEnd
415 b.dataOffset = 0
416 }
417
418 // NewBatchBuilder creates a new batch builder. buf must be large enough for
419 // maxItems*8 (directory) + packed data.
420 func NewBatchBuilder(buf []byte, maxItems uint32) *BatchBuilder {
421 b := &BatchBuilder{}
422 b.Reset(buf, maxItems)
423 return b
424 }
425
426 // Add appends an item payload. Handles alignment padding.
427 func (b *BatchBuilder) Add(item []byte) error {
428 if b.itemCount >= b.maxItems {
429 return ErrOverflow
430 }
431
432 alignedOff, ok := checkedAlign8(b.dataOffset)
433 if !ok {
434 return ErrOverflow
435 }
436 absPos, ok := checkedAddInt(b.dirEnd, alignedOff)
437 if !ok {
438 return ErrOverflow
439 }
440
441 itemEnd, ok := checkedAddInt(absPos, len(item))
442 if !ok || itemEnd > len(b.buf) {
443 return ErrOverflow
444 }
445
446 // Zero alignment padding.
447 if alignedOff > b.dataOffset {
448 padStart, ok := checkedAddInt(b.dirEnd, b.dataOffset)
449 if !ok {
450 return ErrOverflow
451 }
452 padEnd, ok := checkedAddInt(b.dirEnd, alignedOff)
453 if !ok {
454 return ErrOverflow
455 }
456 clear(b.buf[padStart:padEnd])
457 }
458
459 copy(b.buf[absPos:], item)
460
461 // Write directory entry.
462 idx, ok := checkedInt(uint64(b.itemCount) * 8)
463 if !ok {
464 return ErrOverflow
465 }
466 alignedOff32, ok := checkedU32Int(alignedOff)
467 if !ok {
468 return ErrOverflow
469 }
470 itemLen32, ok := checkedU32Int(len(item))
471 if !ok {
472 return ErrOverflow
473 }
474 ne.PutUint32(b.buf[idx:idx+4], alignedOff32)
475 ne.PutUint32(b.buf[idx+4:idx+8], itemLen32)
476
477 b.dataOffset, ok = checkedAddInt(alignedOff, len(item))
478 if !ok {
479 return ErrOverflow
480 }
481 b.itemCount++
482 return nil
483 }
484
485 // Finish finalizes the batch. Returns (totalPayloadSize, itemCount).
486 // Compacts if fewer items were added than maxItems.
487 func (b *BatchBuilder) Finish() (int, uint32) {
488 count := b.itemCount
489 dirSize, ok := checkedInt(uint64(count) * 8)
490 if !ok {
491 return 0, count
492 }
493 finalDirAligned, ok := checkedAlign8(dirSize)
494 if !ok {
495 return 0, count
496 }
497
498 if finalDirAligned < b.dirEnd && b.dataOffset > 0 {
499 dataEnd, ok := checkedAddInt(b.dirEnd, b.dataOffset)
500 if !ok {
501 return 0, count
502 }
503 // Shift packed data left.
504 copy(b.buf[finalDirAligned:], b.buf[b.dirEnd:dataEnd])
505 }
506
507 alignedData, ok := checkedAlign8(b.dataOffset)
508 if !ok {
509 return 0, count
510 }
511 total, ok := checkedAddInt(finalDirAligned, alignedData)
512 if !ok {
513 return 0, count
514 }
515 return total, count
516 }
517
518 // ---------------------------------------------------------------------------
519 // Hello payload (44 bytes)
520 // ---------------------------------------------------------------------------
521
522 // Hello is the client handshake payload (44 bytes on the wire).
523 type Hello struct {
524 LayoutVersion uint16
525 Flags uint16
526 SupportedProfiles uint32
527 PreferredProfiles uint32
528 MaxRequestPayloadBytes uint32
529 MaxRequestBatchItems uint32
530 MaxResponsePayloadBytes uint32
531 MaxResponseBatchItems uint32
532 AuthToken uint64
533 PacketSize uint32
534 }
535
536 // Encode writes the Hello payload into buf. Returns 44 on success, 0 if
537 // buf is too small.
538 func (h *Hello) Encode(buf []byte) int {
539 if len(buf) < helloSize {
540 return 0
541 }
542 ne.PutUint16(buf[0:2], h.LayoutVersion)
543 ne.PutUint16(buf[2:4], h.Flags)
544 ne.PutUint32(buf[4:8], h.SupportedProfiles)
545 ne.PutUint32(buf[8:12], h.PreferredProfiles)
546 ne.PutUint32(buf[12:16], h.MaxRequestPayloadBytes)
547 ne.PutUint32(buf[16:20], h.MaxRequestBatchItems)
548 ne.PutUint32(buf[20:24], h.MaxResponsePayloadBytes)
549 ne.PutUint32(buf[24:28], h.MaxResponseBatchItems)
550 ne.PutUint32(buf[28:32], 0) // padding
551 ne.PutUint64(buf[32:40], h.AuthToken)
552 ne.PutUint32(buf[40:44], h.PacketSize)
553 return helloSize
554 }
555
556 // DecodeHello decodes a Hello payload from buf. Validates layout_version.
557 func DecodeHello(buf []byte) (Hello, error) {
558 if len(buf) < helloSize {
559 return Hello{}, ErrTruncated
560 }
561 h := Hello{
562 LayoutVersion: ne.Uint16(buf[0:2]),
563 Flags: ne.Uint16(buf[2:4]),
564 SupportedProfiles: ne.Uint32(buf[4:8]),
565 PreferredProfiles: ne.Uint32(buf[8:12]),
566 MaxRequestPayloadBytes: ne.Uint32(buf[12:16]),
567 MaxRequestBatchItems: ne.Uint32(buf[16:20]),
568 MaxResponsePayloadBytes: ne.Uint32(buf[20:24]),
569 MaxResponseBatchItems: ne.Uint32(buf[24:28]),
570 // buf[28:32] is reserved padding, must be zero.
571 AuthToken: ne.Uint64(buf[32:40]),
572 PacketSize: ne.Uint32(buf[40:44]),
573 }
574 if h.LayoutVersion != 1 {
575 return Hello{}, ErrBadLayout
576 }
577 // Validate padding bytes 28..32 are zero
578 if ne.Uint32(buf[28:32]) != 0 {
579 return Hello{}, ErrBadLayout
580 }
581 return h, nil
582 }
583
584 // ---------------------------------------------------------------------------
585 // Hello-ack payload (44 bytes)
586 // ---------------------------------------------------------------------------
587
588 // HelloAck is the server handshake response payload (48 bytes on the wire).
589 type HelloAck struct {
590 LayoutVersion uint16
591 Flags uint16
592 ServerSupportedProfiles uint32
593 IntersectionProfiles uint32
594 SelectedProfile uint32
595 AgreedMaxRequestPayloadBytes uint32
596 AgreedMaxRequestBatchItems uint32
597 AgreedMaxResponsePayloadBytes uint32
598 AgreedMaxResponseBatchItems uint32
599 AgreedPacketSize uint32
600 SessionID uint64
601 }
602
603 // Encode writes the HelloAck payload into buf. Returns 48 on success, 0
604 // if buf is too small.
605 func (h *HelloAck) Encode(buf []byte) int {
606 if len(buf) < helloAckSize {
607 return 0
608 }
609 ne.PutUint16(buf[0:2], h.LayoutVersion)
610 ne.PutUint16(buf[2:4], h.Flags)
611 ne.PutUint32(buf[4:8], h.ServerSupportedProfiles)
612 ne.PutUint32(buf[8:12], h.IntersectionProfiles)
613 ne.PutUint32(buf[12:16], h.SelectedProfile)
614 ne.PutUint32(buf[16:20], h.AgreedMaxRequestPayloadBytes)
615 ne.PutUint32(buf[20:24], h.AgreedMaxRequestBatchItems)
616 ne.PutUint32(buf[24:28], h.AgreedMaxResponsePayloadBytes)
617 ne.PutUint32(buf[28:32], h.AgreedMaxResponseBatchItems)
618 ne.PutUint32(buf[32:36], h.AgreedPacketSize)
619 ne.PutUint32(buf[36:40], 0) // padding
620 ne.PutUint64(buf[40:48], h.SessionID)
621 return helloAckSize
622 }
623
624 // DecodeHelloAck decodes a HelloAck payload from buf. Validates
625 // layout_version.
626 func DecodeHelloAck(buf []byte) (HelloAck, error) {
627 if len(buf) < helloAckSize {
628 return HelloAck{}, ErrTruncated
629 }
630 h := HelloAck{
631 LayoutVersion: ne.Uint16(buf[0:2]),
632 Flags: ne.Uint16(buf[2:4]),
633 ServerSupportedProfiles: ne.Uint32(buf[4:8]),
634 IntersectionProfiles: ne.Uint32(buf[8:12]),
635 SelectedProfile: ne.Uint32(buf[12:16]),
636 AgreedMaxRequestPayloadBytes: ne.Uint32(buf[16:20]),
637 AgreedMaxRequestBatchItems: ne.Uint32(buf[20:24]),
638 AgreedMaxResponsePayloadBytes: ne.Uint32(buf[24:28]),
639 AgreedMaxResponseBatchItems: ne.Uint32(buf[28:32]),
640 AgreedPacketSize: ne.Uint32(buf[32:36]),
641 // skip padding at 36:40
642 SessionID: ne.Uint64(buf[40:48]),
643 }
644 if h.LayoutVersion != 1 {
645 return HelloAck{}, ErrBadLayout
646 }
647 if h.Flags != 0 {
648 return HelloAck{}, ErrBadLayout
649 }
650 return h, nil
651 }