package v3 import ( "encoding/binary" "net/netip" "time" ) type DatagramType byte const ( // UDP Registration UDPSessionRegistrationType DatagramType = 0x0 // UDP Session Payload UDPSessionPayloadType DatagramType = 0x1 // DatagramTypeICMP (supporting both ICMPv4 and ICMPv6) ICMPType DatagramType = 0x2 // UDP Session Registration Response UDPSessionRegistrationResponseType DatagramType = 0x3 ) const ( // Total number of bytes representing the [DatagramType] datagramTypeLen = 1 // 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 maxDatagramLen = 1280 ) func parseDatagramType(data []byte) (DatagramType, error) { if len(data) < datagramTypeLen { return 0, ErrDatagramHeaderTooSmall } return DatagramType(data[0]), nil } // UDPSessionRegistrationDatagram handles a request to initialize a UDP session on the remote client. type UDPSessionRegistrationDatagram struct { RequestID RequestID Dest netip.AddrPort Traced bool IdleDurationHint time.Duration Payload []byte } const ( sessionRegistrationFlagsIPMask byte = 0b0000_0001 sessionRegistrationFlagsTracedMask byte = 0b0000_0010 sessionRegistrationFlagsBundledMask byte = 0b0000_0100 sessionRegistrationIPv4DatagramHeaderLen = datagramTypeLen + 1 + // Flag length 2 + // Destination port length 2 + // Idle duration seconds length datagramRequestIdLen + // Request ID length 4 // IPv4 address length // The IPv4 and IPv6 address share space, so adding 12 to the header length gets the space taken by the IPv6 field. sessionRegistrationIPv6DatagramHeaderLen = sessionRegistrationIPv4DatagramHeaderLen + 12 ) // The datagram structure for UDPSessionRegistrationDatagram is: // // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | Flags | Destination Port | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 4| Idle Duration Seconds | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // 8| | // + Session Identifier + // 12| (16 Bytes) | // + + // 16| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 20| | Destination IPv4 Address | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - - - - - - - -+ // 24| Destination IPv4 Address cont | | // +- - - - - - - - - - - - - - - - + // 28| Destination IPv6 Address | // + (extension of IPv4 region) + // 32| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 36| | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // . . // . Bundle Payload . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ func (s *UDPSessionRegistrationDatagram) MarshalBinary() (data []byte, err error) { ipv6 := s.Dest.Addr().Is6() var flags byte if s.Traced { flags |= sessionRegistrationFlagsTracedMask } hasPayload := len(s.Payload) > 0 if hasPayload { flags |= sessionRegistrationFlagsBundledMask } var maxPayloadLen int if ipv6 { maxPayloadLen = maxDatagramLen - sessionRegistrationIPv6DatagramHeaderLen flags |= sessionRegistrationFlagsIPMask } else { maxPayloadLen = maxDatagramLen - sessionRegistrationIPv4DatagramHeaderLen } // Make sure that the payload being bundled can actually fit in the payload destination if len(s.Payload) > maxPayloadLen { return nil, wrapMarshalErr(ErrDatagramPayloadTooLarge) } // Allocate the buffer with the right size for the destination IP family if ipv6 { data = make([]byte, sessionRegistrationIPv6DatagramHeaderLen+len(s.Payload)) } else { data = make([]byte, sessionRegistrationIPv4DatagramHeaderLen+len(s.Payload)) } data[0] = byte(UDPSessionRegistrationType) data[1] = byte(flags) binary.BigEndian.PutUint16(data[2:4], s.Dest.Port()) binary.BigEndian.PutUint16(data[4:6], uint16(s.IdleDurationHint.Seconds())) err = s.RequestID.MarshalBinaryTo(data[6:22]) if err != nil { return nil, wrapMarshalErr(err) } var end int if ipv6 { copy(data[22:38], s.Dest.Addr().AsSlice()) end = 38 } else { copy(data[22:26], s.Dest.Addr().AsSlice()) end = 26 } if hasPayload { copy(data[end:], s.Payload) } return data, nil } func (s *UDPSessionRegistrationDatagram) UnmarshalBinary(data []byte) error { datagramType, err := parseDatagramType(data) if err != nil { return err } if datagramType != UDPSessionRegistrationType { return wrapUnmarshalErr(ErrInvalidDatagramType) } requestID, err := RequestIDFromSlice(data[6:22]) if err != nil { return wrapUnmarshalErr(err) } traced := (data[1] & sessionRegistrationFlagsTracedMask) == sessionRegistrationFlagsTracedMask bundled := (data[1] & sessionRegistrationFlagsBundledMask) == sessionRegistrationFlagsBundledMask ipv6 := (data[1] & sessionRegistrationFlagsIPMask) == sessionRegistrationFlagsIPMask port := binary.BigEndian.Uint16(data[2:4]) var datagramHeaderSize int var dest netip.AddrPort if ipv6 { datagramHeaderSize = sessionRegistrationIPv6DatagramHeaderLen dest = netip.AddrPortFrom(netip.AddrFrom16([16]byte(data[22:38])), port) } else { datagramHeaderSize = sessionRegistrationIPv4DatagramHeaderLen dest = netip.AddrPortFrom(netip.AddrFrom4([4]byte(data[22:26])), port) } idle := time.Duration(binary.BigEndian.Uint16(data[4:6])) * time.Second var payload []byte if bundled && len(data) >= datagramHeaderSize && len(data[datagramHeaderSize:]) > 0 { payload = data[datagramHeaderSize:] } *s = UDPSessionRegistrationDatagram{ RequestID: requestID, Dest: dest, Traced: traced, IdleDurationHint: idle, Payload: payload, } return nil } // UDPSessionPayloadDatagram provides the payload for a session to be send to either the origin or the client. type UDPSessionPayloadDatagram struct { RequestID RequestID Payload []byte } const ( datagramPayloadHeaderLen = datagramTypeLen + datagramRequestIdLen // The maximum size that a proxied UDP payload can be in a [UDPSessionPayloadDatagram] maxPayloadPlusHeaderLen = maxDatagramLen - datagramPayloadHeaderLen ) // The datagram structure for UDPSessionPayloadDatagram is: // // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | | // +-+-+-+-+-+-+-+-+ + // 4| | // + + // 8| Session Identifier | // + (16 Bytes) + // 12| | // + +-+-+-+-+-+-+-+-+ // 16| | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // . . // . Payload . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // MarshalPayloadHeaderTo provides a way to insert the Session Payload header into an already existing byte slice // without having to allocate and copy the payload into the destination. // // This method should be used in-place of MarshalBinary which will allocate in-place the required byte array to return. func MarshalPayloadHeaderTo(requestID RequestID, payload []byte) error { if len(payload) < 17 { return wrapMarshalErr(ErrDatagramPayloadHeaderTooSmall) } payload[0] = byte(UDPSessionPayloadType) return requestID.MarshalBinaryTo(payload[1:17]) } func (s *UDPSessionPayloadDatagram) UnmarshalBinary(data []byte) error { datagramType, err := parseDatagramType(data) if err != nil { return err } if datagramType != UDPSessionPayloadType { return wrapUnmarshalErr(ErrInvalidDatagramType) } // Make sure that the slice provided is the right size to be parsed. if len(data) < 17 || len(data) > maxPayloadPlusHeaderLen { return wrapUnmarshalErr(ErrDatagramPayloadInvalidSize) } requestID, err := RequestIDFromSlice(data[1:17]) if err != nil { return wrapUnmarshalErr(err) } *s = UDPSessionPayloadDatagram{ RequestID: requestID, Payload: data[17:], } return nil } // UDPSessionRegistrationResponseDatagram is used to either return a successful registration or error to the client // that requested the registration of a UDP session. type UDPSessionRegistrationResponseDatagram struct { RequestID RequestID ResponseType SessionRegistrationResp ErrorMsg string } const ( datagramRespTypeLen = 1 datagramRespErrMsgLen = 2 datagramSessionRegistrationResponseLen = datagramTypeLen + datagramRespTypeLen + datagramRequestIdLen + datagramRespErrMsgLen // The maximum size that an error message can be in a [UDPSessionRegistrationResponseDatagram]. maxResponseErrorMessageLen = maxDatagramLen - datagramSessionRegistrationResponseLen ) // SessionRegistrationResp represents all of the responses that a UDP session registration response // can return back to the client. type SessionRegistrationResp byte const ( // Session was received and is ready to proxy. ResponseOk SessionRegistrationResp = 0x00 // Session registration was unable to reach the requested origin destination. ResponseDestinationUnreachable SessionRegistrationResp = 0x01 // Session registration was unable to bind to a local UDP socket. ResponseUnableToBindSocket SessionRegistrationResp = 0x02 // Session registration failed with an unexpected error but provided a message. ResponseErrorWithMsg SessionRegistrationResp = 0xff ) // The datagram structure for UDPSessionRegistrationResponseDatagram is: // // 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | Resp Type | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // 4| | // + Session Identifier + // 8| (16 Bytes) | // + + // 12| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 16| | Error Length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . . // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ func (s *UDPSessionRegistrationResponseDatagram) MarshalBinary() (data []byte, err error) { if len(s.ErrorMsg) > maxResponseErrorMessageLen { return nil, wrapMarshalErr(ErrDatagramResponseMsgInvalidSize) } errMsgLen := uint16(len(s.ErrorMsg)) data = make([]byte, datagramSessionRegistrationResponseLen+errMsgLen) data[0] = byte(UDPSessionRegistrationResponseType) data[1] = byte(s.ResponseType) err = s.RequestID.MarshalBinaryTo(data[2:18]) if err != nil { return nil, wrapMarshalErr(err) } if errMsgLen > 0 { binary.BigEndian.PutUint16(data[18:20], errMsgLen) copy(data[20:], []byte(s.ErrorMsg)) } return data, nil } func (s *UDPSessionRegistrationResponseDatagram) UnmarshalBinary(data []byte) error { datagramType, err := parseDatagramType(data) if err != nil { return wrapUnmarshalErr(err) } if datagramType != UDPSessionRegistrationResponseType { return wrapUnmarshalErr(ErrInvalidDatagramType) } if len(data) < datagramSessionRegistrationResponseLen { return wrapUnmarshalErr(ErrDatagramResponseInvalidSize) } respType := SessionRegistrationResp(data[1]) requestID, err := RequestIDFromSlice(data[2:18]) if err != nil { return wrapUnmarshalErr(err) } errMsgLen := binary.BigEndian.Uint16(data[18:20]) if errMsgLen > maxResponseErrorMessageLen { return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeMaximum) } if len(data[20:]) < int(errMsgLen) { return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeDatagram) } var errMsg string if errMsgLen > 0 { errMsg = string(data[20:]) } *s = UDPSessionRegistrationResponseDatagram{ RequestID: requestID, ResponseType: respType, ErrorMsg: errMsg, } return nil }