From 7beb0327c43aae4df85e13a00cf13b6b3dbfd37f Mon Sep 17 00:00:00 2001 From: soypat Date: Tue, 24 Dec 2024 20:18:06 -0300 Subject: [PATCH] add TCP options, more validation --- lneto/frames.go | 83 +++++++++++++++++++++-------- lneto/tcp/definitions.go | 112 ++++++++++++++++++++++++++++++++++++++- lneto/tcp/stringers.go | 71 ++++++++++++++++++++++++- lneto/validation.go | 14 +++++ 4 files changed, 257 insertions(+), 23 deletions(-) diff --git a/lneto/frames.go b/lneto/frames.go index 07b3bee..74887f0 100644 --- a/lneto/frames.go +++ b/lneto/frames.go @@ -2,34 +2,69 @@ package lneto import ( "encoding/binary" + "errors" "github.com/soypat/tseq/lneto/tcp" ) -func NewEthFrame(buf []byte) EthFrame { - return EthFrame{buf: mustBufferFrameLen(buf, sizeHeaderEthNoVLAN, "Ethernet")} -} -func NewARPFrame(buf []byte) ARPFrame { - return ARPFrame{buf: mustBufferFrameLen(buf, sizeHeaderARPv4, "ARPv4")} -} -func NewIPv4Frame(buf []byte) IPv4Frame { - return IPv4Frame{buf: mustBufferFrameLen(buf, sizeHeaderIPv4, "IPv4")} -} -func NewIPv6Frame(buf []byte) IPv6Frame { - return IPv6Frame{buf: mustBufferFrameLen(buf, sizeHeaderIPv6, "IPv6")} -} -func NewTCPFrame(buf []byte) TCPFrame { - return TCPFrame{buf: mustBufferFrameLen(buf, sizeHeaderIPv4, "TCP")} -} -func NewUDPFrame(buf []byte) UDPFrame { - return UDPFrame{buf: mustBufferFrameLen(buf, sizeHeaderUDP, "UDP")} +// NewEthFrame returns a EthFrame with data set to buf. +// Users should still call [EthFrame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewEthFrame(buf []byte) (EthFrame, error) { + if len(buf) < sizeHeaderEthNoVLAN { + return EthFrame{buf: nil}, errors.New("ethernet packet too short") + } + return EthFrame{buf: buf}, nil } -func mustBufferFrameLen(b []byte, minLen int, name string) []byte { - if len(b) < minLen { - panic(name + " frame too short") +// NewARPFrame returns a ARPFrame with data set to buf. +// Users should still call [ARPFrame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewARPFrame(buf []byte) (ARPFrame, error) { + if len(buf) < sizeHeaderARPv4 { + return ARPFrame{buf: nil}, errors.New("ARP packet too short") } - return b + return ARPFrame{buf: buf}, nil +} + +// NewIPv4Frame returns a new IPv4Frame with data set to buf. +// Users should still call [IPv4Frame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewIPv4Frame(buf []byte) (IPv4Frame, error) { + if len(buf) < sizeHeaderIPv4 { + return IPv4Frame{buf: nil}, errors.New("IPv4 packet too short") + } + return IPv4Frame{buf: buf}, nil +} + +// NewIPv6Frame returns a new IPv6Frame with data set to buf. +// Users should still call [IPv6Frame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewIPv6Frame(buf []byte) (IPv6Frame, error) { + if len(buf) < sizeHeaderIPv6 { + return IPv6Frame{buf: nil}, errors.New("IPv6 packet too short") + } + return IPv6Frame{buf: buf}, nil +} + +// NewTCPFrame returns a new TCPFrame with data set to buf. +// Users should still call [TCPFrame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewTCPFrame(buf []byte) (TCPFrame, error) { + if len(buf) < sizeHeaderTCP { + return TCPFrame{buf: nil}, errors.New("TCP packet too short") + } + return TCPFrame{buf: buf}, nil +} + +// NewUDPFrame returns a new UDPFrame with data set to buf. +// Users should still call [UDPFrame.ValidateSize] before working +// with payload/options of frames to avoid panics. +func NewUDPFrame(buf []byte) (UDPFrame, error) { + if len(buf) < sizeHeaderUDP { + return UDPFrame{buf: buf}, errors.New("UDP packet too short") + } + return UDPFrame{buf: buf}, nil } // EthFrame represents a Ethernet frame without including a preamble. The first byte is start of destination MAC address. @@ -512,6 +547,12 @@ func (tfrm TCPFrame) Payload() []byte { return tfrm.buf[tfrm.HeaderLength():] } +// Options returns the TCP option buffer portion of the frame. The returned slice may be zero length. +// Be sure to call [TCPFrame.ValidateSize] beforehand to avoid panic. +func (tfrm TCPFrame) Options() []byte { + return tfrm.buf[sizeHeaderTCP:tfrm.HeaderLength()] +} + type UDPFrame struct { buf []byte } diff --git a/lneto/tcp/definitions.go b/lneto/tcp/definitions.go index 5e6a645..f290d80 100644 --- a/lneto/tcp/definitions.go +++ b/lneto/tcp/definitions.go @@ -1,12 +1,15 @@ package tcp import ( + "errors" + "fmt" "math/bits" "strconv" + "strings" "unsafe" ) -//go:generate stringer -type=State -linecomment -output stringers.go . +//go:generate stringer -type=State,OptionKind -linecomment -output stringers.go . // Segment represents an incoming/outgoing TCP segment in the sequence space. type Segment struct { @@ -242,3 +245,110 @@ func (s State) IsSynchronized() bool { func (s State) isOpen() bool { return s != StateClosed && s != StateTimeWait // TODO: is this api ok? } + +type OptionKind uint8 + +const ( + OptEnd OptionKind = iota // end of option list + OptNop // no-operation + OptMaxSegmentSize // maximum segment size + OptWindowScale // window scale + OptSACKPermitted // SACK permitted + OptSACK // SACK + OptEcho // echo(obsolete) + optEchoReply // echo reply(obsolete) + OptTimestamps // timestamps + optPOCP // partial order connection permitted(obsolete) + optPOSP // partial order service profile(obsolete) + optCC // CC(obsolete) + optCCnew // CC.new(obsolete) + optCCecho // CC.echo(obsolete) + optACR // alternate checksum request(obsolete) + optACD // alternate checksum data(obsolete) + optSkeeter // skeeter + optBubba // bubba + OptTrailerChecksum // trailer checksum + optMD5Signature // MD5 signature(obsolete) + OptSCPSCapabilities // SCPS capabilities + OptSNA // selective negative acks + OptRecordBoundaries // record boundaries + OptCorruptionExperienced // corruption experienced + OptSNAP // SNAP + OptUnassigned // unassigned + OptCompressionFilter // compression filter + OptQuickStartResponse // quick-start response + OptUserTimeout // user timeout or unauthorized use + OptAuthetication // Authentication TCP-AO + OptMultipath // multipath TCP +) + +const ( + OptFastOpenCookie OptionKind = 34 // fast open cookie + OptEncryptionNegotiation OptionKind = 69 // encryption negotiation + OptAccurateECN0 OptionKind = 172 // accurate ECN order 0 + OptAccurateECN1 OptionKind = 174 // accurate ECN order 1 +) + +// IsObsolete returns true if option considered obsolete by newer TCP specifications. +func (kind OptionKind) IsObsolete() bool { + if kind.IsDefined() { + return strings.HasSuffix(kind.String(), "(obsolete)") + } + return false +} + +// IsDefined returns true if the option is a known unreserved option kind. +func (kind OptionKind) IsDefined() bool { + return kind <= 30 || kind == 34 || kind == 69 || kind == 172 || kind == 174 +} + +type OptionParser struct { + SkipSizeValidation bool + SkipObsolete bool +} + +func (op *OptionParser) ForEachOption(opts []byte, fn func(OptionKind, []byte) error) error { + off := 0 + skipSizeValidation := op.SkipSizeValidation + skipObsolete := op.SkipObsolete + for off < len(opts) && opts[off] != 0 { + kind := OptionKind(opts[off]) + off++ + if kind == OptNop { + continue + } + if len(opts[off:]) < 2 { + return errors.New("short TCP options") + } + size := int(opts[off]) + off++ + if len(opts[off:]) < size { + return fmt.Errorf("option %q length %d exceeds buffer size %d", kind.String(), size, len(opts[off:])) + } + + if !skipSizeValidation { + expectSize := -1 + switch kind { + case OptTimestamps: + expectSize = 10 + case OptMaxSegmentSize, OptUserTimeout: + expectSize = 4 + case OptWindowScale: + expectSize = 3 + case OptSACKPermitted: + expectSize = 2 + } + if expectSize != -1 && size != expectSize { + return fmt.Errorf("bad TCP option %q size want %d got %d", kind.String(), expectSize, opts[off]) + } + } + if skipObsolete && kind.IsObsolete() { + err := fn(kind, opts[off:off+size]) + if err != nil { + return err + } + } + off += size + } + return nil +} diff --git a/lneto/tcp/stringers.go b/lneto/tcp/stringers.go index d197738..52c90c7 100644 --- a/lneto/tcp/stringers.go +++ b/lneto/tcp/stringers.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=State -linecomment -output stringers.go ."; DO NOT EDIT. +// Code generated by "stringer -type=State,OptionKind -linecomment -output stringers.go ."; DO NOT EDIT. package tcp @@ -31,3 +31,72 @@ func (i State) String() string { } return _State_name[_State_index[i]:_State_index[i+1]] } +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[OptEnd-0] + _ = x[OptNop-1] + _ = x[OptMaxSegmentSize-2] + _ = x[OptWindowScale-3] + _ = x[OptSACKPermitted-4] + _ = x[OptSACK-5] + _ = x[OptEcho-6] + _ = x[optEchoReply-7] + _ = x[OptTimestamps-8] + _ = x[optPOCP-9] + _ = x[optPOSP-10] + _ = x[optCC-11] + _ = x[optCCnew-12] + _ = x[optCCecho-13] + _ = x[optACR-14] + _ = x[optACD-15] + _ = x[optSkeeter-16] + _ = x[optBubba-17] + _ = x[OptTrailerChecksum-18] + _ = x[optMD5Signature-19] + _ = x[OptSCPSCapabilities-20] + _ = x[OptSNA-21] + _ = x[OptRecordBoundaries-22] + _ = x[OptCorruptionExperienced-23] + _ = x[OptSNAP-24] + _ = x[OptUnassigned-25] + _ = x[OptCompressionFilter-26] + _ = x[OptQuickStartResponse-27] + _ = x[OptUserTimeout-28] + _ = x[OptAuthetication-29] + _ = x[OptMultipath-30] + _ = x[OptFastOpenCookie-34] + _ = x[OptEncryptionNegotiation-69] + _ = x[OptAccurateECN0-172] + _ = x[OptAccurateECN1-174] +} + +const ( + _OptionKind_name_0 = "end of option listno-operationmaximum segment sizewindow scaleSACK permittedSACKecho(obsolete)echo reply(obsolete)timestampspartial order connection permitted(obsolete)partial order service profile(obsolete)CC(obsolete)CC.new(obsolete)CC.echo(obsolete)alternate checksum request(obsolete)alternate checksum data(obsolete)skeeterbubbatrailer checksumMD5 signature(obsolete)SCPS capabilitiesselective negative acksrecord boundariescorruption experiencedSNAPunassignedcompression filterquick-start responseuser timeout or unauthorized useAuthentication TCP-AOmultipath TCP" + _OptionKind_name_1 = "fast open cookie" + _OptionKind_name_2 = "encryption negotiation" + _OptionKind_name_3 = "accurate ECN order 0" + _OptionKind_name_4 = "accurate ECN order 1" +) + +var ( + _OptionKind_index_0 = [...]uint16{0, 18, 30, 50, 62, 76, 80, 94, 114, 124, 168, 207, 219, 235, 252, 288, 321, 328, 333, 349, 372, 389, 412, 429, 451, 455, 465, 483, 503, 535, 556, 569} +) + +func (i OptionKind) String() string { + switch { + case i <= 30: + return _OptionKind_name_0[_OptionKind_index_0[i]:_OptionKind_index_0[i+1]] + case i == 34: + return _OptionKind_name_1 + case i == 69: + return _OptionKind_name_2 + case i == 172: + return _OptionKind_name_3 + case i == 174: + return _OptionKind_name_4 + default: + return "OptionKind(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/lneto/validation.go b/lneto/validation.go index e2802e4..f763463 100644 --- a/lneto/validation.go +++ b/lneto/validation.go @@ -3,6 +3,8 @@ package lneto import "errors" var ( + errShortEth = errors.New("ethernet length exceeds frame") + errShortVLAN = errors.New("ethernet length too short for VLAN") errShortUDP = errors.New("UDP length exceeds frame") errBadUDPLen = errors.New("UDP length invalid") errShortIPv4 = errors.New("IPv4 total length exceeds frame") @@ -13,6 +15,18 @@ var ( errBadTCPOff = errors.New("TCP offset invalid") ) +// ValidateSize checks the frame's size fields and compares with the actual buffer +// the frame. It returns a non-nil error on finding an inconsistency. +func (efrm EthFrame) ValidateSize() error { + sz := efrm.EtherTypeOrSize() + if sz.IsSize() && len(efrm.buf) < int(sz) { + return errShortEth + } else if sz == EtherTypeVLAN && len(efrm.buf) < 18 { + return errShortVLAN + } + return nil +} + // ValidateSize checks the frame's size fields and compares with the actual buffer // the frame. It returns a non-nil error on finding an inconsistency. func (afrm ARPFrame) ValidateSize() error {