Skip to content

Commit abb3466

Browse files
committed
TUN-8638: Add datagram v3 serializers and deserializers
Closes TUN-8638
1 parent a3ee49d commit abb3466

File tree

5 files changed

+872
-0
lines changed

5 files changed

+872
-0
lines changed

quic/v3/datagram.go

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
package v3
2+
3+
import (
4+
"encoding/binary"
5+
"net/netip"
6+
"time"
7+
)
8+
9+
type DatagramType byte
10+
11+
const (
12+
// UDP Registration
13+
UDPSessionRegistrationType DatagramType = 0x0
14+
// UDP Session Payload
15+
UDPSessionPayloadType DatagramType = 0x1
16+
// DatagramTypeICMP (supporting both ICMPv4 and ICMPv6)
17+
ICMPType DatagramType = 0x2
18+
// UDP Session Registration Response
19+
UDPSessionRegistrationResponseType DatagramType = 0x3
20+
)
21+
22+
const (
23+
// Total number of bytes representing the [DatagramType]
24+
datagramTypeLen = 1
25+
26+
// 1280 is the default datagram packet length used before MTU discovery: https://github.com/quic-go/quic-go/blob/v0.45.0/internal/protocol/params.go#L12
27+
maxDatagramLen = 1280
28+
)
29+
30+
func parseDatagramType(data []byte) (DatagramType, error) {
31+
if len(data) < datagramTypeLen {
32+
return 0, ErrDatagramHeaderTooSmall
33+
}
34+
return DatagramType(data[0]), nil
35+
}
36+
37+
// UDPSessionRegistrationDatagram handles a request to initialize a UDP session on the remote client.
38+
type UDPSessionRegistrationDatagram struct {
39+
RequestID RequestID
40+
Dest netip.AddrPort
41+
Traced bool
42+
IdleDurationHint time.Duration
43+
Payload []byte
44+
}
45+
46+
const (
47+
sessionRegistrationFlagsIPMask byte = 0b0000_0001
48+
sessionRegistrationFlagsTracedMask byte = 0b0000_0010
49+
sessionRegistrationFlagsBundledMask byte = 0b0000_0100
50+
51+
sessionRegistrationIPv4DatagramHeaderLen = datagramTypeLen +
52+
1 + // Flag length
53+
2 + // Destination port length
54+
2 + // Idle duration seconds length
55+
datagramRequestIdLen + // Request ID length
56+
4 // IPv4 address length
57+
58+
// The IPv4 and IPv6 address share space, so adding 12 to the header length gets the space taken by the IPv6 field.
59+
sessionRegistrationIPv6DatagramHeaderLen = sessionRegistrationIPv4DatagramHeaderLen + 12
60+
)
61+
62+
// The datagram structure for UDPSessionRegistrationDatagram is:
63+
//
64+
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
65+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
66+
// 0| Type | Flags | Destination Port |
67+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
68+
// 4| Idle Duration Seconds | |
69+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
70+
// 8| |
71+
// + Session Identifier +
72+
// 12| (16 Bytes) |
73+
// + +
74+
// 16| |
75+
// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
76+
// 20| | Destination IPv4 Address |
77+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - - - - - - - -+
78+
// 24| Destination IPv4 Address cont | |
79+
// +- - - - - - - - - - - - - - - - +
80+
// 28| Destination IPv6 Address |
81+
// + (extension of IPv4 region) +
82+
// 32| |
83+
// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
84+
// 36| | |
85+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
86+
// . .
87+
// . Bundle Payload .
88+
// . .
89+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90+
91+
func (s *UDPSessionRegistrationDatagram) MarshalBinary() (data []byte, err error) {
92+
ipv6 := s.Dest.Addr().Is6()
93+
var flags byte
94+
if s.Traced {
95+
flags |= sessionRegistrationFlagsTracedMask
96+
}
97+
hasPayload := len(s.Payload) > 0
98+
if hasPayload {
99+
flags |= sessionRegistrationFlagsBundledMask
100+
}
101+
var maxPayloadLen int
102+
if ipv6 {
103+
maxPayloadLen = maxDatagramLen - sessionRegistrationIPv6DatagramHeaderLen
104+
flags |= sessionRegistrationFlagsIPMask
105+
} else {
106+
maxPayloadLen = maxDatagramLen - sessionRegistrationIPv4DatagramHeaderLen
107+
}
108+
// Make sure that the payload being bundled can actually fit in the payload destination
109+
if len(s.Payload) > maxPayloadLen {
110+
return nil, wrapMarshalErr(ErrDatagramPayloadTooLarge)
111+
}
112+
// Allocate the buffer with the right size for the destination IP family
113+
if ipv6 {
114+
data = make([]byte, sessionRegistrationIPv6DatagramHeaderLen+len(s.Payload))
115+
} else {
116+
data = make([]byte, sessionRegistrationIPv4DatagramHeaderLen+len(s.Payload))
117+
}
118+
data[0] = byte(UDPSessionRegistrationType)
119+
data[1] = byte(flags)
120+
binary.BigEndian.PutUint16(data[2:4], s.Dest.Port())
121+
binary.BigEndian.PutUint16(data[4:6], uint16(s.IdleDurationHint.Seconds()))
122+
err = s.RequestID.MarshalBinaryTo(data[6:22])
123+
if err != nil {
124+
return nil, wrapMarshalErr(err)
125+
}
126+
var end int
127+
if ipv6 {
128+
copy(data[22:38], s.Dest.Addr().AsSlice())
129+
end = 38
130+
} else {
131+
copy(data[22:26], s.Dest.Addr().AsSlice())
132+
end = 26
133+
}
134+
135+
if hasPayload {
136+
copy(data[end:], s.Payload)
137+
}
138+
139+
return data, nil
140+
}
141+
142+
func (s *UDPSessionRegistrationDatagram) UnmarshalBinary(data []byte) error {
143+
datagramType, err := parseDatagramType(data)
144+
if err != nil {
145+
return err
146+
}
147+
if datagramType != UDPSessionRegistrationType {
148+
return wrapUnmarshalErr(ErrInvalidDatagramType)
149+
}
150+
151+
requestID, err := RequestIDFromSlice(data[6:22])
152+
if err != nil {
153+
return wrapUnmarshalErr(err)
154+
}
155+
156+
traced := (data[1] & sessionRegistrationFlagsTracedMask) == sessionRegistrationFlagsTracedMask
157+
bundled := (data[1] & sessionRegistrationFlagsBundledMask) == sessionRegistrationFlagsBundledMask
158+
ipv6 := (data[1] & sessionRegistrationFlagsIPMask) == sessionRegistrationFlagsIPMask
159+
160+
port := binary.BigEndian.Uint16(data[2:4])
161+
var datagramHeaderSize int
162+
var dest netip.AddrPort
163+
if ipv6 {
164+
datagramHeaderSize = sessionRegistrationIPv6DatagramHeaderLen
165+
dest = netip.AddrPortFrom(netip.AddrFrom16([16]byte(data[22:38])), port)
166+
} else {
167+
datagramHeaderSize = sessionRegistrationIPv4DatagramHeaderLen
168+
dest = netip.AddrPortFrom(netip.AddrFrom4([4]byte(data[22:26])), port)
169+
}
170+
171+
idle := time.Duration(binary.BigEndian.Uint16(data[4:6])) * time.Second
172+
173+
var payload []byte
174+
if bundled && len(data) >= datagramHeaderSize && len(data[datagramHeaderSize:]) > 0 {
175+
payload = data[datagramHeaderSize:]
176+
}
177+
178+
*s = UDPSessionRegistrationDatagram{
179+
RequestID: requestID,
180+
Dest: dest,
181+
Traced: traced,
182+
IdleDurationHint: idle,
183+
Payload: payload,
184+
}
185+
return nil
186+
}
187+
188+
// UDPSessionPayloadDatagram provides the payload for a session to be send to either the origin or the client.
189+
type UDPSessionPayloadDatagram struct {
190+
RequestID RequestID
191+
Payload []byte
192+
}
193+
194+
const (
195+
datagramPayloadHeaderLen = datagramTypeLen + datagramRequestIdLen
196+
197+
// The maximum size that a proxied UDP payload can be in a [UDPSessionPayloadDatagram]
198+
maxPayloadPlusHeaderLen = maxDatagramLen - datagramPayloadHeaderLen
199+
)
200+
201+
// The datagram structure for UDPSessionPayloadDatagram is:
202+
//
203+
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
204+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
205+
// 0| Type | |
206+
// +-+-+-+-+-+-+-+-+ +
207+
// 4| |
208+
// + +
209+
// 8| Session Identifier |
210+
// + (16 Bytes) +
211+
// 12| |
212+
// + +-+-+-+-+-+-+-+-+
213+
// 16| | |
214+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
215+
// . .
216+
// . Payload .
217+
// . .
218+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
219+
220+
// MarshalPayloadHeaderTo provides a way to insert the Session Payload header into an already existing byte slice
221+
// without having to allocate and copy the payload into the destination.
222+
//
223+
// This method should be used in-place of MarshalBinary which will allocate in-place the required byte array to return.
224+
func MarshalPayloadHeaderTo(requestID RequestID, payload []byte) error {
225+
if len(payload) < 17 {
226+
return wrapMarshalErr(ErrDatagramPayloadHeaderTooSmall)
227+
}
228+
payload[0] = byte(UDPSessionPayloadType)
229+
return requestID.MarshalBinaryTo(payload[1:17])
230+
}
231+
232+
func (s *UDPSessionPayloadDatagram) UnmarshalBinary(data []byte) error {
233+
datagramType, err := parseDatagramType(data)
234+
if err != nil {
235+
return err
236+
}
237+
if datagramType != UDPSessionPayloadType {
238+
return wrapUnmarshalErr(ErrInvalidDatagramType)
239+
}
240+
241+
// Make sure that the slice provided is the right size to be parsed.
242+
if len(data) < 17 || len(data) > maxPayloadPlusHeaderLen {
243+
return wrapUnmarshalErr(ErrDatagramPayloadInvalidSize)
244+
}
245+
246+
requestID, err := RequestIDFromSlice(data[1:17])
247+
if err != nil {
248+
return wrapUnmarshalErr(err)
249+
}
250+
251+
*s = UDPSessionPayloadDatagram{
252+
RequestID: requestID,
253+
Payload: data[17:],
254+
}
255+
return nil
256+
}
257+
258+
// UDPSessionRegistrationResponseDatagram is used to either return a successful registration or error to the client
259+
// that requested the registration of a UDP session.
260+
type UDPSessionRegistrationResponseDatagram struct {
261+
RequestID RequestID
262+
ResponseType SessionRegistrationResp
263+
ErrorMsg string
264+
}
265+
266+
const (
267+
datagramRespTypeLen = 1
268+
datagramRespErrMsgLen = 2
269+
270+
datagramSessionRegistrationResponseLen = datagramTypeLen + datagramRespTypeLen + datagramRequestIdLen + datagramRespErrMsgLen
271+
272+
// The maximum size that an error message can be in a [UDPSessionRegistrationResponseDatagram].
273+
maxResponseErrorMessageLen = maxDatagramLen - datagramSessionRegistrationResponseLen
274+
)
275+
276+
// SessionRegistrationResp represents all of the responses that a UDP session registration response
277+
// can return back to the client.
278+
type SessionRegistrationResp byte
279+
280+
const (
281+
// Session was received and is ready to proxy.
282+
ResponseOk SessionRegistrationResp = 0x00
283+
// Session registration was unable to reach the requested origin destination.
284+
ResponseDestinationUnreachable SessionRegistrationResp = 0x01
285+
// Session registration was unable to bind to a local UDP socket.
286+
ResponseUnableToBindSocket SessionRegistrationResp = 0x02
287+
// Session registration failed with an unexpected error but provided a message.
288+
ResponseErrorWithMsg SessionRegistrationResp = 0xff
289+
)
290+
291+
// The datagram structure for UDPSessionRegistrationResponseDatagram is:
292+
//
293+
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
294+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
295+
// 0| Type | Resp Type | |
296+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
297+
// 4| |
298+
// + Session Identifier +
299+
// 8| (16 Bytes) |
300+
// + +
301+
// 12| |
302+
// + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
303+
// 16| | Error Length |
304+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
305+
// . .
306+
// . .
307+
// . .
308+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
309+
310+
func (s *UDPSessionRegistrationResponseDatagram) MarshalBinary() (data []byte, err error) {
311+
if len(s.ErrorMsg) > maxResponseErrorMessageLen {
312+
return nil, wrapMarshalErr(ErrDatagramResponseMsgInvalidSize)
313+
}
314+
errMsgLen := uint16(len(s.ErrorMsg))
315+
316+
data = make([]byte, datagramSessionRegistrationResponseLen+errMsgLen)
317+
data[0] = byte(UDPSessionRegistrationResponseType)
318+
data[1] = byte(s.ResponseType)
319+
err = s.RequestID.MarshalBinaryTo(data[2:18])
320+
if err != nil {
321+
return nil, wrapMarshalErr(err)
322+
}
323+
324+
if errMsgLen > 0 {
325+
binary.BigEndian.PutUint16(data[18:20], errMsgLen)
326+
copy(data[20:], []byte(s.ErrorMsg))
327+
}
328+
329+
return data, nil
330+
}
331+
332+
func (s *UDPSessionRegistrationResponseDatagram) UnmarshalBinary(data []byte) error {
333+
datagramType, err := parseDatagramType(data)
334+
if err != nil {
335+
return wrapUnmarshalErr(err)
336+
}
337+
if datagramType != UDPSessionRegistrationResponseType {
338+
return wrapUnmarshalErr(ErrInvalidDatagramType)
339+
}
340+
341+
if len(data) < datagramSessionRegistrationResponseLen {
342+
return wrapUnmarshalErr(ErrDatagramResponseInvalidSize)
343+
}
344+
345+
respType := SessionRegistrationResp(data[1])
346+
347+
requestID, err := RequestIDFromSlice(data[2:18])
348+
if err != nil {
349+
return wrapUnmarshalErr(err)
350+
}
351+
352+
errMsgLen := binary.BigEndian.Uint16(data[18:20])
353+
if errMsgLen > maxResponseErrorMessageLen {
354+
return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeMaximum)
355+
}
356+
357+
if len(data[20:]) < int(errMsgLen) {
358+
return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeDatagram)
359+
}
360+
361+
var errMsg string
362+
if errMsgLen > 0 {
363+
errMsg = string(data[20:])
364+
}
365+
366+
*s = UDPSessionRegistrationResponseDatagram{
367+
RequestID: requestID,
368+
ResponseType: respType,
369+
ErrorMsg: errMsg,
370+
}
371+
return nil
372+
}

