mirror of
https://github.com/soypat/lneto.git
synced 2026-08-10 01:43:41 +00:00
tls: add pcap capturing
This commit is contained in:
@@ -52,11 +52,12 @@ type PacketBreakdown struct {
|
||||
// 0: Ethernet (3 base + VLAN tag = 4)
|
||||
// 1: L3 max(IPv4=12, ARP=9, IPv6=8) = 12
|
||||
// 2: L4 max(TCP=10, ICMP=8, UDP=4) = 10
|
||||
// 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1) = 16
|
||||
// 4-5: overflow/remaining = 2
|
||||
// 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1, TLS record=4) = 16
|
||||
// 4: TLS handshake message (ClientHello=8) = 8
|
||||
// 5-7: extra TLS records/overflow/remaining = 4,2,2
|
||||
func (pc *PacketBreakdown) initFrames() []Frame {
|
||||
const nframes = 6
|
||||
var fieldCaps = [nframes]int{4, 12, 10, 16, 2, 2}
|
||||
const nframes = 8
|
||||
var fieldCaps = [nframes]int{4, 12, 10, 16, 8, 4, 2, 2}
|
||||
frames := make([]Frame, nframes)
|
||||
for i := range frames {
|
||||
frames[i].Fields = make([]FrameField, 0, fieldCaps[i])
|
||||
@@ -328,9 +329,18 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
|
||||
}
|
||||
payload := tfrm.Payload()
|
||||
if len(payload) > 0 {
|
||||
debuglog("pcap:tcp:http-start")
|
||||
dst, err = pc.CaptureHTTP(dst, pkt, end)
|
||||
debuglog("pcap:tcp:http-done")
|
||||
// Application protocol is picked by inspecting the payload rather than
|
||||
// by port, so that TLS on a port other than 443 and HTTP on a port
|
||||
// other than 80 are both broken down correctly.
|
||||
if payloadIsTLS(payload) {
|
||||
debuglog("pcap:tcp:tls-start")
|
||||
dst, err = pc.CaptureTLS(dst, pkt, end)
|
||||
debuglog("pcap:tcp:tls-done")
|
||||
} else {
|
||||
debuglog("pcap:tcp:http-start")
|
||||
dst, err = pc.CaptureHTTP(dst, pkt, end)
|
||||
debuglog("pcap:tcp:http-done")
|
||||
}
|
||||
if err != nil {
|
||||
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
|
||||
}
|
||||
@@ -835,10 +845,13 @@ const (
|
||||
// FlagContainer is used for [FrameField]s whose SubFields represent
|
||||
// the entirety of the FrameField's data. i.e: DNS Questions/Answers.
|
||||
FlagContainer
|
||||
// FlagEncrypted marks fields whose bytes are ciphertext. i.e: TLS application_data fragment.
|
||||
FlagEncrypted
|
||||
)
|
||||
|
||||
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
|
||||
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
|
||||
func (ff Flags) IsEncrypted() bool { return ff&FlagEncrypted != 0 }
|
||||
|
||||
type Frame struct {
|
||||
PacketBitOffset int
|
||||
@@ -1009,6 +1022,9 @@ func (frm Frame) LenBits() (totalBitlen int) {
|
||||
|
||||
func (ff FrameField) String() string {
|
||||
if ff.Class == FieldClassPayload {
|
||||
if ff.Flags.IsEncrypted() {
|
||||
return "Encrypted len=" + strconv.Itoa(ff.BitLength/8)
|
||||
}
|
||||
return "Payload len=" + strconv.Itoa(ff.BitLength/8)
|
||||
}
|
||||
if ff.Name != "" {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/soypat/lneto/dns"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/udp"
|
||||
)
|
||||
|
||||
@@ -106,6 +107,39 @@ func buildDNSPacket(b testing.TB) []byte {
|
||||
return pkt
|
||||
}
|
||||
|
||||
// buildTLSPacket builds an Ethernet+IPv4+TCP packet carrying a real
|
||||
// ClientHello record, exercising the string-heavy TLS path: cipher suite and
|
||||
// extension subfields, SNI and ALPN text.
|
||||
func buildTLSPacket(b testing.TB) []byte {
|
||||
const (
|
||||
ethSize = 14
|
||||
ipv4Size = 20
|
||||
tcpSize = 20
|
||||
)
|
||||
hello := captureClientHelloRecord(b, "example.com", []string{"h2", "http/1.1"})
|
||||
pkt := make([]byte, ethSize+ipv4Size+tcpSize+len(hello))
|
||||
|
||||
efrm, _ := ethernet.NewFrame(pkt)
|
||||
*efrm.DestinationHardwareAddr() = [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}
|
||||
*efrm.SourceHardwareAddr() = [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
efrm.SetEtherType(ethernet.TypeIPv4)
|
||||
|
||||
ifrm, _ := ipv4.NewFrame(pkt[ethSize:])
|
||||
ifrm.SetVersionAndIHL(4, 5)
|
||||
ifrm.SetID(0x1234)
|
||||
ifrm.SetTTL(64)
|
||||
ifrm.SetProtocol(lneto.IPProtoTCP)
|
||||
ifrm.SetTotalLength(uint16(ipv4Size + tcpSize + len(hello)))
|
||||
|
||||
tfrm, _ := tcp.NewFrame(pkt[ethSize+ipv4Size:])
|
||||
tfrm.SetSourcePort(51000)
|
||||
tfrm.SetDestinationPort(443)
|
||||
tfrm.SetOffsetAndFlags(5, tcp.FlagPSH|tcp.FlagACK)
|
||||
|
||||
copy(pkt[ethSize+ipv4Size+tcpSize:], hello)
|
||||
return pkt
|
||||
}
|
||||
|
||||
func configureBenchFormatter(f *Formatter) {
|
||||
f.SubfieldLimit = benchSubfieldLimit
|
||||
f.FrameSep = "\n"
|
||||
@@ -123,6 +157,7 @@ func BenchmarkPcap(b *testing.B) {
|
||||
}{
|
||||
{"DHCP", buildDHCPPacket(b)},
|
||||
{"DNS", buildDNSPacket(b)},
|
||||
{"TLS", buildTLSPacket(b)},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
@@ -182,6 +217,7 @@ func BenchmarkPcapPhases(b *testing.B) {
|
||||
}{
|
||||
{"DHCP", buildDHCPPacket(b)},
|
||||
{"DNS", buildDNSPacket(b)},
|
||||
{"TLS", buildTLSPacket(b)},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
package pcap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/x/tls"
|
||||
)
|
||||
|
||||
// maxTLSRecordsPerPacket bounds how many record frames a single packet may
|
||||
// produce. A TCP segment commonly coalesces several small records, but without
|
||||
// a bound a segment full of 5-byte empty records would produce thousands of
|
||||
// frames.
|
||||
const maxTLSRecordsPerPacket = 8
|
||||
|
||||
// payloadIsTLS reports whether payload begins with what could be a TLS record
|
||||
// header. TLS has no magic number, so this is a heuristic: the content type
|
||||
// must be one of the four TLS 1.3 defines, the legacy record version must be
|
||||
// 0x03xx and the declared fragment length must be legal.
|
||||
//
|
||||
// It is what lets TLS be captured on any port instead of only on 443, and it
|
||||
// does not collide with HTTP, whose first byte is a method or version letter
|
||||
// and never a valid content type.
|
||||
func payloadIsTLS(payload []byte) bool {
|
||||
if len(payload) < tls.SizeHeaderRecord {
|
||||
return false
|
||||
}
|
||||
switch tls.ContentType(payload[0]) {
|
||||
case tls.ContentTypeChangeCipherSpec, tls.ContentTypeAlert,
|
||||
tls.ContentTypeHandshake, tls.ContentTypeApplicationData:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if payload[1] != 0x03 || payload[2] > 0x04 {
|
||||
// Every record version in use is 3.x: TLS 1.0 through 1.3 inclusive.
|
||||
return false
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(payload[3:5]))
|
||||
return n > 0 && n <= tls.MaxCiphertext
|
||||
}
|
||||
|
||||
// CaptureTLS breaks down the TLS records starting at bitOffset. A single entry
|
||||
// point handles every kind of TLS traffic: a record names its own content type,
|
||||
// so the caller does not need to know whether it is looking at a handshake, an
|
||||
// alert or application data, which is what makes capturing a whole port 443
|
||||
// conversation possible without tracking connection state.
|
||||
//
|
||||
// One [Frame] is produced per record, plus one more per cleartext handshake
|
||||
// message carried inside a handshake record. Everything after the ServerHello
|
||||
// is encrypted and appears on the wire as application_data; its fragment is
|
||||
// reported as an opaque payload, since decrypting it needs keys a capture does
|
||||
// not have.
|
||||
//
|
||||
// TLS is a byte stream: a record may span TCP segments and a handshake message
|
||||
// may span records. Whatever arrived is reported and the affected frame carries
|
||||
// [tls.ErrNeedMore]; reassembly is out of scope for a stateless breakdown.
|
||||
func (pc *PacketBreakdown) CaptureTLS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
|
||||
debuglog("pcap:tls:start")
|
||||
if bitOffset%8 != 0 {
|
||||
return dst, errNotByteAligned
|
||||
}
|
||||
off := bitOffset / 8
|
||||
for nrec := 0; off < len(pkt); nrec++ {
|
||||
if nrec == maxTLSRecordsPerPacket {
|
||||
reclaimRemainingFrame(&dst, "TLS records?", FieldClassPayload, off*octet, octet*len(pkt))
|
||||
break
|
||||
}
|
||||
newdst, consumed, err := pc.captureTLSRecord(dst, pkt, off*octet)
|
||||
dst = newdst
|
||||
if err != nil {
|
||||
if nrec == 0 {
|
||||
// Not TLS after all; let the caller frame the payload.
|
||||
return dst, err
|
||||
}
|
||||
// Bytes trailing the last complete record that are too few or too
|
||||
// malformed to be a record header of their own.
|
||||
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, off*octet, octet*len(pkt))
|
||||
break
|
||||
}
|
||||
off += consumed
|
||||
}
|
||||
debuglog("pcap:tls:done")
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// captureTLSRecord appends the frames of the single record at bitOffset and
|
||||
// returns how many bytes of pkt the record occupied.
|
||||
func (pc *PacketBreakdown) captureTLSRecord(dst []Frame, pkt []byte, bitOffset int) ([]Frame, int, error) {
|
||||
rec := pkt[bitOffset/8:]
|
||||
rfrm, err := tls.NewRecordFrame(rec)
|
||||
if err != nil {
|
||||
return dst, 0, err
|
||||
}
|
||||
rfrm.ValidateSize(pc.validator())
|
||||
if pc.validator().HasError() {
|
||||
return dst, 0, pc.validator().ErrPop()
|
||||
}
|
||||
debuglog("pcap:tls:validated")
|
||||
const fragOff = tls.SizeHeaderRecord * octet
|
||||
ctype := rfrm.ContentType()
|
||||
finfo := reclaimFrame(&dst, "TLS", bitOffset, baseTLSRecordFields[:])
|
||||
finfo.Fields[0].Name = ctype.StringConst()
|
||||
frag := rfrm.Payload()
|
||||
if frag == nil {
|
||||
// Fragment continues in a later segment. Report what arrived.
|
||||
avail := len(rec) - tls.SizeHeaderRecord
|
||||
finfo.Errors = append(finfo.Errors, tls.ErrNeedMore)
|
||||
if avail > 0 {
|
||||
var flags Flags
|
||||
if ctype == tls.ContentTypeApplicationData {
|
||||
flags = FlagEncrypted
|
||||
}
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: fragOff,
|
||||
BitLength: avail * octet,
|
||||
Flags: flags,
|
||||
})
|
||||
}
|
||||
return dst, len(rec), nil
|
||||
}
|
||||
|
||||
switch ctype {
|
||||
case tls.ContentTypeHandshake:
|
||||
// Handshake messages get frames of their own. finfo must not be touched
|
||||
// past this point: appending to dst may move the Frame it points at.
|
||||
dst = pc.captureTLSHandshake(dst, pkt, bitOffset+fragOff, len(frag))
|
||||
|
||||
case tls.ContentTypeAlert:
|
||||
if len(frag) < 2 {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
break
|
||||
}
|
||||
// The level byte is advisory only: in TLS 1.3 every alert except
|
||||
// close_notify and user_canceled is fatal whatever it says.
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: tls.AlertLevel(frag[0]).StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: fragOff,
|
||||
BitLength: octet,
|
||||
Flags: FlagLegacy,
|
||||
}, FrameField{
|
||||
Name: tls.AlertDescription(frag[1]).StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: fragOff + octet,
|
||||
BitLength: octet,
|
||||
})
|
||||
|
||||
case tls.ContentTypeApplicationData:
|
||||
// Either genuine application data or a protected handshake or alert
|
||||
// record; which of the three is only knowable after decryption.
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: fragOff,
|
||||
BitLength: len(frag) * octet,
|
||||
Flags: FlagEncrypted,
|
||||
})
|
||||
|
||||
default: // change_cipher_spec and unrecognized content types.
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: fragOff,
|
||||
BitLength: len(frag) * octet,
|
||||
})
|
||||
}
|
||||
return dst, rfrm.RecordLength(), nil
|
||||
}
|
||||
|
||||
// captureTLSHandshake appends one frame per handshake message found in the
|
||||
// fragLen bytes of handshake record fragment starting at bitOffset.
|
||||
func (pc *PacketBreakdown) captureTLSHandshake(dst []Frame, pkt []byte, bitOffset, fragLen int) []Frame {
|
||||
debuglog("pcap:tls:hs-start")
|
||||
const hdr = tls.SizeHeaderHandshake
|
||||
frag := pkt[bitOffset/8:][:fragLen]
|
||||
fragEnd := bitOffset + fragLen*octet
|
||||
for off := 0; off < fragLen; {
|
||||
msgBitOff := bitOffset + off*octet
|
||||
hfrm, err := tls.NewHandshakeFrame(frag[off:])
|
||||
if err != nil {
|
||||
// Fewer than 4 bytes left: a message header split across records.
|
||||
reclaimRemainingFrame(&dst, "TLS Handshake?", FieldClassPayload, msgBitOff, fragEnd)
|
||||
return dst
|
||||
}
|
||||
mtype := hfrm.MsgType()
|
||||
finfo := reclaimFrame(&dst, tlsHandshakeProto(mtype), msgBitOff, baseTLSHandshakeFields[:])
|
||||
finfo.Fields[0].Name = mtype.StringConst()
|
||||
body := hfrm.Body()
|
||||
if body == nil {
|
||||
// Message body continues in the next record.
|
||||
finfo.Errors = append(finfo.Errors, tls.ErrNeedMore)
|
||||
if avail := fragLen - off - hdr; avail > 0 {
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: hdr * octet,
|
||||
BitLength: avail * octet,
|
||||
})
|
||||
}
|
||||
return dst
|
||||
}
|
||||
switch mtype {
|
||||
case tls.HandshakeTypeClientHello:
|
||||
pc.captureTLSHello(finfo, body, true)
|
||||
case tls.HandshakeTypeServerHello:
|
||||
pc.captureTLSHello(finfo, body, false)
|
||||
default:
|
||||
if len(body) > 0 {
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: hdr * octet,
|
||||
BitLength: len(body) * octet,
|
||||
})
|
||||
}
|
||||
}
|
||||
off += hfrm.MessageLength()
|
||||
}
|
||||
debuglog("pcap:tls:hs-done")
|
||||
return dst
|
||||
}
|
||||
|
||||
// captureTLSHello appends the fields of a ClientHello (isClient) or ServerHello
|
||||
// body to finfo. The two differ only in that the client offers vectors of
|
||||
// cipher suites and compression methods where the server names exactly one of
|
||||
// each. Offsets are relative to the start of the handshake message, so the
|
||||
// handshake header size is added throughout.
|
||||
//
|
||||
// The walk is deliberately more permissive than [tls.NewClientHelloFrame]: a
|
||||
// capture must show what a malformed hello contains, so it reports the fields
|
||||
// it did decode and stops at the first inconsistent length instead of
|
||||
// discarding the message.
|
||||
func (pc *PacketBreakdown) captureTLSHello(finfo *Frame, body []byte, isClient bool) {
|
||||
const hdr = tls.SizeHeaderHandshake
|
||||
const fixed = 2 + tls.SizeRandom + 1 // legacy_version + random + session id length
|
||||
if len(body) < fixed {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
// Pinned to 0x0303 by TLS 1.3 whatever version is really negotiated;
|
||||
// the real version travels in the supported_versions extension.
|
||||
Class: FieldClassVersion,
|
||||
FrameBitOffset: hdr * octet,
|
||||
BitLength: 2 * octet,
|
||||
Flags: FlagLegacy,
|
||||
}, FrameField{
|
||||
Name: "Random",
|
||||
Class: FieldClassID,
|
||||
FrameBitOffset: (hdr + 2) * octet,
|
||||
BitLength: tls.SizeRandom * octet,
|
||||
})
|
||||
|
||||
off := 2 + tls.SizeRandom
|
||||
sidLen := int(body[off])
|
||||
off++
|
||||
if sidLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
if sidLen > 0 {
|
||||
// TLS 1.3 has no session resumption by ID; a non-empty value means
|
||||
// middlebox compatibility mode, echoed verbatim by the server.
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: "Session ID",
|
||||
Class: FieldClassID,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: sidLen * octet,
|
||||
})
|
||||
}
|
||||
off += sidLen
|
||||
|
||||
if isClient {
|
||||
if len(body)-off < 2 {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
suitesLen := int(binary.BigEndian.Uint16(body[off:]))
|
||||
off += 2
|
||||
if suitesLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
pc.appendTLSCipherSuites(finfo, body[off:off+suitesLen], hdr+off)
|
||||
off += suitesLen
|
||||
|
||||
compLen := int(body[off])
|
||||
off++
|
||||
if compLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: "compression methods",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: compLen * octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
off += compLen
|
||||
} else {
|
||||
if len(body)-off < 3 {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
suite := tls.CipherSuite(binary.BigEndian.Uint16(body[off:]))
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: suite.StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "compression method",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: (hdr + off + 2) * octet,
|
||||
BitLength: octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
off += 3
|
||||
}
|
||||
|
||||
if len(body)-off < 2 {
|
||||
return // No extensions block. Not a legal 1.3 hello, but not a framing error either.
|
||||
}
|
||||
extsLen := int(binary.BigEndian.Uint16(body[off:]))
|
||||
off += 2
|
||||
if extsLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
extsLen = len(body) - off
|
||||
}
|
||||
pc.appendTLSExtensions(finfo, body[off:off+extsLen], hdr+off)
|
||||
}
|
||||
|
||||
// appendTLSCipherSuites appends a cipher_suites container field whose subfields
|
||||
// name each offered suite. base is the byte offset of suites within the frame.
|
||||
// GREASE values show up as their numeric type, which is what a capture should
|
||||
// display: they carry no meaning and are not an error.
|
||||
func (pc *PacketBreakdown) appendTLSCipherSuites(finfo *Frame, suites []byte, base int) {
|
||||
// Reclaim from the Fields backing array to reuse its SubFields backing array.
|
||||
sfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*sfield = FrameField{
|
||||
Name: "cipher suites",
|
||||
Class: FieldClassOptions,
|
||||
SubFields: sfield.SubFields[:0],
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(suites) * octet,
|
||||
}
|
||||
if pc.SubfieldLimit <= 0 {
|
||||
return
|
||||
}
|
||||
for off := 0; off+2 <= len(suites); off += 2 {
|
||||
if len(sfield.SubFields) >= pc.SubfieldLimit {
|
||||
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||
return
|
||||
}
|
||||
suite := tls.CipherSuite(binary.BigEndian.Uint16(suites[off:]))
|
||||
sfield.SubFields = append(sfield.SubFields, FrameField{
|
||||
Name: suite.StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: (base + off) * octet,
|
||||
BitLength: 2 * octet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// appendTLSExtensions appends an extensions container field whose subfields are
|
||||
// the individual extensions. base is the byte offset of exts within the frame.
|
||||
func (pc *PacketBreakdown) appendTLSExtensions(finfo *Frame, exts []byte, base int) {
|
||||
extfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*extfield = FrameField{
|
||||
Name: "extensions",
|
||||
Class: FieldClassOptions,
|
||||
SubFields: extfield.SubFields[:0],
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(exts) * octet,
|
||||
}
|
||||
if pc.SubfieldLimit <= 0 {
|
||||
return
|
||||
}
|
||||
for off := 0; off+4 <= len(exts); {
|
||||
ext := tls.ExtensionType(binary.BigEndian.Uint16(exts[off:]))
|
||||
n := int(binary.BigEndian.Uint16(exts[off+2:]))
|
||||
off += 4
|
||||
if n > len(exts)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
if len(extfield.SubFields) >= pc.SubfieldLimit {
|
||||
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||
return
|
||||
}
|
||||
extfield.SubFields = append(extfield.SubFields, tlsExtensionField(ext, exts[off:off+n], base+off))
|
||||
off += n
|
||||
}
|
||||
}
|
||||
|
||||
// tlsExtensionField describes a single hello extension. Extensions carrying a
|
||||
// human readable value point at that value instead of at the whole extension
|
||||
// body, and the bulky opaque ones are classed as payload so that a
|
||||
// [Formatter.FilterClasses] can drop them without losing the rest of the hello.
|
||||
func tlsExtensionField(ext tls.ExtensionType, data []byte, base int) FrameField {
|
||||
field := FrameField{
|
||||
Name: ext.StringConst(),
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(data) * octet,
|
||||
}
|
||||
switch ext {
|
||||
case tls.ExtServerName:
|
||||
// server_name_list(2) + name_type(1) + HostName length(2), then the name.
|
||||
// Only host_name(0) is defined, and no client has ever sent a second entry.
|
||||
const nameOff = 5
|
||||
if len(data) >= nameOff && data[2] == 0 {
|
||||
n := int(binary.BigEndian.Uint16(data[3:5]))
|
||||
if n <= len(data)-nameOff {
|
||||
field.Class = FieldClassText
|
||||
field.FrameBitOffset = (base + nameOff) * octet
|
||||
field.BitLength = n * octet
|
||||
}
|
||||
}
|
||||
|
||||
case tls.ExtALPN:
|
||||
// Each protocol name is length prefixed. Quoting the whole list keeps
|
||||
// every name visible, with the length bytes showing up as escapes.
|
||||
if len(data) >= 2 {
|
||||
field.Class = FieldClassText
|
||||
field.FrameBitOffset = (base + 2) * octet
|
||||
field.BitLength = (len(data) - 2) * octet
|
||||
}
|
||||
|
||||
case tls.ExtKeyShare, tls.ExtPreSharedKey, tls.ExtPadding, tls.ExtSessionTicket,
|
||||
tls.ExtCookie, tls.ExtEncryptedClientHello, tls.ExtSignedCertificateTimestamp:
|
||||
// Opaque and large: a post-quantum key share alone runs past 1kB.
|
||||
field.Class = FieldClassPayload
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// tlsHandshakeProto names the frame of a handshake message. Only the messages a
|
||||
// capture can see in cleartext get a name of their own; the rest travel inside
|
||||
// a protected record and never reach here undecrypted.
|
||||
func tlsHandshakeProto(t tls.HandshakeType) string {
|
||||
switch t {
|
||||
case tls.HandshakeTypeClientHello:
|
||||
return "TLS ClientHello"
|
||||
case tls.HandshakeTypeServerHello:
|
||||
return "TLS ServerHello"
|
||||
}
|
||||
return "TLS Handshake"
|
||||
}
|
||||
|
||||
var baseTLSRecordFields = [...]FrameField{
|
||||
{
|
||||
// Name is filled in with the content type's name by captureTLSRecord.
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: 0,
|
||||
BitLength: 1 * octet,
|
||||
},
|
||||
{
|
||||
// legacy_record_version, which TLS 1.3 receivers ignore entirely.
|
||||
Class: FieldClassVersion,
|
||||
FrameBitOffset: 1 * octet,
|
||||
BitLength: 2 * octet,
|
||||
Flags: FlagLegacy,
|
||||
},
|
||||
{
|
||||
Class: FieldClassSize,
|
||||
FrameBitOffset: 3 * octet,
|
||||
BitLength: 2 * octet,
|
||||
},
|
||||
}
|
||||
|
||||
var baseTLSHandshakeFields = [...]FrameField{
|
||||
{
|
||||
// Name is filled in with the message type's name by captureTLSHandshake.
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: 0,
|
||||
BitLength: 1 * octet,
|
||||
},
|
||||
{
|
||||
Class: FieldClassSize,
|
||||
FrameBitOffset: 1 * octet,
|
||||
BitLength: 3 * octet,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
package pcap
|
||||
|
||||
import (
|
||||
stdtls "crypto/tls"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/x/tls"
|
||||
)
|
||||
|
||||
// captureClientHelloRecord drives a standard library TLS client far enough to
|
||||
// emit its first flight and returns the complete handshake record, header
|
||||
// included. A real client gives realistic extension ordering, a 32-byte
|
||||
// middlebox compatibility session ID and a post-quantum key share.
|
||||
func captureClientHelloRecord(t testing.TB, serverName string, protos []string) []byte {
|
||||
t.Helper()
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
go func() {
|
||||
c := stdtls.Client(client, &stdtls.Config{
|
||||
ServerName: serverName,
|
||||
MinVersion: stdtls.VersionTLS13,
|
||||
MaxVersion: stdtls.VersionTLS13,
|
||||
NextProtos: protos,
|
||||
})
|
||||
_ = c.Handshake() // Will fail; only the first flight is needed.
|
||||
}()
|
||||
server.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
var buf [4096]byte
|
||||
n, err := server.Read(buf[:])
|
||||
if err != nil {
|
||||
t.Fatalf("reading ClientHello: %v", err)
|
||||
}
|
||||
rec, err := tls.NewRecordFrame(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rec.Complete() {
|
||||
t.Fatalf("ClientHello record split across reads: have %d want %d", n, rec.RecordLength())
|
||||
}
|
||||
return append([]byte{}, rec.RawData()...)
|
||||
}
|
||||
|
||||
// TestCaptureTLSClientHello runs a real ClientHello through the full
|
||||
// Ethernet/IPv4/TCP path to check that TLS is detected by payload content
|
||||
// rather than by port, and that the SNI hostname is recovered.
|
||||
func TestCaptureTLSClientHello(t *testing.T) {
|
||||
const mtu = ethernet.MaxMTU
|
||||
const serverName = "example.com"
|
||||
payload := captureClientHelloRecord(t, serverName, []string{"h2", "http/1.1"})
|
||||
|
||||
var buf [mtu]byte
|
||||
var gen ltesto.PacketGen
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
gen.RandomizeAddrs(rng)
|
||||
pkt := gen.AppendRandomIPv4TCPPacket(buf[:0], rng, tcp.Segment{
|
||||
SEQ: 100,
|
||||
ACK: 200,
|
||||
DATALEN: tcp.Size(len(payload)),
|
||||
WND: 1024,
|
||||
Flags: tcp.FlagPSH | tcp.FlagACK,
|
||||
})
|
||||
copy(pkt[len(pkt)-len(payload):], payload)
|
||||
|
||||
var pbreak PacketBreakdown
|
||||
pbreak.SubfieldLimit = 32
|
||||
frames, err := pbreak.CaptureEthernet(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Ethernet+IPv4+TCP+TLS record+TLS ClientHello = 5 frames.
|
||||
if len(frames) != 5 {
|
||||
for i := range frames {
|
||||
t.Logf("frame[%d]=%s", i, frames[i].String())
|
||||
}
|
||||
t.Fatalf("want 5 frames, got %d", len(frames))
|
||||
}
|
||||
if frames[3].Protocol != "TLS" {
|
||||
t.Errorf("frame 3 protocol=%q want TLS", frames[3].Protocol)
|
||||
}
|
||||
if frames[4].Protocol != "TLS ClientHello" {
|
||||
t.Errorf("frame 4 protocol=%q want TLS ClientHello", frames[4].Protocol)
|
||||
}
|
||||
if len(frames[3].Errors) > 0 || len(frames[4].Errors) > 0 {
|
||||
t.Errorf("unexpected errors: %v %v", frames[3].Errors, frames[4].Errors)
|
||||
}
|
||||
// A handshake record frame describes the record header only; its fragment is
|
||||
// broken down by the handshake frame that follows, so that no bytes are
|
||||
// claimed by two frames at once.
|
||||
if got := frames[3].LenBits() / 8; got != tls.SizeHeaderRecord {
|
||||
t.Errorf("TLS record frame len=%d want %d", got, tls.SizeHeaderRecord)
|
||||
}
|
||||
if got := frames[4].LenBits() / 8; got != len(payload)-tls.SizeHeaderRecord {
|
||||
t.Errorf("handshake frame len=%d want %d", got, len(payload)-tls.SizeHeaderRecord)
|
||||
}
|
||||
|
||||
ctype := fieldByName(frames[3], "handshake")
|
||||
if ctype == nil {
|
||||
t.Fatal("no handshake content type field")
|
||||
}
|
||||
if v, _ := frames[3].FieldAsUint(indexOfField(frames[3], "handshake"), pkt); v != uint64(tls.ContentTypeHandshake) {
|
||||
t.Errorf("content type=%d want %d", v, tls.ContentTypeHandshake)
|
||||
}
|
||||
|
||||
// SNI and ALPN live as subfields of the extensions container.
|
||||
exts := fieldByName(frames[4], "extensions")
|
||||
if exts == nil {
|
||||
t.Fatal("no extensions field in ClientHello")
|
||||
}
|
||||
var sni, alpn *FrameField
|
||||
for i := range exts.SubFields {
|
||||
switch exts.SubFields[i].Name {
|
||||
case tls.ExtServerName.String():
|
||||
sni = &exts.SubFields[i]
|
||||
case tls.ExtALPN.String():
|
||||
alpn = &exts.SubFields[i]
|
||||
}
|
||||
}
|
||||
if sni == nil {
|
||||
t.Fatal("no server_name extension field")
|
||||
}
|
||||
got := string(pkt[(frames[4].PacketBitOffset+sni.FrameBitOffset)/8:][:sni.BitLength/8])
|
||||
if got != serverName {
|
||||
t.Errorf("server_name=%q want %q", got, serverName)
|
||||
}
|
||||
if alpn == nil {
|
||||
t.Error("no application_layer_protocol_negotiation extension field")
|
||||
}
|
||||
|
||||
// Formatted output is the point of the exercise: the hostname and the
|
||||
// negotiated suites must be readable in one line.
|
||||
var f Formatter
|
||||
f.SubfieldLimit = 32
|
||||
out, err := f.FormatFrames(nil, frames, pkt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
str := string(out)
|
||||
for _, want := range []string{"TLS", "handshake=", "client_hello=", serverName,
|
||||
"TLS_AES_128_GCM_SHA256", "http/1.1"} {
|
||||
if !strings.Contains(str, want) {
|
||||
t.Errorf("formatted output missing %q:\n%s", want, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCaptureTLSRecordSequence checks the multi-record path: a server flight
|
||||
// coalesces ServerHello, the compatibility ChangeCipherSpec and the first
|
||||
// protected record into a single segment.
|
||||
func TestCaptureTLSRecordSequence(t *testing.T) {
|
||||
var b tls.Builder
|
||||
var buf [512]byte
|
||||
b.Reset(buf[:])
|
||||
|
||||
// ServerHello record.
|
||||
b.AddU8(uint8(tls.ContentTypeHandshake))
|
||||
b.AddU16(tls.VersionTLS12)
|
||||
b.OpenU16()
|
||||
b.AddU8(uint8(tls.HandshakeTypeServerHello))
|
||||
b.OpenU24()
|
||||
b.AddU16(tls.VersionTLS12) // legacy_version
|
||||
for range tls.SizeRandom {
|
||||
b.AddU8(0xab) // server_random
|
||||
}
|
||||
b.OpenU8() // legacy_session_id echo
|
||||
for range 32 {
|
||||
b.AddU8(0xcd)
|
||||
}
|
||||
b.Close()
|
||||
b.AddU16(uint16(tls.SuiteAES128GCMSHA256))
|
||||
b.AddU8(0) // legacy_compression_method
|
||||
b.OpenU16() // extensions
|
||||
b.AddU16(uint16(tls.ExtSupportedVersions))
|
||||
b.OpenU16()
|
||||
b.AddU16(tls.VersionTLS13)
|
||||
b.Close()
|
||||
b.AddU16(uint16(tls.ExtKeyShare))
|
||||
b.OpenU16()
|
||||
b.AddU16(uint16(tls.GroupX25519))
|
||||
b.OpenU16()
|
||||
for range 32 {
|
||||
b.AddU8(0xee)
|
||||
}
|
||||
b.Close()
|
||||
b.Close()
|
||||
b.Close() // extensions
|
||||
b.Close() // handshake body
|
||||
b.Close() // record fragment
|
||||
|
||||
// change_cipher_spec record.
|
||||
b.AddU8(uint8(tls.ContentTypeChangeCipherSpec))
|
||||
b.AddU16(tls.VersionTLS12)
|
||||
b.OpenU16()
|
||||
b.AddU8(1)
|
||||
b.Close()
|
||||
|
||||
// First protected record: outwardly application_data.
|
||||
b.AddU8(uint8(tls.ContentTypeApplicationData))
|
||||
b.AddU16(tls.VersionTLS12)
|
||||
b.OpenU16()
|
||||
for range 24 {
|
||||
b.AddU8(0x5a)
|
||||
}
|
||||
b.Close()
|
||||
|
||||
pkt, err := b.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !payloadIsTLS(pkt) {
|
||||
t.Fatal("payloadIsTLS did not recognize a ServerHello record")
|
||||
}
|
||||
|
||||
var pbreak PacketBreakdown
|
||||
pbreak.SubfieldLimit = 8
|
||||
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"TLS", "TLS ServerHello", "TLS", "TLS"}
|
||||
if len(frames) != len(want) {
|
||||
for i := range frames {
|
||||
t.Logf("frame[%d]=%s", i, frames[i].String())
|
||||
}
|
||||
t.Fatalf("got %d frames want %d", len(frames), len(want))
|
||||
}
|
||||
for i, wantProto := range want {
|
||||
if frames[i].Protocol != wantProto {
|
||||
t.Errorf("frame[%d] protocol=%q want %q", i, frames[i].Protocol, wantProto)
|
||||
}
|
||||
if len(frames[i].Errors) > 0 {
|
||||
t.Errorf("frame[%d] errors=%v", i, frames[i].Errors)
|
||||
}
|
||||
}
|
||||
if fieldByName(frames[1], tls.SuiteAES128GCMSHA256.String()) == nil {
|
||||
t.Error("ServerHello cipher suite field not named after the selected suite")
|
||||
}
|
||||
i, err := frames[3].FieldByClass(FieldClassPayload)
|
||||
if err != nil {
|
||||
t.Error("no payload field in application_data record:", err)
|
||||
} else if !frames[3].Fields[i].Flags.IsEncrypted() {
|
||||
t.Error("application_data fragment not flagged as encrypted")
|
||||
}
|
||||
// Record frames must start exactly where the previous record ended, and the
|
||||
// last one must end at the packet end: no record skipped, none double read.
|
||||
off := 0
|
||||
for i := range frames {
|
||||
if frames[i].Protocol != "TLS" {
|
||||
continue
|
||||
}
|
||||
if frames[i].PacketBitOffset != off*octet {
|
||||
t.Errorf("frame[%d] starts at bit %d want %d", i, frames[i].PacketBitOffset, off*octet)
|
||||
}
|
||||
rec, err := tls.NewRecordFrame(pkt[off:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
off += rec.RecordLength()
|
||||
}
|
||||
if off != len(pkt) {
|
||||
t.Errorf("records cover %d bytes of %d", off, len(pkt))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCaptureTLSIncomplete checks the stream cases a stateless breakdown cannot
|
||||
// reassemble: a record whose fragment continues in the next TCP segment, and a
|
||||
// handshake message whose body continues in the next record.
|
||||
func TestCaptureTLSIncomplete(t *testing.T) {
|
||||
t.Run("record", func(t *testing.T) {
|
||||
pkt := []byte{byte(tls.ContentTypeApplicationData), 0x03, 0x03, 0x04, 0x00, 1, 2, 3}
|
||||
var pbreak PacketBreakdown
|
||||
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(frames) != 1 {
|
||||
t.Fatalf("got %d frames want 1", len(frames))
|
||||
}
|
||||
if len(frames[0].Errors) != 1 || frames[0].Errors[0] != tls.ErrNeedMore {
|
||||
t.Errorf("errors=%v want %v", frames[0].Errors, tls.ErrNeedMore)
|
||||
}
|
||||
if got := frames[0].LenBits() / 8; got != len(pkt) {
|
||||
t.Errorf("frame covers %d bytes want %d", got, len(pkt))
|
||||
}
|
||||
})
|
||||
t.Run("handshake", func(t *testing.T) {
|
||||
// A 5-byte record carrying a Certificate message header that declares a
|
||||
// 1000-byte body: the rest arrives in later records.
|
||||
pkt := []byte{
|
||||
byte(tls.ContentTypeHandshake), 0x03, 0x03, 0x00, 0x06,
|
||||
byte(tls.HandshakeTypeCertificate), 0x00, 0x03, 0xe8, 0xaa, 0xbb,
|
||||
}
|
||||
var pbreak PacketBreakdown
|
||||
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(frames) != 2 {
|
||||
t.Fatalf("got %d frames want 2", len(frames))
|
||||
}
|
||||
if frames[1].Protocol != "TLS Handshake" {
|
||||
t.Errorf("protocol=%q want TLS Handshake", frames[1].Protocol)
|
||||
}
|
||||
if len(frames[1].Errors) != 1 || frames[1].Errors[0] != tls.ErrNeedMore {
|
||||
t.Errorf("errors=%v want %v", frames[1].Errors, tls.ErrNeedMore)
|
||||
}
|
||||
if got := frames[1].LenBits() / 8; got != 6 {
|
||||
t.Errorf("handshake frame covers %d bytes want 6", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCaptureTLSAlert checks the cleartext alert path, which is the only way a
|
||||
// TLS 1.3 handshake failure is visible to a capture.
|
||||
func TestCaptureTLSAlert(t *testing.T) {
|
||||
pkt := []byte{
|
||||
byte(tls.ContentTypeAlert), 0x03, 0x03, 0x00, 0x02,
|
||||
byte(tls.AlertLevelFatal), byte(tls.AlertHandshakeFailure),
|
||||
}
|
||||
var pbreak PacketBreakdown
|
||||
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(frames) != 1 {
|
||||
t.Fatalf("got %d frames want 1", len(frames))
|
||||
}
|
||||
if fieldByName(frames[0], tls.AlertHandshakeFailure.String()) == nil {
|
||||
t.Errorf("no field named %q in %s", tls.AlertHandshakeFailure.String(), frames[0].String())
|
||||
}
|
||||
var f Formatter
|
||||
f.DisableLegacyFilter = true
|
||||
out, err := f.FormatFrame(nil, frames[0], pkt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(out), "handshake_failure") {
|
||||
t.Errorf("formatted alert missing description: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPayloadIsTLS guards the heuristic that routes a TCP payload to CaptureTLS
|
||||
// against the HTTP traffic it shares the datapath with.
|
||||
func TestPayloadIsTLS(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
payload []byte
|
||||
want bool
|
||||
}{
|
||||
{"client hello", []byte{22, 3, 1, 0, 10}, true},
|
||||
{"app data", []byte{23, 3, 3, 0x40, 0x00}, true},
|
||||
{"alert", []byte{21, 3, 3, 0, 2}, true},
|
||||
{"http request", []byte("GET / HTTP/1.1\r\n"), false},
|
||||
{"http response", []byte("HTTP/1.1 200 OK\r\n"), false},
|
||||
{"bad content type", []byte{25, 3, 3, 0, 10}, false},
|
||||
{"bad version", []byte{22, 2, 1, 0, 10}, false},
|
||||
{"future version", []byte{22, 3, 5, 0, 10}, false},
|
||||
{"zero length", []byte{22, 3, 3, 0, 0}, false},
|
||||
{"oversize length", []byte{23, 3, 3, 0xff, 0xff}, false},
|
||||
{"short", []byte{22, 3, 3}, false},
|
||||
} {
|
||||
if got := payloadIsTLS(tc.payload); got != tc.want {
|
||||
t.Errorf("%s: payloadIsTLS=%v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCaptureTLSRecordLimit checks that a segment packed with tiny records
|
||||
// cannot make a single packet produce unbounded frames.
|
||||
func TestCaptureTLSRecordLimit(t *testing.T) {
|
||||
var pkt []byte
|
||||
for range maxTLSRecordsPerPacket + 3 {
|
||||
pkt = append(pkt, byte(tls.ContentTypeApplicationData), 3, 3, 0, 1, 0x99)
|
||||
}
|
||||
var pbreak PacketBreakdown
|
||||
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(frames) != maxTLSRecordsPerPacket+1 {
|
||||
t.Fatalf("got %d frames want %d", len(frames), maxTLSRecordsPerPacket+1)
|
||||
}
|
||||
last := frames[len(frames)-1]
|
||||
if last.Protocol != "TLS records?" {
|
||||
t.Errorf("last frame protocol=%q want TLS records?", last.Protocol)
|
||||
}
|
||||
if end := (last.PacketBitOffset + last.LenBits()) / 8; end != len(pkt) {
|
||||
t.Errorf("remaining frame ends at %d want %d", end, len(pkt))
|
||||
}
|
||||
}
|
||||
|
||||
func fieldByName(frm Frame, name string) *FrameField {
|
||||
i := indexOfField(frm, name)
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return &frm.Fields[i]
|
||||
}
|
||||
|
||||
func indexOfField(frm Frame, name string) int {
|
||||
for i := range frm.Fields {
|
||||
if frm.Fields[i].Name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -136,6 +136,9 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
||||
name := field.Name
|
||||
if name == "" {
|
||||
name = field.Class.String()
|
||||
if field.Flags.IsEncrypted() {
|
||||
name = "encrypted"
|
||||
}
|
||||
}
|
||||
hasSpaces := strings.IndexByte(name, ' ') >= 0
|
||||
if hasSpaces {
|
||||
@@ -191,8 +194,10 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
||||
nameOff := field.FrameBitOffset / 8
|
||||
dst = dnsAppendDottedName(dst, dnsMsg, nameOff)
|
||||
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
|
||||
// IP, MAC addresses and ports.
|
||||
if field.BitLength <= 16 {
|
||||
// IP, MAC addresses and ports. Sizes are always numbers, never
|
||||
// addresses, so a wide one such as the TLS 24-bit handshake length is
|
||||
// still printed in decimal.
|
||||
if field.BitLength <= 16 || field.Class == FieldClassSize && field.BitLength <= 64 {
|
||||
v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
|
||||
if err != nil {
|
||||
return dst, err
|
||||
|
||||
@@ -106,6 +106,7 @@ const (
|
||||
ExtMaxFragmentLength ExtensionType = 1 // max_fragment_length
|
||||
ExtStatusRequest ExtensionType = 5 // status_request
|
||||
ExtSupportedGroups ExtensionType = 10 // supported_groups
|
||||
ExtECPointFormats ExtensionType = 11 // ec_point_formats
|
||||
ExtSignatureAlgorithms ExtensionType = 13 // signature_algorithms
|
||||
ExtALPN ExtensionType = 16 // application_layer_protocol_negotiation
|
||||
ExtSignedCertificateTimestamp ExtensionType = 18 // signed_certificate_timestamp
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// Code generated by "stringer -type=ContentType,HandshakeType,ExtensionType,AlertDescription,AlertLevel,NamedGroup,SignatureScheme,CipherSuite -linecomment -output stringers.go ."; DO NOT EDIT.
|
||||
|
||||
package tls
|
||||
|
||||
import "strconv"
|
||||
|
||||
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[ContentTypeInvalid-0]
|
||||
_ = x[ContentTypeChangeCipherSpec-20]
|
||||
_ = x[ContentTypeAlert-21]
|
||||
_ = x[ContentTypeHandshake-22]
|
||||
_ = x[ContentTypeApplicationData-23]
|
||||
}
|
||||
|
||||
const (
|
||||
_ContentType_name_0 = "invalid"
|
||||
_ContentType_name_1 = "change_cipher_specalerthandshakeapplication_data"
|
||||
)
|
||||
|
||||
var (
|
||||
_ContentType_index_1 = [...]uint8{0, 18, 23, 32, 48}
|
||||
)
|
||||
|
||||
func (i ContentType) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _ContentType_name_0
|
||||
case 20 <= i && i <= 23:
|
||||
i -= 20
|
||||
return _ContentType_name_1[_ContentType_index_1[i]:_ContentType_index_1[i+1]]
|
||||
default:
|
||||
return "ContentType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
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[HandshakeTypeClientHello-1]
|
||||
_ = x[HandshakeTypeServerHello-2]
|
||||
_ = x[HandshakeTypeNewSessionTicket-4]
|
||||
_ = x[HandshakeTypeEndOfEarlyData-5]
|
||||
_ = x[HandshakeTypeEncryptedExtensions-8]
|
||||
_ = x[HandshakeTypeCertificate-11]
|
||||
_ = x[HandshakeTypeCertificateRequest-13]
|
||||
_ = x[HandshakeTypeCertificateVerify-15]
|
||||
_ = x[HandshakeTypeFinished-20]
|
||||
_ = x[HandshakeTypeKeyUpdate-24]
|
||||
_ = x[HandshakeTypeMessageHash-254]
|
||||
}
|
||||
|
||||
const (
|
||||
_HandshakeType_name_0 = "client_helloserver_hello"
|
||||
_HandshakeType_name_1 = "new_session_ticketend_of_early_data"
|
||||
_HandshakeType_name_2 = "encrypted_extensions"
|
||||
_HandshakeType_name_3 = "certificate"
|
||||
_HandshakeType_name_4 = "certificate_request"
|
||||
_HandshakeType_name_5 = "certificate_verify"
|
||||
_HandshakeType_name_6 = "finished"
|
||||
_HandshakeType_name_7 = "key_update"
|
||||
_HandshakeType_name_8 = "message_hash"
|
||||
)
|
||||
|
||||
var (
|
||||
_HandshakeType_index_0 = [...]uint8{0, 12, 24}
|
||||
_HandshakeType_index_1 = [...]uint8{0, 18, 35}
|
||||
)
|
||||
|
||||
func (i HandshakeType) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _HandshakeType_name_0[_HandshakeType_index_0[i]:_HandshakeType_index_0[i+1]]
|
||||
case 4 <= i && i <= 5:
|
||||
i -= 4
|
||||
return _HandshakeType_name_1[_HandshakeType_index_1[i]:_HandshakeType_index_1[i+1]]
|
||||
case i == 8:
|
||||
return _HandshakeType_name_2
|
||||
case i == 11:
|
||||
return _HandshakeType_name_3
|
||||
case i == 13:
|
||||
return _HandshakeType_name_4
|
||||
case i == 15:
|
||||
return _HandshakeType_name_5
|
||||
case i == 20:
|
||||
return _HandshakeType_name_6
|
||||
case i == 24:
|
||||
return _HandshakeType_name_7
|
||||
case i == 254:
|
||||
return _HandshakeType_name_8
|
||||
default:
|
||||
return "HandshakeType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
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[ExtServerName-0]
|
||||
_ = x[ExtMaxFragmentLength-1]
|
||||
_ = x[ExtStatusRequest-5]
|
||||
_ = x[ExtSupportedGroups-10]
|
||||
_ = x[ExtECPointFormats-11]
|
||||
_ = x[ExtSignatureAlgorithms-13]
|
||||
_ = x[ExtALPN-16]
|
||||
_ = x[ExtSignedCertificateTimestamp-18]
|
||||
_ = x[ExtPadding-21]
|
||||
_ = x[ExtExtendedMasterSecret-23]
|
||||
_ = x[ExtCompressCertificate-27]
|
||||
_ = x[ExtRecordSizeLimit-28]
|
||||
_ = x[ExtSessionTicket-35]
|
||||
_ = x[ExtPreSharedKey-41]
|
||||
_ = x[ExtEarlyData-42]
|
||||
_ = x[ExtSupportedVersions-43]
|
||||
_ = x[ExtCookie-44]
|
||||
_ = x[ExtPSKKeyExchangeModes-45]
|
||||
_ = x[ExtCertificateAuthorities-47]
|
||||
_ = x[ExtSignatureAlgorithmsCert-50]
|
||||
_ = x[ExtKeyShare-51]
|
||||
_ = x[ExtApplicationSettings-17513]
|
||||
_ = x[ExtEncryptedClientHello-65037]
|
||||
_ = x[ExtRenegotiationInfo-65281]
|
||||
}
|
||||
|
||||
const _ExtensionType_name = "server_namemax_fragment_lengthstatus_requestsupported_groupsec_point_formatssignature_algorithmsapplication_layer_protocol_negotiationsigned_certificate_timestamppaddingextended_master_secretcompress_certificaterecord_size_limitsession_ticketpre_shared_keyearly_datasupported_versionscookiepsk_key_exchange_modescertificate_authoritiessignature_algorithms_certkey_shareapplication_settingsencrypted_client_hellorenegotiation_info"
|
||||
|
||||
var _ExtensionType_map = map[ExtensionType]string{
|
||||
0: _ExtensionType_name[0:11],
|
||||
1: _ExtensionType_name[11:30],
|
||||
5: _ExtensionType_name[30:44],
|
||||
10: _ExtensionType_name[44:60],
|
||||
11: _ExtensionType_name[60:76],
|
||||
13: _ExtensionType_name[76:96],
|
||||
16: _ExtensionType_name[96:134],
|
||||
18: _ExtensionType_name[134:162],
|
||||
21: _ExtensionType_name[162:169],
|
||||
23: _ExtensionType_name[169:191],
|
||||
27: _ExtensionType_name[191:211],
|
||||
28: _ExtensionType_name[211:228],
|
||||
35: _ExtensionType_name[228:242],
|
||||
41: _ExtensionType_name[242:256],
|
||||
42: _ExtensionType_name[256:266],
|
||||
43: _ExtensionType_name[266:284],
|
||||
44: _ExtensionType_name[284:290],
|
||||
45: _ExtensionType_name[290:312],
|
||||
47: _ExtensionType_name[312:335],
|
||||
50: _ExtensionType_name[335:360],
|
||||
51: _ExtensionType_name[360:369],
|
||||
17513: _ExtensionType_name[369:389],
|
||||
65037: _ExtensionType_name[389:411],
|
||||
65281: _ExtensionType_name[411:429],
|
||||
}
|
||||
|
||||
func (i ExtensionType) String() string {
|
||||
if str, ok := _ExtensionType_map[i]; ok {
|
||||
return str
|
||||
}
|
||||
return "ExtensionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
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[AlertCloseNotify-0]
|
||||
_ = x[AlertUnexpectedMessage-10]
|
||||
_ = x[AlertBadRecordMAC-20]
|
||||
_ = x[AlertRecordOverflow-22]
|
||||
_ = x[AlertHandshakeFailure-40]
|
||||
_ = x[AlertBadCertificate-42]
|
||||
_ = x[AlertUnsupportedCertificate-43]
|
||||
_ = x[AlertCertificateRevoked-44]
|
||||
_ = x[AlertCertificateExpired-45]
|
||||
_ = x[AlertCertificateUnknown-46]
|
||||
_ = x[AlertIllegalParameter-47]
|
||||
_ = x[AlertUnknownCA-48]
|
||||
_ = x[AlertAccessDenied-49]
|
||||
_ = x[AlertDecodeError-50]
|
||||
_ = x[AlertDecryptError-51]
|
||||
_ = x[AlertProtocolVersion-70]
|
||||
_ = x[AlertInsufficientSecurity-71]
|
||||
_ = x[AlertInternalError-80]
|
||||
_ = x[AlertInappropriateFallback-86]
|
||||
_ = x[AlertUserCanceled-90]
|
||||
_ = x[AlertMissingExtension-109]
|
||||
_ = x[AlertUnsupportedExtension-110]
|
||||
_ = x[AlertUnrecognizedName-112]
|
||||
_ = x[AlertBadCertificateStatusResponse-113]
|
||||
_ = x[AlertUnknownPSKIdentity-115]
|
||||
_ = x[AlertCertificateRequired-116]
|
||||
_ = x[AlertNoApplicationProtocol-120]
|
||||
}
|
||||
|
||||
const _AlertDescription_name = "close_notifyunexpected_messagebad_record_macrecord_overflowhandshake_failurebad_certificateunsupported_certificatecertificate_revokedcertificate_expiredcertificate_unknownillegal_parameterunknown_caaccess_denieddecode_errordecrypt_errorprotocol_versioninsufficient_securityinternal_errorinappropriate_fallbackuser_canceledmissing_extensionunsupported_extensionunrecognized_namebad_certificate_status_responseunknown_psk_identitycertificate_requiredno_application_protocol"
|
||||
|
||||
var _AlertDescription_map = map[AlertDescription]string{
|
||||
0: _AlertDescription_name[0:12],
|
||||
10: _AlertDescription_name[12:30],
|
||||
20: _AlertDescription_name[30:44],
|
||||
22: _AlertDescription_name[44:59],
|
||||
40: _AlertDescription_name[59:76],
|
||||
42: _AlertDescription_name[76:91],
|
||||
43: _AlertDescription_name[91:114],
|
||||
44: _AlertDescription_name[114:133],
|
||||
45: _AlertDescription_name[133:152],
|
||||
46: _AlertDescription_name[152:171],
|
||||
47: _AlertDescription_name[171:188],
|
||||
48: _AlertDescription_name[188:198],
|
||||
49: _AlertDescription_name[198:211],
|
||||
50: _AlertDescription_name[211:223],
|
||||
51: _AlertDescription_name[223:236],
|
||||
70: _AlertDescription_name[236:252],
|
||||
71: _AlertDescription_name[252:273],
|
||||
80: _AlertDescription_name[273:287],
|
||||
86: _AlertDescription_name[287:309],
|
||||
90: _AlertDescription_name[309:322],
|
||||
109: _AlertDescription_name[322:339],
|
||||
110: _AlertDescription_name[339:360],
|
||||
112: _AlertDescription_name[360:377],
|
||||
113: _AlertDescription_name[377:408],
|
||||
115: _AlertDescription_name[408:428],
|
||||
116: _AlertDescription_name[428:448],
|
||||
120: _AlertDescription_name[448:471],
|
||||
}
|
||||
|
||||
func (i AlertDescription) String() string {
|
||||
if str, ok := _AlertDescription_map[i]; ok {
|
||||
return str
|
||||
}
|
||||
return "AlertDescription(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
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[AlertLevelWarning-1]
|
||||
_ = x[AlertLevelFatal-2]
|
||||
}
|
||||
|
||||
const _AlertLevel_name = "warningfatal"
|
||||
|
||||
var _AlertLevel_index = [...]uint8{0, 7, 12}
|
||||
|
||||
func (i AlertLevel) String() string {
|
||||
i -= 1
|
||||
if i >= AlertLevel(len(_AlertLevel_index)-1) {
|
||||
return "AlertLevel(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _AlertLevel_name[_AlertLevel_index[i]:_AlertLevel_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[GroupSECP256R1-23]
|
||||
_ = x[GroupSECP384R1-24]
|
||||
_ = x[GroupSECP521R1-25]
|
||||
_ = x[GroupX25519-29]
|
||||
_ = x[GroupX448-30]
|
||||
_ = x[GroupX25519MLKEM768-4588]
|
||||
}
|
||||
|
||||
const (
|
||||
_NamedGroup_name_0 = "secp256r1secp384r1secp521r1"
|
||||
_NamedGroup_name_1 = "x25519x448"
|
||||
_NamedGroup_name_2 = "x25519mlkem768"
|
||||
)
|
||||
|
||||
var (
|
||||
_NamedGroup_index_0 = [...]uint8{0, 9, 18, 27}
|
||||
_NamedGroup_index_1 = [...]uint8{0, 6, 10}
|
||||
)
|
||||
|
||||
func (i NamedGroup) String() string {
|
||||
switch {
|
||||
case 23 <= i && i <= 25:
|
||||
i -= 23
|
||||
return _NamedGroup_name_0[_NamedGroup_index_0[i]:_NamedGroup_index_0[i+1]]
|
||||
case 29 <= i && i <= 30:
|
||||
i -= 29
|
||||
return _NamedGroup_name_1[_NamedGroup_index_1[i]:_NamedGroup_index_1[i+1]]
|
||||
case i == 4588:
|
||||
return _NamedGroup_name_2
|
||||
default:
|
||||
return "NamedGroup(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
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[SigRSAPKCS1SHA256-1025]
|
||||
_ = x[SigRSAPKCS1SHA384-1281]
|
||||
_ = x[SigRSAPKCS1SHA512-1537]
|
||||
_ = x[SigECDSAP256SHA256-1027]
|
||||
_ = x[SigECDSAP384SHA384-1283]
|
||||
_ = x[SigECDSAP521SHA512-1539]
|
||||
_ = x[SigRSAPSSRSAESHA256-2052]
|
||||
_ = x[SigRSAPSSRSAESHA384-2053]
|
||||
_ = x[SigRSAPSSRSAESHA512-2054]
|
||||
_ = x[SigEd25519-2055]
|
||||
_ = x[SigRSAPSSPSSSHA256-2057]
|
||||
}
|
||||
|
||||
const (
|
||||
_SignatureScheme_name_0 = "rsa_pkcs1_sha256"
|
||||
_SignatureScheme_name_1 = "ecdsa_secp256r1_sha256"
|
||||
_SignatureScheme_name_2 = "rsa_pkcs1_sha384"
|
||||
_SignatureScheme_name_3 = "ecdsa_secp384r1_sha384"
|
||||
_SignatureScheme_name_4 = "rsa_pkcs1_sha512"
|
||||
_SignatureScheme_name_5 = "ecdsa_secp521r1_sha512"
|
||||
_SignatureScheme_name_6 = "rsa_pss_rsae_sha256rsa_pss_rsae_sha384rsa_pss_rsae_sha512ed25519"
|
||||
_SignatureScheme_name_7 = "rsa_pss_pss_sha256"
|
||||
)
|
||||
|
||||
var (
|
||||
_SignatureScheme_index_6 = [...]uint8{0, 19, 38, 57, 64}
|
||||
)
|
||||
|
||||
func (i SignatureScheme) String() string {
|
||||
switch {
|
||||
case i == 1025:
|
||||
return _SignatureScheme_name_0
|
||||
case i == 1027:
|
||||
return _SignatureScheme_name_1
|
||||
case i == 1281:
|
||||
return _SignatureScheme_name_2
|
||||
case i == 1283:
|
||||
return _SignatureScheme_name_3
|
||||
case i == 1537:
|
||||
return _SignatureScheme_name_4
|
||||
case i == 1539:
|
||||
return _SignatureScheme_name_5
|
||||
case 2052 <= i && i <= 2055:
|
||||
i -= 2052
|
||||
return _SignatureScheme_name_6[_SignatureScheme_index_6[i]:_SignatureScheme_index_6[i+1]]
|
||||
case i == 2057:
|
||||
return _SignatureScheme_name_7
|
||||
default:
|
||||
return "SignatureScheme(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
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[SuiteAES128GCMSHA256-4865]
|
||||
_ = x[SuiteAES256GCMSHA384-4866]
|
||||
_ = x[SuiteChaCha20Poly1305SHA256-4867]
|
||||
_ = x[SuiteAES128CCMSHA256-4868]
|
||||
_ = x[SuiteAES128CCM8SHA256-4869]
|
||||
}
|
||||
|
||||
const _CipherSuite_name = "TLS_AES_128_GCM_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256TLS_AES_128_CCM_8_SHA256"
|
||||
|
||||
var _CipherSuite_index = [...]uint8{0, 22, 44, 72, 94, 118}
|
||||
|
||||
func (i CipherSuite) String() string {
|
||||
i -= 4865
|
||||
if i >= CipherSuite(len(_CipherSuite_index)-1) {
|
||||
return "CipherSuite(" + strconv.FormatInt(int64(i+4865), 10) + ")"
|
||||
}
|
||||
return _CipherSuite_name[_CipherSuite_index[i]:_CipherSuite_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package tls
|
||||
|
||||
// StringConst methods name values as String does but always return a
|
||||
// compile-time constant, unlike String which formats an unrecognized value with
|
||||
// strconv. Wire values are attacker controlled, so decoders and logs on
|
||||
// embedded targets use StringConst and print the number alongside it.
|
||||
|
||||
const (
|
||||
// nameGREASE names the reserved values of RFC 8701, which carry no meaning.
|
||||
nameGREASE = "GREASE"
|
||||
// nameUnknown names a value this package does not recognize. Other RFCs may
|
||||
// well define it: this is TLS 1.3 only.
|
||||
nameUnknown = "unknown"
|
||||
)
|
||||
|
||||
// StringConst returns the content type's name, or "unknown".
|
||||
func (v ContentType) StringConst() string {
|
||||
switch v {
|
||||
case ContentTypeInvalid, ContentTypeChangeCipherSpec, ContentTypeAlert,
|
||||
ContentTypeHandshake, ContentTypeApplicationData:
|
||||
return v.String()
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the handshake message type's name, or "unknown".
|
||||
func (v HandshakeType) StringConst() string {
|
||||
switch v {
|
||||
case HandshakeTypeClientHello, HandshakeTypeServerHello, HandshakeTypeNewSessionTicket,
|
||||
HandshakeTypeEndOfEarlyData, HandshakeTypeEncryptedExtensions, HandshakeTypeCertificate,
|
||||
HandshakeTypeCertificateRequest, HandshakeTypeCertificateVerify, HandshakeTypeFinished,
|
||||
HandshakeTypeKeyUpdate, HandshakeTypeMessageHash:
|
||||
return v.String()
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the extension type's name, "GREASE" or "unknown".
|
||||
func (v ExtensionType) StringConst() string {
|
||||
switch v {
|
||||
case ExtServerName, ExtMaxFragmentLength, ExtStatusRequest, ExtSupportedGroups,
|
||||
ExtECPointFormats, ExtSignatureAlgorithms, ExtALPN, ExtSignedCertificateTimestamp,
|
||||
ExtPadding, ExtExtendedMasterSecret, ExtCompressCertificate, ExtRecordSizeLimit,
|
||||
ExtSessionTicket, ExtPreSharedKey, ExtEarlyData, ExtSupportedVersions, ExtCookie,
|
||||
ExtPSKKeyExchangeModes, ExtCertificateAuthorities, ExtSignatureAlgorithmsCert,
|
||||
ExtKeyShare, ExtApplicationSettings, ExtEncryptedClientHello, ExtRenegotiationInfo:
|
||||
return v.String()
|
||||
}
|
||||
if IsGREASE(uint16(v)) {
|
||||
return nameGREASE
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the alert level's name, or "unknown".
|
||||
func (v AlertLevel) StringConst() string {
|
||||
switch v {
|
||||
case AlertLevelWarning, AlertLevelFatal:
|
||||
return v.String()
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the alert description's name, or "unknown".
|
||||
func (v AlertDescription) StringConst() string {
|
||||
switch v {
|
||||
case AlertCloseNotify, AlertUnexpectedMessage, AlertBadRecordMAC, AlertRecordOverflow,
|
||||
AlertHandshakeFailure, AlertBadCertificate, AlertUnsupportedCertificate,
|
||||
AlertCertificateRevoked, AlertCertificateExpired, AlertCertificateUnknown,
|
||||
AlertIllegalParameter, AlertUnknownCA, AlertAccessDenied, AlertDecodeError,
|
||||
AlertDecryptError, AlertProtocolVersion, AlertInsufficientSecurity,
|
||||
AlertInternalError, AlertInappropriateFallback, AlertUserCanceled,
|
||||
AlertMissingExtension, AlertUnsupportedExtension, AlertUnrecognizedName,
|
||||
AlertBadCertificateStatusResponse, AlertUnknownPSKIdentity,
|
||||
AlertCertificateRequired, AlertNoApplicationProtocol:
|
||||
return v.String()
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the named group's name, "GREASE" or "unknown".
|
||||
func (v NamedGroup) StringConst() string {
|
||||
switch v {
|
||||
case GroupSECP256R1, GroupSECP384R1, GroupSECP521R1,
|
||||
GroupX25519, GroupX448, GroupX25519MLKEM768:
|
||||
return v.String()
|
||||
}
|
||||
if IsGREASE(uint16(v)) {
|
||||
return nameGREASE
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the signature scheme's name, "GREASE" or "unknown".
|
||||
func (v SignatureScheme) StringConst() string {
|
||||
switch v {
|
||||
case SigRSAPKCS1SHA256, SigRSAPKCS1SHA384, SigRSAPKCS1SHA512,
|
||||
SigECDSAP256SHA256, SigECDSAP384SHA384, SigECDSAP521SHA512,
|
||||
SigRSAPSSRSAESHA256, SigRSAPSSRSAESHA384, SigRSAPSSRSAESHA512,
|
||||
SigEd25519, SigRSAPSSPSSSHA256:
|
||||
return v.String()
|
||||
}
|
||||
if IsGREASE(uint16(v)) {
|
||||
return nameGREASE
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
|
||||
// StringConst returns the cipher suite's name, "GREASE" or "unknown".
|
||||
// Every TLS 1.2 suite a browser still offers is undefined here: this package
|
||||
// implements TLS 1.3 only.
|
||||
func (v CipherSuite) StringConst() string {
|
||||
switch v {
|
||||
case SuiteAES128GCMSHA256, SuiteAES256GCMSHA384, SuiteChaCha20Poly1305SHA256,
|
||||
SuiteAES128CCMSHA256, SuiteAES128CCM8SHA256:
|
||||
return v.String()
|
||||
}
|
||||
if IsGREASE(uint16(v)) {
|
||||
return nameGREASE
|
||||
}
|
||||
return nameUnknown
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package tls_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto/x/tls"
|
||||
)
|
||||
|
||||
// StringConst must name defined values as String does, GREASE values "GREASE"
|
||||
// and everything else "unknown", over the whole range and without allocating.
|
||||
// A constant added without extending a StringConst switch fails here.
|
||||
func TestStringConst(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
n int
|
||||
grease bool // type carries GREASE values
|
||||
str func(int) string
|
||||
cst func(int) string
|
||||
}{
|
||||
{"ContentType", 1 << 8, false,
|
||||
func(i int) string { return tls.ContentType(i).String() },
|
||||
func(i int) string { return tls.ContentType(i).StringConst() }},
|
||||
{"HandshakeType", 1 << 8, false,
|
||||
func(i int) string { return tls.HandshakeType(i).String() },
|
||||
func(i int) string { return tls.HandshakeType(i).StringConst() }},
|
||||
{"AlertLevel", 1 << 8, false,
|
||||
func(i int) string { return tls.AlertLevel(i).String() },
|
||||
func(i int) string { return tls.AlertLevel(i).StringConst() }},
|
||||
{"AlertDescription", 1 << 8, false,
|
||||
func(i int) string { return tls.AlertDescription(i).String() },
|
||||
func(i int) string { return tls.AlertDescription(i).StringConst() }},
|
||||
{"ExtensionType", 1 << 16, true,
|
||||
func(i int) string { return tls.ExtensionType(i).String() },
|
||||
func(i int) string { return tls.ExtensionType(i).StringConst() }},
|
||||
{"NamedGroup", 1 << 16, true,
|
||||
func(i int) string { return tls.NamedGroup(i).String() },
|
||||
func(i int) string { return tls.NamedGroup(i).StringConst() }},
|
||||
{"SignatureScheme", 1 << 16, true,
|
||||
func(i int) string { return tls.SignatureScheme(i).String() },
|
||||
func(i int) string { return tls.SignatureScheme(i).StringConst() }},
|
||||
{"CipherSuite", 1 << 16, true,
|
||||
func(i int) string { return tls.CipherSuite(i).String() },
|
||||
func(i int) string { return tls.CipherSuite(i).StringConst() }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for i := range tc.n {
|
||||
str, cst := tc.str(i), tc.cst(i)
|
||||
if !strings.ContainsRune(str, '(') { // stringer names undefined values "Type(9)".
|
||||
if cst != str {
|
||||
t.Fatalf("%s(%d)=%q want %q", tc.name, i, cst, str)
|
||||
}
|
||||
continue
|
||||
}
|
||||
want := "unknown"
|
||||
if tc.grease && tls.IsGREASE(uint16(i)) {
|
||||
want = "GREASE"
|
||||
}
|
||||
if cst != want {
|
||||
t.Fatalf("%s(%d)=%q want %q", tc.name, i, cst, want)
|
||||
}
|
||||
}
|
||||
allocs := testing.AllocsPerRun(1, func() {
|
||||
for i := range tc.n {
|
||||
_ = tc.cst(i)
|
||||
}
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("%s allocated %v times", tc.name, allocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user