diff --git a/disco/udp/stun.go b/disco/udp/stun.go index 8307e2f..69bcbce 100644 --- a/disco/udp/stun.go +++ b/disco/udp/stun.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "tailscale.com/net/stun" + "github.com/sigcn/pg/stun" ) type stunRoundTripper struct { diff --git a/disco/udp/udp.go b/disco/udp/udp.go index 2dc3b08..fa75f29 100644 --- a/disco/udp/udp.go +++ b/disco/udp/udp.go @@ -17,8 +17,8 @@ import ( "github.com/sigcn/pg/cache" "github.com/sigcn/pg/disco" + "github.com/sigcn/pg/stun" "golang.org/x/time/rate" - "tailscale.com/net/stun" ) var ( diff --git a/go.mod b/go.mod index ded23fb..945e55e 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,23 @@ module github.com/sigcn/pg -go 1.23.4 +go 1.24 require ( - github.com/coreos/go-oidc/v3 v3.11.0 + github.com/coreos/go-oidc/v3 v3.12.0 github.com/gorilla/websocket v1.5.3 - github.com/jedib0t/go-pretty/v6 v6.6.3 + github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/mdp/qrterminal/v3 v3.2.0 github.com/vishvananda/netlink v1.3.0 - golang.org/x/crypto v0.28.0 - golang.org/x/net v0.30.0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/sys v0.27.0 - golang.org/x/time v0.7.0 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.37.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sys v0.31.0 + golang.org/x/time v0.11.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/windows v0.5.3 gopkg.in/yaml.v3 v3.0.1 - gvisor.dev/gvisor v0.0.0-20241218235220-7bf5820dea8f + gvisor.dev/gvisor v0.0.0-20250307022919-35e47cb01460 storj.io/common v0.0.0-20240425113201-9815a85cbc32 - tailscale.com v1.56.1 ) require ( @@ -27,7 +26,8 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/vishvananda/netns v0.0.4 // indirect - golang.org/x/term v0.26.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index aa1ffe3..de8076d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= -github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= +github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= @@ -10,8 +10,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jedib0t/go-pretty/v6 v6.6.3 h1:nGqgS0tgIO1Hto47HSaaK4ac/I/Bu7usmdD3qvs0WvM= -github.com/jedib0t/go-pretty/v6 v6.6.3/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= +github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= @@ -21,26 +21,28 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= @@ -51,11 +53,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20241218235220-7bf5820dea8f h1:u7Zpe3o5/MYKtMVvj8+bsEya99PYufLjqfdmXzJg7N8= -gvisor.dev/gvisor v0.0.0-20241218235220-7bf5820dea8f/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= +gvisor.dev/gvisor v0.0.0-20250307022919-35e47cb01460 h1:nTgsrWccO1oBUcZMjQQDLfbJyluzbrA+orPvDLu2Y+g= +gvisor.dev/gvisor v0.0.0-20250307022919-35e47cb01460/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= storj.io/common v0.0.0-20240425113201-9815a85cbc32 h1:Py/vvWBasKRGG8pO1M89idDEW3LWdU5o/w83xwUNNgU= storj.io/common v0.0.0-20240425113201-9815a85cbc32/go.mod h1:MFl009RHY4tIqySVNy/6EmgRw2q60d26h9N/nb7JxGU= -tailscale.com v1.56.1 h1:V3HBDJai3u7xo22Xlv7ioqKNZQdxOJebLYCNqCXVwZg= -tailscale.com v1.56.1/go.mod h1:XQk6fCN8oMJ+qbCmW+2WS/VM3jTA9nIHT6O19t0hZeQ= diff --git a/stun/stun.go b/stun/stun.go new file mode 100644 index 0000000..d882a4e --- /dev/null +++ b/stun/stun.go @@ -0,0 +1,313 @@ +// Copyright (c) sigcn/pg +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package STUN generates STUN request packets and parses response packets. +package stun + +import ( + "bytes" + crand "crypto/rand" + "encoding/binary" + "errors" + "hash/crc32" + "net" + "net/netip" +) + +const ( + attrNumSoftware = 0x8022 + attrNumFingerprint = 0x8028 + attrMappedAddress = 0x0001 + attrXorMappedAddress = 0x0020 + // This alternative attribute type is not + // mentioned in the RFC, but the shift into + // the "comprehension-optional" range seems + // like an easy mistake for a server to make. + // And servers appear to send it. + attrXorMappedAddressAlt = 0x8020 + + software = "tailnode" // notably: 8 bytes long, so no padding + bindingRequest = "\x00\x01" + magicCookie = "\x21\x12\xa4\x42" + lenFingerprint = 8 // 2+byte header + 2-byte length + 4-byte crc32 + headerLen = 20 +) + +// TxID is a transaction ID. +type TxID [12]byte + +// NewTxID returns a new random TxID. +func NewTxID() TxID { + var tx TxID + if _, err := crand.Read(tx[:]); err != nil { + panic(err) + } + return tx +} + +// Request generates a binding request STUN packet. +// The transaction ID, tID, should be a random sequence of bytes. +func Request(tID TxID) []byte { + // STUN header, RFC5389 Section 6. + const lenAttrSoftware = 4 + len(software) + b := make([]byte, 0, headerLen+lenAttrSoftware+lenFingerprint) + b = append(b, bindingRequest...) + b = appendU16(b, uint16(lenAttrSoftware+lenFingerprint)) // number of bytes following header + b = append(b, magicCookie...) + b = append(b, tID[:]...) + + // Attribute SOFTWARE, RFC5389 Section 15.5. + b = appendU16(b, attrNumSoftware) + b = appendU16(b, uint16(len(software))) + b = append(b, software...) + + // Attribute FINGERPRINT, RFC5389 Section 15.5. + fp := fingerPrint(b) + b = appendU16(b, attrNumFingerprint) + b = appendU16(b, 4) + b = appendU32(b, fp) + + return b +} + +func fingerPrint(b []byte) uint32 { return crc32.ChecksumIEEE(b) ^ 0x5354554e } + +func appendU16(b []byte, v uint16) []byte { + return append(b, byte(v>>8), byte(v)) +} + +func appendU32(b []byte, v uint32) []byte { + return append(b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} + +// ParseBindingRequest parses a STUN binding request. +// +// It returns an error unless it advertises that it came from +// Tailscale. +func ParseBindingRequest(b []byte) (TxID, error) { + if !Is(b) { + return TxID{}, ErrNotSTUN + } + if string(b[:len(bindingRequest)]) != bindingRequest { + return TxID{}, ErrNotBindingRequest + } + var txID TxID + copy(txID[:], b[8:8+len(txID)]) + var softwareOK bool + var lastAttr uint16 + var gotFP uint32 + if err := foreachAttr(b[headerLen:], func(attrType uint16, a []byte) error { + lastAttr = attrType + if attrType == attrNumSoftware && string(a) == software { + softwareOK = true + } + if attrType == attrNumFingerprint && len(a) == 4 { + gotFP = binary.BigEndian.Uint32(a) + } + return nil + }); err != nil { + return TxID{}, err + } + if !softwareOK { + return TxID{}, ErrWrongSoftware + } + if lastAttr != attrNumFingerprint { + return TxID{}, ErrNoFingerprint + } + wantFP := fingerPrint(b[:len(b)-lenFingerprint]) + if gotFP != wantFP { + return TxID{}, ErrWrongFingerprint + } + return txID, nil +} + +var ( + ErrNotSTUN = errors.New("response is not a STUN packet") + ErrNotSuccessResponse = errors.New("STUN packet is not a response") + ErrMalformedAttrs = errors.New("STUN response has malformed attributes") + ErrNotBindingRequest = errors.New("STUN request not a binding request") + ErrWrongSoftware = errors.New("STUN request came from non-Tailscale software") + ErrNoFingerprint = errors.New("STUN request didn't end in fingerprint") + ErrWrongFingerprint = errors.New("STUN request had bogus fingerprint") +) + +func foreachAttr(b []byte, fn func(attrType uint16, a []byte) error) error { + for len(b) > 0 { + if len(b) < 4 { + return ErrMalformedAttrs + } + attrType := binary.BigEndian.Uint16(b[:2]) + attrLen := int(binary.BigEndian.Uint16(b[2:4])) + attrLenWithPad := (attrLen + 3) &^ 3 + b = b[4:] + if attrLenWithPad > len(b) { + return ErrMalformedAttrs + } + if err := fn(attrType, b[:attrLen]); err != nil { + return err + } + b = b[attrLenWithPad:] + } + return nil +} + +// Response generates a binding response. +func Response(txID TxID, addrPort netip.AddrPort) []byte { + addr := addrPort.Addr() + + var fam byte + if addr.Is4() { + fam = 1 + } else if addr.Is6() { + fam = 2 + } else { + return nil + } + attrsLen := 8 + addr.BitLen()/8 + b := make([]byte, 0, headerLen+attrsLen) + + // Header + b = append(b, 0x01, 0x01) // success + b = appendU16(b, uint16(attrsLen)) + b = append(b, magicCookie...) + b = append(b, txID[:]...) + + // Attributes (well, one) + b = appendU16(b, attrXorMappedAddress) + b = appendU16(b, uint16(4+addr.BitLen()/8)) + b = append(b, + 0, // unused byte + fam) + b = appendU16(b, addrPort.Port()^0x2112) // first half of magicCookie + ipa := addr.As16() + for i, o := range ipa[16-addr.BitLen()/8:] { + if i < 4 { + b = append(b, o^magicCookie[i]) + } else { + b = append(b, o^txID[i-len(magicCookie)]) + } + } + return b +} + +// ParseResponse parses a successful binding response STUN packet. +// The IP address is extracted from the XOR-MAPPED-ADDRESS attribute. +func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) { + if !Is(b) { + return tID, netip.AddrPort{}, ErrNotSTUN + } + copy(tID[:], b[8:8+len(tID)]) + if b[0] != 0x01 || b[1] != 0x01 { + return tID, netip.AddrPort{}, ErrNotSuccessResponse + } + attrsLen := int(binary.BigEndian.Uint16(b[2:4])) + b = b[headerLen:] // remove STUN header + if attrsLen > len(b) { + return tID, netip.AddrPort{}, ErrMalformedAttrs + } else if len(b) > attrsLen { + b = b[:attrsLen] // trim trailing packet bytes + } + + var fallbackAddr netip.AddrPort + + // Read through the attributes. + // The the addr+port reported by XOR-MAPPED-ADDRESS + // as the canonical value. If the attribute is not + // present but the STUN server responds with + // MAPPED-ADDRESS we fall back to it. + if err := foreachAttr(b, func(attrType uint16, attr []byte) error { + switch attrType { + case attrXorMappedAddress, attrXorMappedAddressAlt: + ipSlice, port, err := xorMappedAddress(tID, attr) + if err != nil { + return err + } + if ip, ok := netip.AddrFromSlice(ipSlice); ok { + addr = netip.AddrPortFrom(ip.Unmap(), port) + } + case attrMappedAddress: + ipSlice, port, err := mappedAddress(attr) + if err != nil { + return ErrMalformedAttrs + } + if ip, ok := netip.AddrFromSlice(ipSlice); ok { + fallbackAddr = netip.AddrPortFrom(ip.Unmap(), port) + } + } + return nil + + }); err != nil { + return TxID{}, netip.AddrPort{}, err + } + + if addr.IsValid() { + return tID, addr, nil + } + if fallbackAddr.IsValid() { + return tID, fallbackAddr, nil + } + return tID, netip.AddrPort{}, ErrMalformedAttrs +} + +func xorMappedAddress(tID TxID, b []byte) (addr []byte, port uint16, err error) { + // XOR-MAPPED-ADDRESS attribute, RFC5389 Section 15.2 + if len(b) < 4 { + return nil, 0, ErrMalformedAttrs + } + xorPort := binary.BigEndian.Uint16(b[2:4]) + addrField := b[4:] + port = xorPort ^ 0x2112 // first half of magicCookie + + addrLen := familyAddrLen(b[1]) + if addrLen == 0 { + return nil, 0, ErrMalformedAttrs + } + if len(addrField) < addrLen { + return nil, 0, ErrMalformedAttrs + } + xorAddr := addrField[:addrLen] + addr = make([]byte, addrLen) + for i := range xorAddr { + if i < len(magicCookie) { + addr[i] = xorAddr[i] ^ magicCookie[i] + } else { + addr[i] = xorAddr[i] ^ tID[i-len(magicCookie)] + } + } + return addr, port, nil +} + +func familyAddrLen(fam byte) int { + switch fam { + case 0x01: // IPv4 + return net.IPv4len + case 0x02: // IPv6 + return net.IPv6len + default: + return 0 + } +} + +func mappedAddress(b []byte) (addr []byte, port uint16, err error) { + if len(b) < 4 { + return nil, 0, ErrMalformedAttrs + } + port = uint16(b[2])<<8 | uint16(b[3]) + addrField := b[4:] + addrLen := familyAddrLen(b[1]) + if addrLen == 0 { + return nil, 0, ErrMalformedAttrs + } + if len(addrField) < addrLen { + return nil, 0, ErrMalformedAttrs + } + return bytes.Clone(addrField[:addrLen]), port, nil +} + +// Is reports whether b is a STUN message. +func Is(b []byte) bool { + return len(b) >= headerLen && + b[0]&0b11000000 == 0 && // top two bits must be zero + string(b[4:8]) == magicCookie +}