quic/v3/datagram_errors.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package v3
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
var (
9+
ErrInvalidDatagramType error = errors.New("invalid datagram type expected")
10+
ErrDatagramHeaderTooSmall error = fmt.Errorf("datagram should have at least %d bytes", datagramTypeLen)
11+
ErrDatagramPayloadTooLarge error = errors.New("payload length is too large to be bundled in datagram")
12+
ErrDatagramPayloadHeaderTooSmall error = errors.New("payload length is too small to fit the datagram header")
13+
ErrDatagramPayloadInvalidSize error = errors.New("datagram provided is an invalid size")
14+
ErrDatagramResponseMsgInvalidSize error = errors.New("datagram response message is an invalid size")
15+
ErrDatagramResponseInvalidSize error = errors.New("datagram response is an invalid size")
16+
ErrDatagramResponseMsgTooLargeMaximum error = fmt.Errorf("datagram response error message length exceeds the length of the datagram maximum: %d", maxResponseErrorMessageLen)
17+
ErrDatagramResponseMsgTooLargeDatagram error = fmt.Errorf("datagram response error message length exceeds the length of the provided datagram")
18+
)
19+
20+
func wrapMarshalErr(err error) error {
21+
return fmt.Errorf("datagram marshal error: %w", err)
22+
}
23+
24+
func wrapUnmarshalErr(err error) error {
25+
return fmt.Errorf("datagram unmarshal error: %w", err)
26+
}

0 commit comments

Comments
 (0)