diff --git a/lneto/dhcp/stringers.go b/lneto/dhcp/stringers.go index 48335f3..618b740 100644 --- a/lneto/dhcp/stringers.go +++ b/lneto/dhcp/stringers.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=OptNum -linecomment -output stringers.go"; DO NOT EDIT. +// Code generated by "stringer -type=OptNum,Op,MessageType -linecomment -output stringers.go"; DO NOT EDIT. package dhcp @@ -72,9 +72,9 @@ func _() { _ = x[OptClientIdentifier1-61] } -const _OptNum_name = "OptWordAlignedOptSubnetMaskTime offset in seconds from UTCN/4 router addressesN/4 time server addressesN/4 IEN-116 server addressesN/4 DNS server addressesN/4 logging server addressesN/4 quote server addressesN/4 printer server addressesN/4 impress server addressesN/4 RLP server addressesHostname stringSize of boot file in 512 byte chunksClient to dump and name of file to dump toThe DNS domain name of the clientSwap server addressesPath name for root diskPatch name for more BOOTP infoEnable or disable IP forwardingEnable or disable source routingRouting policy filtersMaximum datagram reassembly sizeDefault IP time-to-livePath MTU aging timeoutPath MTU plateau tableInterface MTU sizeAll subnets are localBroadcast addressPerform mask discoveryProvide mask to othersPerform router discoveryRouter solicitation addressStatic routing tableTrailer encapsulationARP cache timeoutEthernet encapsulationDefault TCP time to liveTCP keepalive intervalTCP keepalive garbageNIS domain nameNIS server addressesNTP servers addressesVendor specific informationNetBIOS name serverNetBIOS datagram distributionNetBIOS node typeNetBIOS scopeX window font serverX window display managerRequested IP addressIP address lease timeOverload “sname” or “file”DHCP message type.DHCP server identificationParameter request listDHCP error messageDHCP maximum message sizeDHCP renewal (T1) timeDHCP rebinding (T2) timeClient identifierClient identifier" +const _OptNum_name = "word-alignedsubnet maskTime offset in seconds from UTCN/4 router addressesN/4 time server addressesN/4 IEN-116 server addressesN/4 DNS server addressesN/4 logging server addressesN/4 quote server addressesN/4 printer server addressesN/4 impress server addressesN/4 RLP server addressesHostname stringSize of boot file in 512 byte chunksClient to dump and name of file to dump toThe DNS domain name of the clientSwap server addressesPath name for root diskPatch name for more BOOTP infoEnable or disable IP forwardingEnable or disable source routingRouting policy filtersMaximum datagram reassembly sizeDefault IP time-to-livePath MTU aging timeoutPath MTU plateau tableInterface MTU sizeAll subnets are localBroadcast addressPerform mask discoveryProvide mask to othersPerform router discoveryRouter solicitation addressStatic routing tableTrailer encapsulationARP cache timeoutEthernet encapsulationDefault TCP time to liveTCP keepalive intervalTCP keepalive garbageNIS domain nameNIS server addressesNTP servers addressesVendor specific informationNetBIOS name serverNetBIOS datagram distributionNetBIOS node typeNetBIOS scopeX window font serverX window display managerRequested IP addressIP address lease timeOverload “sname” or “file”DHCP message type.DHCP server identificationParameter request listDHCP error messageDHCP maximum message sizeDHCP renewal (T1) timeDHCP rebinding (T2) timeClient identifierClient identifier(1)" -var _OptNum_index = [...]uint16{0, 14, 27, 58, 78, 103, 131, 155, 183, 209, 237, 265, 289, 304, 340, 382, 415, 436, 459, 489, 520, 552, 574, 606, 629, 651, 673, 691, 712, 729, 751, 773, 797, 824, 844, 865, 882, 904, 928, 950, 971, 986, 1006, 1027, 1054, 1073, 1102, 1119, 1132, 1152, 1176, 1196, 1217, 1251, 1269, 1295, 1317, 1335, 1360, 1382, 1406, 1423, 1440} +var _OptNum_index = [...]uint16{0, 12, 23, 54, 74, 99, 127, 151, 179, 205, 233, 261, 285, 300, 336, 378, 411, 432, 455, 485, 516, 548, 570, 602, 625, 647, 669, 687, 708, 725, 747, 769, 793, 820, 840, 861, 878, 900, 924, 946, 967, 982, 1002, 1023, 1050, 1069, 1098, 1115, 1128, 1148, 1172, 1192, 1213, 1247, 1265, 1291, 1313, 1331, 1356, 1378, 1402, 1419, 1439} func (i OptNum) String() string { if i >= OptNum(len(_OptNum_index)-1) { @@ -82,3 +82,47 @@ func (i OptNum) String() string { } return _OptNum_name[_OptNum_index[i]:_OptNum_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[opUndefined-0] + _ = x[OpRequest-1] + _ = x[OpReply-2] +} + +const _Op_name = "undefinedrequestreply" + +var _Op_index = [...]uint8{0, 9, 16, 21} + +func (i Op) String() string { + if i >= Op(len(_Op_index)-1) { + return "Op(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Op_name[_Op_index[i]:_Op_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[msg-0] + _ = x[MsgDiscover-1] + _ = x[MsgOffer-2] + _ = x[MsgRequest-3] + _ = x[MsgDecline-4] + _ = x[MsgAck-5] + _ = x[MsgNak-6] + _ = x[MsgRelease-7] + _ = x[MsgInform-8] +} + +const _MessageType_name = "undefineddiscoverofferrequestdeclineacknakreleaseinform" + +var _MessageType_index = [...]uint8{0, 9, 17, 22, 29, 36, 39, 42, 49, 55} + +func (i MessageType) String() string { + if i >= MessageType(len(_MessageType_index)-1) { + return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] +} diff --git a/lneto/dns/definitions.go b/lneto/dns/definitions.go new file mode 100644 index 0000000..8559eb3 --- /dev/null +++ b/lneto/dns/definitions.go @@ -0,0 +1,221 @@ +package dns + +import ( + "encoding/binary" + "errors" +) + +//go:generate stringer -type=Type,Class,RCode,OpCode -linecomment -output stringers.go . + +// common errors. Taken from golang.org/x/net/dns/dnsmessage module. +var ( + errNameTooLong = errors.New("DNS name exceeds maximum length") + errNoNullTerm = errors.New("DNS name missing null terminator") + errCalcLen = errors.New("DNS calculated name label length exceeds remaining buffer length") + errCantAddLabel = errors.New("long/empty/zterm/escape DNS label or not enough space") + errBaseLen = errors.New("insufficient data for base length type") + errReserved = errors.New("segment prefix is reserved") + errTooManyPtr = errors.New("too many pointers (>10)") + errInvalidPtr = errors.New("invalid pointer") + errInvalidName = errors.New("invalid dns name") + errNilResouceBody = errors.New("nil resource body") + errResourceLen = errors.New("insufficient data for resource body length") + errSegTooLong = errors.New("segment length too long") + errZeroSegLen = errors.New("zero length segment") + errResTooLong = errors.New("resource length too long") + errTooManyQuestions = errors.New("too many Questions") + errTooManyAnswers = errors.New("too many Answers") + errTooManyAuthorities = errors.New("too many Authorities") + errTooManyAdditionals = errors.New("too many Additionals") + errNonCanonicalName = errors.New("name is not in canonical format (it must end with a .)") + errStringTooLong = errors.New("character string exceeds maximum length (255)") + errCompressedSRV = errors.New("compressed name in SRV resource data") +) + +type Frame struct { + buf []byte +} + +func NewFrame(buf []byte) Frame { + return Frame{buf: buf} +} + +func (frm Frame) TxID() uint16 { + return binary.BigEndian.Uint16(frm.buf[0:2]) +} + +func (frm Frame) SetTxID(txid uint16) { + binary.BigEndian.PutUint16(frm.buf[0:2], txid) +} + +func (frm Frame) Flags() HeaderFlags { + return HeaderFlags(binary.BigEndian.Uint16(frm.buf[2:4])) +} + +func (frm Frame) SetFlags(flags HeaderFlags) { + binary.BigEndian.PutUint16(frm.buf[2:4], uint16(flags)) +} + +// QDCount returns number of entries in the question section. +func (frm Frame) QDCount() uint16 { + return binary.BigEndian.Uint16(frm.buf[4:6]) +} + +func (frm Frame) SetQDCount(qdCount uint16) { + binary.BigEndian.PutUint16(frm.buf[4:6], qdCount) +} + +// ANCount returns number of resource records in the answer section. +func (frm Frame) ANCount() uint16 { + return binary.BigEndian.Uint16(frm.buf[6:8]) +} + +func (frm Frame) SetANCount(anCount uint16) { + binary.BigEndian.PutUint16(frm.buf[6:8], anCount) +} + +// NSCount returns number of name server resource records in the authority records section. +func (frm Frame) NSCount() uint16 { + return binary.BigEndian.Uint16(frm.buf[8:10]) +} + +func (frm Frame) SetNSCount(nsCount uint16) { + binary.BigEndian.PutUint16(frm.buf[8:10], nsCount) +} + +// ARCount returns number of resource records in the additional records section. +func (frm Frame) ARCount() uint16 { + return binary.BigEndian.Uint16(frm.buf[10:12]) +} + +func (frm Frame) SetARCount(arCount uint16) { + binary.BigEndian.PutUint16(frm.buf[10:12], arCount) +} + +// HeaderFlags gathers the flags in bits 16..31 of the header. +type HeaderFlags uint16 + +// NewClientHeaderFlags creates the header flags for a client request. +func NewClientHeaderFlags(op OpCode, enableRecursion bool) HeaderFlags { + return HeaderFlags(op&0b1111)<<11 | HeaderFlags(b2u8(enableRecursion))<<8 +} + +// IsResponse returns QR bit which specifies whether this message is a query (0), or a response (1). +func (flags HeaderFlags) IsResponse() bool { return flags&(1<<15) != 0 } + +// OpCode returns the 4-bit opcode. +func (flags HeaderFlags) OpCode() OpCode { return OpCode(flags>>11) & 0b1111 } + +// IsAuthorativeAnswer returns AA bit which specifies that the responding name server is an authority for the domain name in question section. +func (flags HeaderFlags) IsAuthorativeAnswer() bool { return flags&(1<<10) != 0 } + +// IsTruncated returns TC bit which specifies that this message was truncated due to length greater than that permitted on the transmission channel. +func (flags HeaderFlags) IsTruncated() bool { return flags&(1<<9) != 0 } + +// IsRecursionDesired returns RD bit which specifies whether recursive query support is desired by the client. Is optionally set by client. +func (flags HeaderFlags) IsRecursionDesired() bool { return flags&(1<<8) != 0 } + +// IsRecursionAvailable returns RA bit which specifies whether recursive query support is available by the server. +func (flags HeaderFlags) IsRecursionAvailable() bool { return flags&(1<<7) != 0 } + +// ResponseCode returns the 4-bit response code set as part of responses. +func (flags HeaderFlags) ResponseCode() RCode { return RCode(flags & 0b1111) } + +func (flags HeaderFlags) String() string { + buf := make([]byte, 0, 16) + return string(flags.appendF(buf)) +} + +func (flags HeaderFlags) appendF(buf []byte) []byte { + writeBit := func(b bool, s string) { + if b { + buf = append(buf, s...) + buf = append(buf, ' ') + } + } + writeBit(flags.IsResponse(), "QR") + writeBit(flags.IsAuthorativeAnswer(), "AA") + writeBit(flags.IsTruncated(), "TC") + writeBit(flags.IsRecursionDesired(), "RD") + writeBit(flags.IsRecursionAvailable(), "RA") + buf = append(buf, flags.OpCode().String()...) + buf = append(buf, ' ') + buf = append(buf, flags.ResponseCode().String()...) + return buf +} + +const allowCompression = true + +// Types taken from golang.org/x/net/dns/dnsmessage package. See https://pkg.go.dev/golang.org/x/net/dns/dnsmessage. + +// Type is a type of DNS request and response. +type Type uint16 + +const ( + // ResourceHeader.Type and Question.Type + TypeA Type = 1 // A + TypeNS Type = 2 // NS + TypeCNAME Type = 5 // CNAME + TypeSOA Type = 6 // SOA + TypePTR Type = 12 // PTR + TypeMX Type = 15 // MX + TypeTXT Type = 16 // TXT + TypeAAAA Type = 28 // AAAA + TypeSRV Type = 33 // SRV + TypeOPT Type = 41 // OPT + + // Question.Type + TypeWKS Type = 11 // WKS + TypeHINFO Type = 13 // HINFO + TypeMINFO Type = 14 // MINFO + TypeAXFR Type = 252 // AXFR + TypeALL Type = 255 // ALL +) + +// A Class is a type of network. +type Class uint16 + +const ( + // ResourceHeader.Class and Question.Class + ClassINET Class = 1 // INET + ClassCSNET Class = 2 // CSNET + ClassCHAOS Class = 3 // CHAOS + ClassHESIOD Class = 4 // HESIOD + + // Question.Class + ClassANY Class = 255 // ANY +) + +// An OpCode is a DNS operation code which specifies the type of query. +type OpCode uint16 + +const ( + OpCodeQuery OpCode = 0 // Standard query + OpCodeInverseQuery OpCode = 1 // Inverse query + OpCodeStatus OpCode = 2 // Server status request +) + +// An RCode is a DNS response status code. +type RCode uint16 + +const ( + // No error condition. + RCodeSuccess RCode = 0 // success + // Format error - The name server was unable to interpret the query. + RCodeFormatError RCode = 1 // format error + // Server failure - The name server was unable to process this query due to a problem with the name server. + RCodeServerFailure RCode = 2 // server failure + // Name Error - Meaningful only for responses from an authoritative name server, this code signifies that the domain name referenced in the query does not exist. + RCodeNameError RCode = 3 // name error + // Not implemented - The name server does not support the requested kind of query. + RCodeNotImplemented RCode = 4 // not implemented + // Refused - The name server refuses to perform the specified operation for policy reasons. For example, a name server may not wish to provide the information to the particular requester, or a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data. + RCodeRefused RCode = 5 // refused +) + +func b2u8(b bool) uint8 { + if b { + return 1 + } + return 0 +} diff --git a/lneto/dns/dns.go b/lneto/dns/dns.go new file mode 100644 index 0000000..9226244 --- /dev/null +++ b/lneto/dns/dns.go @@ -0,0 +1,566 @@ +package dns + +import ( + "bytes" + "encoding/binary" + "math" + "slices" + "strconv" + "strings" +) + +// Global parameters. +const ( + // SizeHeader is the length (in bytes) of a DNS header. + // A header is comprised of 6 uint16s and no padding. + SizeHeader = 6 * 2 + // The Internet supports name server access using TCP [RFC-9293] on server + // port 53 (decimal) as well as datagram access using UDP [RFC-768] on UDP port 53 (decimal). + ServerPort = 53 + ClientPort = 53 + // Messages carried by UDP are restricted to 512 bytes (not counting the IP + // or UDP headers). Longer messages are truncated and the TC bit is set in the header. + MaxSizeUDP = 512 +) + +type Message struct { + Questions []Question + Answers []Resource + Authorities []Resource + Additionals []Resource +} + +type Question struct { + Name Name + Type Type + Class Class +} + +type Resource struct { + Header ResourceHeader + data []byte +} + +// A ResourceHeader is the header of a DNS resource record. There are +// many types of DNS resource records, but they all share the same header. +type ResourceHeader struct { + Name Name + Type Type + Class Class + TTL uint32 + Length uint16 +} + +type Name struct { + data []byte +} + +// Decode decodes the DNS message in b into m. It returns the number of bytes +// consumed from b (0 if no bytes were consumed) and any error encountered. +// If the message was not completely parsed due to LimitResourceDecoding, +// incompleteButOK is true and an error is returned, though the message is still usable. +func (m *Message) Decode(msg []byte) (_ uint16, incompleteButOK bool, err error) { + if len(msg) < SizeHeader { + return 0, false, errBaseLen + } else if len(msg) > math.MaxUint16 { + return 0, false, errResTooLong + } + m.Reset() + hdr := NewFrame(msg) + nq := int(hdr.QDCount()) + off := uint16(SizeHeader) + // Return tooManyErr if found to flag to the caller that the message was + // decoded but contained too many resources to decode completely. + + var tooManyErr error + switch { + case nq > cap(m.Questions): + tooManyErr = errTooManyQuestions + case hdr.ANCount() > uint16(cap(m.Answers)): + tooManyErr = errTooManyAnswers + case hdr.NSCount() > uint16(cap(m.Authorities)): + tooManyErr = errTooManyAuthorities + case hdr.ARCount() > uint16(cap(m.Additionals)): + tooManyErr = errTooManyAdditionals + } + if nq > cap(m.Questions) { + nq = cap(m.Questions) + } + m.Questions = m.Questions[:nq] + for i := 0; i < nq; i++ { + off, err = m.Questions[i].Decode(msg, off) + if err != nil { + m.Questions = m.Questions[:i] // Trim non-decoded/failed questions. + return off, false, err + } + } + // Skip undecoded questions. + for i := 0; i < int(hdr.QDCount())-nq; i++ { + off, err = skipQuestion(msg, off) + if err != nil { + return off, false, err + } + } + + off, err = decodeToCapResources(&m.Answers, msg, hdr.ANCount(), off) + if err != nil { + return off, false, err + } + off, err = decodeToCapResources(&m.Authorities, msg, hdr.NSCount(), off) + if err != nil { + return off, false, err + } + off, err = decodeToCapResources(&m.Additionals, msg, hdr.ARCount(), off) + if err != nil { + return off, false, err + } + return off, tooManyErr != nil, tooManyErr +} + +func decodeToCapResources(dst *[]Resource, msg []byte, nrec, off uint16) (_ uint16, err error) { + originalRec := nrec + if nrec > uint16(cap(*dst)) { + nrec = uint16(cap(*dst)) // Decode up to cap. Caller will return an error flag. + } + *dst = (*dst)[:nrec] + for i := uint16(0); i < nrec; i++ { + off, err = (*dst)[i].Decode(msg, off) + if err != nil { + *dst = (*dst)[:i] // Trim non-decoded/failed resources. + return off, err + } + } + // Parse undecoded resources, effectively skipping them. + for i := uint16(0); i < originalRec-nrec; i++ { + off, err = skipResource(msg, off) + if err != nil { + return off, err + } + } + return off, nil +} + +func skipQuestion(msg []byte, off uint16) (_ uint16, err error) { + off, err = skipName(msg, off) + if err != nil { + return off, err + } + if off+4 > uint16(len(msg)) { + return off, errBaseLen + } + return off + 4, nil +} + +func skipResource(msg []byte, off uint16) (_ uint16, err error) { + off, err = skipName(msg, off) + if err != nil { + return off, err + } + // | Name... | Type16 | Class16 | TTL32 | Length16 | Data... | + datalen := binary.BigEndian.Uint16(msg[off+8:]) + off += datalen + 10 + if off > uint16(len(msg)) { + return off, errBaseLen + } + return off, nil +} + +func skipName(msg []byte, off uint16) (uint16, error) { + return visitAllLabels(msg, off, func(b []byte) {}, allowCompression) +} + +func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte, err error) { + nq := uint16(len(m.Questions)) + nans := uint16(len(m.Answers)) + nauth := uint16(len(m.Authorities)) + nadd := uint16(len(m.Additionals)) + var hdr [SizeHeader]byte + f := NewFrame(hdr[:]) + f.SetTxID(txid) + f.SetFlags(flags) + f.SetQDCount(nq) + f.SetANCount(nans) + f.SetNSCount(nauth) + f.SetARCount(nadd) + + buf = slices.Grow(buf, int(m.Len())) + buf = append(buf, hdr[:]...) + for _, q := range m.Questions { + buf, err = q.appendTo(buf) + if err != nil { + return buf, err + } + } + for _, r := range m.Answers { + buf, err = r.appendTo(buf) + if err != nil { + return buf, err + } + } + for _, r := range m.Authorities { + buf, err = r.appendTo(buf) + if err != nil { + return buf, err + } + } + for _, r := range m.Additionals { + buf, err = r.appendTo(buf) + if err != nil { + return buf, err + } + } + return buf, nil +} + +func (m *Message) Len() uint16 { + return SizeHeader + m.lenResources() +} + +func (m *Message) lenResources() (l uint16) { + for i := range m.Questions { + l += m.Questions[i].Len() + } + for i := range m.Answers { + l += m.Answers[i].Len() + } + for i := range m.Authorities { + l += m.Authorities[i].Len() + } + for i := range m.Additionals { + l += m.Additionals[i].Len() + } + return l +} + +func (m *Message) AddQuestions(questions []Question) { + // This question slice handling here is done in spirit of DNSClient being owner of its own buffer. + // If this is not done we risk the Questions being edited by user and interfering with the DNS request. + qoff := len(m.Questions) + m.Questions = slices.Grow(m.Questions, len(questions)) + m.Questions = m.Questions[:qoff+len(questions)] + for i := range questions { + m.Questions[qoff+i].Name.CloneFrom(questions[i].Name) + m.Questions[qoff+i].Type = questions[i].Type + m.Questions[qoff+i].Class = questions[i].Class + } +} + +func (m *Message) LimitResourceDecoding(maxQ, maxAns, maxAuth, maxAdd uint16) { + m.Questions = slices.Grow(m.Questions, int(maxQ)) + m.Answers = slices.Grow(m.Answers, int(maxQ)) + m.Authorities = slices.Grow(m.Authorities, int(maxQ)) + m.Additionals = slices.Grow(m.Additionals, int(maxQ)) +} + +func (m *Message) Reset() { + m.Questions = m.Questions[:0] + m.Answers = m.Answers[:0] + m.Authorities = m.Authorities[:0] + m.Additionals = m.Additionals[:0] +} + +// String returns a string representation of the header. +func (h *ResourceHeader) String() string { + return h.Name.String() + " " + h.Type.String() + " " + h.Class.String() + + " ttl=" + strconv.FormatUint(uint64(h.TTL), 10) + " len=" + strconv.FormatUint(uint64(h.Length), 10) +} + +func (r *Resource) Reset() { + r.Header.Reset() + r.data = r.data[:0] +} + +func (r *Resource) Len() uint16 { + return r.Header.Name.Len() + 10 + uint16(len(r.data)) +} + +func (r *Resource) RawData() []byte { + length := r.Header.Length + if int(length) > len(r.data) { + length = uint16(len(r.data)) + } + return r.data[:length] +} + +func (q *Question) Reset() { + q.Name.Reset() + *q = Question{Name: q.Name} // Reuse Name's buffer. +} + +// Len returns Question's length over-the-wire. +func (q *Question) Len() uint16 { return q.Name.Len() + 4 } + +func (r *ResourceHeader) Reset() { + r.Name.Reset() + *r = ResourceHeader{Name: r.Name} // Reuse Name's buffer. +} + +func (q *Question) Decode(msg []byte, off uint16) (uint16, error) { + off, err := q.Name.Decode(msg, off) + if err != nil { + return off, err + } + if off+4 > uint16(len(msg)) { + return off, errResourceLen + } + q.Type = Type(binary.BigEndian.Uint16(msg[off:])) + q.Class = Class(binary.BigEndian.Uint16(msg[off+2:])) + return off + 4, nil +} + +func (q *Question) appendTo(buf []byte) (_ []byte, err error) { + buf, err = q.Name.AppendTo(buf) + if err != nil { + return buf, err + } + buf = append16(buf, uint16(q.Type)) + buf = append16(buf, uint16(q.Class)) + return buf, nil +} + +// String returns a string representation of the Question with the Name in dotted format. +func (q *Question) String() string { + return q.Name.String() + " " + q.Type.String() + " " + q.Class.String() +} + +func (r *Resource) Decode(b []byte, off uint16) (uint16, error) { + off, err := r.Header.Decode(b, off) + if err != nil { + return off, err + } + if r.Header.Length > uint16(len(b[off:])) { + return off, errResourceLen + } + r.data = append(r.data[:0], b[off:off+r.Header.Length]...) + return off + r.Header.Length, nil +} + +func (r *Resource) appendTo(buf []byte) (_ []byte, err error) { + r.Header.Length = uint16(len(r.data)) + buf, err = r.Header.appendTo(buf) + if err != nil { + return buf, err + } + buf = append(buf, r.data...) + return buf, nil +} + +func (rhdr *ResourceHeader) Decode(msg []byte, off uint16) (uint16, error) { + off, err := rhdr.Name.Decode(msg, off) + if err != nil { + return off, err + } + if off+10 > uint16(len(msg)) { + return off, errResourceLen + } + rhdr.Type = Type(binary.BigEndian.Uint16(msg[off:])) // 2 + rhdr.Class = Class(binary.BigEndian.Uint16(msg[off+2:])) // 4 + rhdr.TTL = binary.BigEndian.Uint32(msg[off+4:]) // 8 + rhdr.Length = binary.BigEndian.Uint16(msg[off+8:]) // 10 + return off + 10, nil +} + +func (rhdr *ResourceHeader) appendTo(buf []byte) (_ []byte, err error) { + buf, err = rhdr.Name.AppendTo(buf) + if err != nil { + return buf, err + } + buf = append16(buf, uint16(rhdr.Type)) + buf = append16(buf, uint16(rhdr.Class)) + buf = append32(buf, rhdr.TTL) + buf = append16(buf, rhdr.Length) + return buf, nil +} + +func MustNewName(s string) Name { + name, err := NewName(s) + if err != nil { + panic(err) + } + return name +} + +// NewName parses a domain name and returns a new Name. +func NewName(domain string) (Name, error) { + if len(domain) == 1 && domain[0] == '.' { + return Name{data: []byte{0}}, nil + } + var name Name + for len(domain) > 0 { + idx := strings.IndexByte(domain, '.') + done := idx < 0 || idx+1 > len(domain) + if done { + idx = len(domain) + } + if !name.CanAddLabel(domain[:idx]) { + return Name{}, errCantAddLabel + } + name.AddLabel(domain[:idx]) + if done { + break + } + domain = domain[idx+1:] + } + return name, nil +} + +// Len returns the length over-the-wire of the encoded Name. +func (n *Name) Len() uint16 { + return uint16(len(n.data)) +} + +func (n *Name) CloneFrom(ex Name) { + n.data = append(n.data[:0], ex.data...) +} + +// AppendTo appends the Name to b in wire format and returns the resulting slice. +func (n *Name) AppendTo(b []byte) ([]byte, error) { + if len(n.data) == 0 { + return b, errInvalidName + } + return append(b, n.data...), nil +} + +// String returns a string representation of the name in dotted format. +func (n *Name) String() string { + b := make([]byte, 0, len(n.data)+3) + return string(n.AppendDottedTo(b)) +} + +// AppendDottedTo appends the Name to b in dotted format and returns the resulting slice. +func (n *Name) AppendDottedTo(b []byte) []byte { + n.VisitLabels(func(label []byte) { + b = append(b, label...) + b = append(b, '.') + }) + return b +} + +// Decode resets internal Name buffer and reads raw wire data from buffer, returning any error encountered. +func (n *Name) Decode(b []byte, off uint16) (uint16, error) { + n.Reset() + off, err := visitAllLabels(b, off, n.vistAddLabel, allowCompression) + if err != nil { + n.Reset() + return off, err + } + n.data = append(n.data, 0) // Add terminator, off counts the terminator already in visitAllLabels. + return off, nil +} + +// Reset resets the Name labels to be empty and reuses buffer. +func (n *Name) Reset() { n.data = n.data[:0] } + +// CanAddLabel reports whether the label can be added to the name. +func (n *Name) CanAddLabel(label string) bool { + return len(label) != 0 && len(label) <= 63 && len(label)+len(n.data)+2 <= 255 && // Include len+terminator+label. + label[len(label)-1] != 0 && // We do not support implicitly zero-terminated labels. + strings.IndexByte(label, '.') < 0 // See issue golang/go#56246 +} + +// AddLabel adds a label to the name. If n.CanAddLabel(label) returns false, it panics. +func (n *Name) AddLabel(label string) { + if !n.CanAddLabel(label) { + panic(errCantAddLabel.Error()) + } + if n.isTerminated() { + n.data = n.data[:len(n.data)-1] // Remove terminator if present to add another label. + } + n.data = append(n.data, byte(len(label))) + n.data = append(n.data, label...) + n.data = append(n.data, 0) +} + +func (n *Name) vistAddLabel(label []byte) { + n.data = append(n.data, byte(len(label))) + n.data = append(n.data, label...) +} + +func (n *Name) isTerminated() bool { + return len(n.data) > 0 && n.data[len(n.data)-1] == 0 +} + +func (n *Name) VisitLabels(fn func(label []byte)) error { + if len(n.data) > 255 { + return errNameTooLong + } + _, err := visitAllLabels(n.data, 0, fn, allowCompression) + return err +} + +func append16(b []byte, v uint16) []byte { + binary.BigEndian.PutUint16(b[len(b):len(b)+2], v) + return b[:len(b)+2] +} + +func append32(b []byte, v uint32) []byte { + binary.BigEndian.PutUint32(b[len(b):len(b)+4], v) + return b[:len(b)+4] +} + +func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression bool) (uint16, error) { + // currOff is the current working offset. + currOff := off + if len(msg) > math.MaxUint16 { + return off, errResTooLong + } + // ptr is the number of pointers followed. + var ptr uint8 + // newOff is the offset where the next record will start. Pointers lead + // to data that belongs to other names and thus doesn't count towards to + // the usage of this name. + var newOff = off + +LOOP: + for { + if currOff >= uint16(len(msg)) { + return off, errBaseLen + } + c := uint16(msg[currOff]) + currOff++ + switch c & 0xc0 { + case 0x00: // String label (segment). + if c == 0x00 { + break LOOP // Nominal end of name, always ends with null terminator. + } + endOff := currOff + c + if endOff > uint16(len(msg)) { + return off, errCalcLen + } + + // Reject names containing dots. See issue golang/go#56246 + if bytes.IndexByte(msg[currOff:endOff], '.') >= 0 { + return off, errInvalidName + } + + fn(msg[currOff:endOff]) + currOff = endOff + + case 0xc0: // Pointer. + // https://cs.opensource.google/go/x/net/+/refs/tags/v0.19.0:dns/dnsmessage/message.go;l=2078 + if !allowCompression { + return off, errCompressedSRV + } + if currOff >= uint16(len(msg)) { + return off, errInvalidPtr + } + c1 := msg[currOff] + currOff++ + if ptr == 0 { + newOff = currOff + } + // Don't follow too many pointers, maybe there's a loop. + if ptr++; ptr > 10 { + return off, errTooManyPtr + } + currOff = (c^0xC0)<<8 | uint16(c1) + default: + // Prefixes 0x80 and 0x40 are reserved. + return off, errReserved + } + } + if ptr == 0 { + newOff = currOff + } + return newOff, nil +} diff --git a/lneto/dns/dns_test.go b/lneto/dns/dns_test.go new file mode 100644 index 0000000..3ed4a82 --- /dev/null +++ b/lneto/dns/dns_test.go @@ -0,0 +1,223 @@ +package dns + +import ( + "fmt" + "strings" + "testing" +) + +var defaultMessageFlags = NewClientHeaderFlags(OpCodeQuery, true) + +func TestNameString(t *testing.T) { + var name Name + domain := "foo.bar.org" + domainSplit := strings.Split(domain, ".") + for i, label := range domainSplit { + name.AddLabel(label) + s := name.String() + if s != strings.Join(domainSplit[:i+1], ".")+"." { + t.Fatalf("unexpected name string %q", s) + } + } +} + +func TestNameAppendDecode(t *testing.T) { + const domain = "foo.bar.org" + name, err := NewName(domain) + if err != nil { + t.Fatal(err) + } else if name.String() != domain+"." { + t.Fatalf("unexpected name string %q", name.String()) + } + var buf [512]byte + b, err := name.AppendTo(buf[:0]) + if err != nil { + t.Fatal(err) + } + if uint16(len(b)) != name.Len() { + t.Fatalf("unexpected name length %d", len(b)) + } + if b[len(b)-1] != 0 { + t.Fatalf("unexpected name terminator byte after construction: %q", b[len(b)-1]) + } + + var name2 Name + n, err := name2.Decode(b, 0) + if err != nil { + t.Fatal(err) + } + if n != name.Len() { + t.Errorf("unexpected name parsed length %q (%d), want %q (%d)", name.data, n, b, name.Len()) + } + if name2.String() != name.String() { + t.Errorf("unexpected name string %q, want %q", name2.String(), name.String()) + } + + // Re-decode. + const okvalidName = "\x03www\x02go\x03dev\x00" + _, err = name.Decode([]byte(okvalidName), 0) + if err != nil { + t.Error("got error decoding valid name", err) + } else if name.String() != "www.go.dev." { + t.Error("unexpected name string", name.String()) + } + b, err = name.AppendTo(buf[:0]) + if err != nil { + t.Fatal(err) + } + if b[len(b)-1] != 0 { + t.Fatalf("unexpected name terminator byte after decoding: %q", b[len(b)-1]) + } + if string(b) != okvalidName { + t.Errorf("unexpected name bytes after decode %q, want %q", b, okvalidName) + } + // Decode invalid name. + const invalidName = "\x03w.w\x02go\x03dev\x00" + _, err = name.Decode([]byte(invalidName), 0) + if err == nil { + t.Error("expected error for invalid name") + } else if err != errInvalidName { + t.Errorf("unexpected error %v, want %v", err, errInvalidName) + } +} + +func TestMessageAppendEncode(t *testing.T) { + var tests = []struct { + Message Message + error error + }{ + { + Message: Message{ + Questions: []Question{ + { + Name: MustNewName("."), + Type: TypeA, + Class: ClassINET, + }, + }, + Answers: []Resource{ + { + Header: ResourceHeader{ + Name: MustNewName("."), + Type: TypeA, + Class: ClassINET, + TTL: 256, + Length: 3, + }, + data: []byte{1, 2, 3}, + }, + }, + }, + }, + } + var buf [512]byte + for _, tt := range tests { + b, err := tt.Message.AppendTo(buf[:0], 123, defaultMessageFlags) + if err != nil { + t.Fatal(err) + } + + var msg Message + msg.LimitResourceDecoding(uint16(len(tt.Message.Questions)), uint16(len(tt.Message.Answers)), uint16(len(tt.Message.Authorities)), uint16(len(tt.Message.Additionals))) + _, incomplete, err := msg.Decode(b) + if err != nil { + t.Fatal(err) + } else if incomplete { + t.Fatal("incomplete parse") + } + if msg.String() != tt.Message.String() { + t.Errorf("mismatch message strings after append/decode:\n%s\n%s", tt.Message.String(), msg.String()) + } + } +} + +func TestMessageAppendEncodeIncompleteOK(t *testing.T) { + var tests = []struct { + Message Message + error error + }{ + { + Message: Message{ + Questions: []Question{ + { + Name: MustNewName("."), + Type: TypeA, + Class: ClassINET, + }, + }, + Answers: []Resource{ + { + Header: ResourceHeader{ + Name: MustNewName("."), + Type: TypeA, + Class: ClassINET, + TTL: 256, + Length: 3, + }, + data: []byte{1, 2, 3}, + }, + { + Header: ResourceHeader{ + Name: MustNewName("."), + Type: TypeA, + Class: ClassINET, + TTL: 256, + Length: 3, + }, + data: []byte{1, 2, 3}, + }, + }, + }, + }, + } + var buf [512]byte + for _, tt := range tests { + b, err := tt.Message.AppendTo(buf[:0], 123, defaultMessageFlags) + if err != nil { + t.Fatal(err) + } + + var msg Message + msg.LimitResourceDecoding(uint16(len(tt.Message.Questions)), uint16(len(tt.Message.Answers)), uint16(len(tt.Message.Authorities)), uint16(len(tt.Message.Additionals))) + _, incomplete, err := msg.Decode(b) + if err != nil && !incomplete { + t.Fatal(err) + } else if !incomplete { + t.Fatal("expected incomplete parse") + } + tt.Message.Answers = tt.Message.Answers[:1] // Trim off the last answer that was not parsed. + if msg.String() != tt.Message.String() { + t.Errorf("mismatch message strings after append/decode:\n%s\n%s", tt.Message.String(), msg.String()) + } + } +} + +func (m *Message) String() string { + // s := fmt.Sprintf("Message: %#v\n", &m.Header) + var s string + if len(m.Questions) > 0 { + s += "-- Questions\n" + for _, q := range m.Questions { + s += fmt.Sprintf("%#v\n", q) + } + } + if len(m.Answers) > 0 { + s += "-- Answers\n" + for _, a := range m.Answers { + s += fmt.Sprintf("%#v\n", a) + } + } + if len(m.Authorities) > 0 { + s += "-- Authorities\n" + for _, ns := range m.Authorities { + s += fmt.Sprintf("%#v\n", ns) + } + } + if len(m.Additionals) > 0 { + s += "-- Additionals\n" + for _, e := range m.Additionals { + s += fmt.Sprintf("%#v\n", e) + } + } + return s +} diff --git a/lneto/dns/stringers.go b/lneto/dns/stringers.go new file mode 100644 index 0000000..68890a2 --- /dev/null +++ b/lneto/dns/stringers.go @@ -0,0 +1,141 @@ +// Code generated by "stringer -type=Type,Class,RCode,OpCode -linecomment -output stringers.go ."; DO NOT EDIT. + +package dns + +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[TypeA-1] + _ = x[TypeNS-2] + _ = x[TypeCNAME-5] + _ = x[TypeSOA-6] + _ = x[TypePTR-12] + _ = x[TypeMX-15] + _ = x[TypeTXT-16] + _ = x[TypeAAAA-28] + _ = x[TypeSRV-33] + _ = x[TypeOPT-41] + _ = x[TypeWKS-11] + _ = x[TypeHINFO-13] + _ = x[TypeMINFO-14] + _ = x[TypeAXFR-252] + _ = x[TypeALL-255] +} + +const ( + _Type_name_0 = "ANS" + _Type_name_1 = "CNAMESOA" + _Type_name_2 = "WKSPTRHINFOMINFOMXTXT" + _Type_name_3 = "AAAA" + _Type_name_4 = "SRV" + _Type_name_5 = "OPT" + _Type_name_6 = "AXFR" + _Type_name_7 = "ALL" +) + +var ( + _Type_index_0 = [...]uint8{0, 1, 3} + _Type_index_1 = [...]uint8{0, 5, 8} + _Type_index_2 = [...]uint8{0, 3, 6, 11, 16, 18, 21} +) + +func (i Type) String() string { + switch { + case 1 <= i && i <= 2: + i -= 1 + return _Type_name_0[_Type_index_0[i]:_Type_index_0[i+1]] + case 5 <= i && i <= 6: + i -= 5 + return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]] + case 11 <= i && i <= 16: + i -= 11 + return _Type_name_2[_Type_index_2[i]:_Type_index_2[i+1]] + case i == 28: + return _Type_name_3 + case i == 33: + return _Type_name_4 + case i == 41: + return _Type_name_5 + case i == 252: + return _Type_name_6 + case i == 255: + return _Type_name_7 + default: + return "Type(" + 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[ClassINET-1] + _ = x[ClassCSNET-2] + _ = x[ClassCHAOS-3] + _ = x[ClassHESIOD-4] + _ = x[ClassANY-255] +} + +const ( + _Class_name_0 = "INETCSNETCHAOSHESIOD" + _Class_name_1 = "ANY" +) + +var ( + _Class_index_0 = [...]uint8{0, 4, 9, 14, 20} +) + +func (i Class) String() string { + switch { + case 1 <= i && i <= 4: + i -= 1 + return _Class_name_0[_Class_index_0[i]:_Class_index_0[i+1]] + case i == 255: + return _Class_name_1 + default: + return "Class(" + 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[RCodeSuccess-0] + _ = x[RCodeFormatError-1] + _ = x[RCodeServerFailure-2] + _ = x[RCodeNameError-3] + _ = x[RCodeNotImplemented-4] + _ = x[RCodeRefused-5] +} + +const _RCode_name = "successformat errorserver failurename errornot implementedrefused" + +var _RCode_index = [...]uint8{0, 7, 19, 33, 43, 58, 65} + +func (i RCode) String() string { + if i >= RCode(len(_RCode_index)-1) { + return "RCode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _RCode_name[_RCode_index[i]:_RCode_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[OpCodeQuery-0] + _ = x[OpCodeInverseQuery-1] + _ = x[OpCodeStatus-2] +} + +const _OpCode_name = "Standard queryInverse queryServer status request" + +var _OpCode_index = [...]uint8{0, 14, 27, 48} + +func (i OpCode) String() string { + if i >= OpCode(len(_OpCode_index)-1) { + return "OpCode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _OpCode_name[_OpCode_index[i]:_OpCode_index[i+1]] +} diff --git a/lneto/ntp/ntp_test.go b/lneto/ntp/ntp_test.go new file mode 100644 index 0000000..fbe9f9c --- /dev/null +++ b/lneto/ntp/ntp_test.go @@ -0,0 +1,60 @@ +package ntp + +import ( + "encoding/binary" + "testing" + "time" +) + +func TestTimestamp(t *testing.T) { + now := time.Now() + nowp1 := now.Add(time.Second) + t1, err := TimestampFromTime(now) + if err != nil { + t.Fatal(err) + } + t2, _ := TimestampFromTime(nowp1) + gotnow := t1.Time() + if gotnow.Sub(now) > time.Microsecond { + t.Fatalf("got %v, want %v", gotnow, now) + } + + d := t2.Sub(t1) + if d != time.Second { + t.Fatalf("expected 1s, got %s", d) + } + d = t1.Sub(t2) + if d != -time.Second { + t.Fatalf("expected -1s, got %s", d) + } + + var b [8]byte + t1.Put(b[:]) + readback := binary.BigEndian.Uint64(b[:]) + t1got := TimestampFromUint64(readback) + if t1got != t1 { + t.Fatalf("got %v, want %v", t1got, t1) + } +} + +func TestTimestampOverflow(t *testing.T) { + const tol = time.Microsecond + var now = time.Now() // time.Date(2035, 2, 7, 6, 28, 16, 0, time.UTC) + told, err := TimestampFromTime(baseTime) + if err != nil { + t.Fatal(err) + } + tmodern, err := TimestampFromTime(now) + if err != nil { + t.Fatal(err) + } + diff := tmodern.Sub(told) + if diff < 0 { + t.Fatalf("got %v, want positive", diff) + } + diffwant := now.Sub(baseTime) + differr := (diff - diffwant).Abs() + if differr > tol { + t.Fatalf("got diff %v", differr) + } +}