mirror of
https://github.com/soypat/lneto.git
synced 2026-08-24 08:29:08 +00:00
restructure package, rename module/repo
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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")
|
||||
)
|
||||
|
||||
// Frame encapsulates the raw data of a DNS packet
|
||||
// and provides methods for manipulating, validating and
|
||||
// retrieving fields and payload data. See [RFC1035].
|
||||
//
|
||||
// [RFC1035]: https://tools.ietf.org/html/rfc1035
|
||||
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)
|
||||
}
|
||||
|
||||
// ClearHeader zeros out the fixed(non-variable) header contents.
|
||||
func (frm Frame) ClearHeader() {
|
||||
for i := range frm.buf[:SizeHeader] {
|
||||
frm.buf[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
+566
@@ -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
|
||||
}
|
||||
+223
@@ -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
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
Reference in New Issue
Block a user