add TCP options, more validation

This commit is contained in:
soypat
2024-12-24 20:18:06 -03:00
parent 4476c7bae3
commit 7beb0327c4
4 changed files with 257 additions and 23 deletions
+62 -21
View File
@@ -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
}
+111 -1
View File
@@ -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
}
+70 -1
View File
@@ -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) + ")"
}
}
+14
View File
@@ -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 {