Compare commits
10 Commits
dsnet/admi
...
jknodt/log
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f1e783ad8 | ||
|
|
b9bd7dbc5d | ||
|
|
26b6fe7f02 | ||
|
|
3700cf9ea4 | ||
|
|
5f45d8f8e6 | ||
|
|
a4e19f2233 | ||
|
|
bdb93c5942 | ||
|
|
26c1183941 | ||
|
|
0796c53404 | ||
|
|
8bdf878832 |
@@ -20,7 +20,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
@@ -101,9 +100,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -126,10 +124,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -143,8 +137,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
@@ -171,10 +164,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember
|
||||
path from debug/dwarf+
|
||||
path from html/template+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
|
||||
@@ -87,7 +87,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
@@ -216,9 +215,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -242,10 +240,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -259,8 +253,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
@@ -288,7 +281,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
|
||||
1
go.mod
1
go.mod
@@ -51,5 +51,4 @@ require (
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
Heroku = EnvType("hr")
|
||||
AzureAppService = EnvType("az")
|
||||
AWSFargate = EnvType("fg")
|
||||
FlyDotIo = EnvType("fly")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -57,6 +58,9 @@ func getEnvType() EnvType {
|
||||
if inAWSFargate() {
|
||||
return AWSFargate
|
||||
}
|
||||
if inFlyDotIo() {
|
||||
return FlyDotIo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -126,3 +130,10 @@ func inAWSFargate() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inFlyDotIo() bool {
|
||||
if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
@@ -65,44 +64,13 @@ func getTxID(packet []byte) txid {
|
||||
}
|
||||
|
||||
dnsid := binary.BigEndian.Uint16(packet[0:2])
|
||||
qcount := binary.BigEndian.Uint16(packet[4:6])
|
||||
if qcount == 0 {
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
offset := headerBytes
|
||||
for i := uint16(0); i < qcount; i++ {
|
||||
// Note: this relies on the fact that names are not compressed in questions,
|
||||
// so they are guaranteed to end with a NUL byte.
|
||||
//
|
||||
// Justification:
|
||||
// RFC 1035 doesn't seem to explicitly prohibit compressing names in questions,
|
||||
// but this is exceedingly unlikely to be done in practice. A DNS request
|
||||
// with multiple questions is ill-defined (which questions do the header flags apply to?)
|
||||
// and a single question would have to contain a pointer to an *answer*,
|
||||
// which would be excessively smart, pointless (an answer can just as well refer to the question)
|
||||
// and perhaps even prohibited: a draft RFC (draft-ietf-dnsind-local-compression-05) states:
|
||||
//
|
||||
// > It is important that these pointers always point backwards.
|
||||
//
|
||||
// This is said in summarizing RFC 1035, although that phrase does not appear in the original RFC.
|
||||
// Additionally, (https://cr.yp.to/djbdns/notes.html) states:
|
||||
//
|
||||
// > The precise rule is that a name can be compressed if it is a response owner name,
|
||||
// > the name in NS data, the name in CNAME data, the name in PTR data, the name in MX data,
|
||||
// > or one of the names in SOA data.
|
||||
namebytes := bytes.IndexByte(packet[offset:], 0)
|
||||
// ... | name | NUL | type | class
|
||||
// ?? 1 2 2
|
||||
offset = offset + namebytes + 5
|
||||
if len(packet) < offset {
|
||||
// Corrupt packet; don't crash.
|
||||
return txid(dnsid)
|
||||
}
|
||||
}
|
||||
|
||||
hash := crc32.ChecksumIEEE(packet[headerBytes:offset])
|
||||
return (txid(hash) << 32) | txid(dnsid)
|
||||
// Previously, we hashed the question and combined it with the original txid
|
||||
// which was useful when concurrent queries were multiplexed on a single
|
||||
// local source port. We encountered some situations where the DNS server
|
||||
// canonicalizes the question in the response (uppercase converted to
|
||||
// lowercase in this case), which resulted in responses that we couldn't
|
||||
// match to the original request due to hash mismatches.
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -66,6 +67,58 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToIPLowercase returns a handler function which canonicalizes responses
|
||||
// by lowercasing the question and answer names, and responds
|
||||
// to queries of type A it receives with an A record containing ipv4,
|
||||
// to queries of type AAAA with an AAAA record containing ipv6,
|
||||
// to queries of type NS with an NS record containg name.
|
||||
func resolveToIPLowercase(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
m.Question[0].Name = strings.ToLower(m.Question[0].Name)
|
||||
question := req.Question[0]
|
||||
|
||||
var ans dns.RR
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
ans = &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ipv4.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
ans = &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ipv6.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeNS:
|
||||
ans = &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
m.Answer = append(m.Answer, ans)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToTXT returns a handler function which responds to queries of type TXT
|
||||
// it receives with the strings in txts.
|
||||
func resolveToTXT(txts []string, ednsMaxSize uint16) dns.HandlerFunc {
|
||||
|
||||
@@ -440,6 +440,8 @@ func TestDelegate(t *testing.T) {
|
||||
records := []interface{}{
|
||||
"test.site.",
|
||||
resolveToIP(testipv4, testipv6, "dns.test.site."),
|
||||
"LCtesT.SiTe.",
|
||||
resolveToIPLowercase(testipv4, testipv6, "dns.test.site."),
|
||||
"nxdomain.site.", resolveToNXDOMAIN,
|
||||
"small.txt.", resolveToTXT(smallTXT, noEdns),
|
||||
"smalledns.txt.", resolveToTXT(smallTXT, 512),
|
||||
@@ -485,6 +487,21 @@ func TestDelegate(t *testing.T) {
|
||||
dnspacket("test.site.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv4",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeA, noEdns),
|
||||
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv6",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeAAAA, noEdns),
|
||||
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ns",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"nxdomain",
|
||||
dnspacket("nxdomain.site.", dns.TypeA, noEdns),
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
|
||||
@@ -21,15 +23,25 @@ type TestIGD struct {
|
||||
upnpConn net.PacketConn // for UPnP discovery
|
||||
pxpConn net.PacketConn // for NAT-PMP and/or PCP
|
||||
ts *httptest.Server
|
||||
logf logger.Logf
|
||||
|
||||
// do* will log which packets are sent, but will not reply to unexpected packets.
|
||||
|
||||
doPMP bool
|
||||
doPCP bool
|
||||
doUPnP bool // TODO: more options for 3 flavors of UPnP services
|
||||
doUPnP bool
|
||||
|
||||
mu sync.Mutex // guards below
|
||||
counters igdCounters
|
||||
}
|
||||
|
||||
// TestIGDOptions are options
|
||||
type TestIGDOptions struct {
|
||||
PMP bool
|
||||
PCP bool
|
||||
UPnP bool // TODO: more options for 3 flavors of UPnP services
|
||||
}
|
||||
|
||||
type igdCounters struct {
|
||||
numUPnPDiscoRecv int32
|
||||
numUPnPOtherUDPRecv int32
|
||||
@@ -38,21 +50,28 @@ type igdCounters struct {
|
||||
numPMPDiscoRecv int32
|
||||
numPCPRecv int32
|
||||
numPCPDiscoRecv int32
|
||||
numPCPMapRecv int32
|
||||
numPCPOtherRecv int32
|
||||
numPMPPublicAddrRecv int32
|
||||
numPMPBogusRecv int32
|
||||
|
||||
numFailedWrites int32
|
||||
invalidPCPMapPkt int32
|
||||
}
|
||||
|
||||
func NewTestIGD() (*TestIGD, error) {
|
||||
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
|
||||
d := &TestIGD{
|
||||
doPMP: true,
|
||||
doPCP: true,
|
||||
doUPnP: true,
|
||||
logf: logf,
|
||||
doPMP: t.PMP,
|
||||
doPCP: t.PCP,
|
||||
doUPnP: t.UPnP,
|
||||
}
|
||||
var err error
|
||||
if d.upnpConn, err = net.ListenPacket("udp", "127.0.0.1:1900"); err != nil {
|
||||
if d.upnpConn, err = testListenUDP(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.pxpConn, err = net.ListenPacket("udp", "127.0.0.1:5351"); err != nil {
|
||||
if d.pxpConn, err = testListenUDP(); err != nil {
|
||||
d.upnpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
|
||||
@@ -61,6 +80,22 @@ func NewTestIGD() (*TestIGD, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func testListenUDP() (net.PacketConn, error) {
|
||||
return net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
}
|
||||
|
||||
func (d *TestIGD) TestPxPPort() uint16 {
|
||||
return uint16(d.pxpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
|
||||
func (d *TestIGD) TestUPnPPort() uint16 {
|
||||
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
|
||||
func testIPAndGateway() (gw, ip netaddr.IP, ok bool) {
|
||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||
}
|
||||
|
||||
func (d *TestIGD) Close() error {
|
||||
d.ts.Close()
|
||||
d.upnpConn.Close()
|
||||
@@ -89,13 +124,19 @@ func (d *TestIGD) serveUPnPDiscovery() {
|
||||
for {
|
||||
n, src, err := d.upnpConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
d.logf("serveUPnP failed: %v", err)
|
||||
return
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
|
||||
d.inc(&d.counters.numUPnPDiscoRecv)
|
||||
resPkt := []byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: %s\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n", d.ts.URL+"/rootDesc.xml"))
|
||||
d.upnpConn.WriteTo(resPkt, src)
|
||||
if d.doUPnP {
|
||||
_, err = d.upnpConn.WriteTo(resPkt, src)
|
||||
if err != nil {
|
||||
d.inc(&d.counters.numFailedWrites)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d.inc(&d.counters.numUPnPOtherUDPRecv)
|
||||
}
|
||||
@@ -108,6 +149,7 @@ func (d *TestIGD) servePxP() {
|
||||
for {
|
||||
n, a, err := d.pxpConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
d.logf("servePxP failed: %v", err)
|
||||
return
|
||||
}
|
||||
ua := a.(*net.UDPAddr)
|
||||
@@ -151,5 +193,55 @@ func (d *TestIGD) handlePMPQuery(pkt []byte, src netaddr.IPPort) {
|
||||
|
||||
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
|
||||
d.inc(&d.counters.numPCPRecv)
|
||||
// TODO
|
||||
if len(pkt) < 24 {
|
||||
return
|
||||
}
|
||||
op := pkt[1]
|
||||
pktSrcBytes := [16]byte{}
|
||||
copy(pktSrcBytes[:], pkt[8:24])
|
||||
pktSrc := netaddr.IPFrom16(pktSrcBytes)
|
||||
if pktSrc != src.IP() {
|
||||
// TODO this error isn't fatal but should be rejected by server.
|
||||
// Since it's a test it's difficult to get them the same though.
|
||||
d.logf("mismatch of packet source and source IP: got %v, expected %v", pktSrc, src.IP())
|
||||
}
|
||||
switch op {
|
||||
case pcpOpAnnounce:
|
||||
d.inc(&d.counters.numPCPDiscoRecv)
|
||||
if !d.doPCP {
|
||||
return
|
||||
}
|
||||
resp := buildPCPDiscoResponse(pkt)
|
||||
if _, err := d.pxpConn.WriteTo(resp, src.UDPAddr()); err != nil {
|
||||
d.inc(&d.counters.numFailedWrites)
|
||||
}
|
||||
case pcpOpMap:
|
||||
if len(pkt) < 60 {
|
||||
d.logf("got too short packet for pcp op map: %v", pkt)
|
||||
d.inc(&d.counters.invalidPCPMapPkt)
|
||||
return
|
||||
}
|
||||
d.inc(&d.counters.numPCPMapRecv)
|
||||
if !d.doPCP {
|
||||
return
|
||||
}
|
||||
resp := buildPCPMapResponse(pkt)
|
||||
d.pxpConn.WriteTo(resp, src.UDPAddr())
|
||||
default:
|
||||
// unknown op code, ignore it for now.
|
||||
d.inc(&d.counters.numPCPOtherRecv)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, igd *TestIGD) *Client {
|
||||
var c *Client
|
||||
c = NewClient(t.Logf, func() {
|
||||
t.Logf("port map changed")
|
||||
t.Logf("have mapping: %v", c.HaveMapping())
|
||||
})
|
||||
c.testPxPPort = igd.TestPxPPort()
|
||||
c.testUPnPPort = igd.TestUPnPPort()
|
||||
c.SetGatewayLookupFunc(testIPAndGateway)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
// References:
|
||||
@@ -22,8 +21,8 @@ import (
|
||||
|
||||
// PCP constants
|
||||
const (
|
||||
pcpVersion = 2
|
||||
pcpPort = 5351
|
||||
pcpVersion = 2
|
||||
pcpDefaultPort = 5351
|
||||
|
||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||
|
||||
@@ -39,7 +38,8 @@ const (
|
||||
)
|
||||
|
||||
type pcpMapping struct {
|
||||
gw netaddr.IP
|
||||
c *Client
|
||||
gw netaddr.IPPort
|
||||
internal netaddr.IPPort
|
||||
external netaddr.IPPort
|
||||
|
||||
@@ -54,13 +54,13 @@ func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
|
||||
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
||||
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
|
||||
func (p *pcpMapping) Release(ctx context.Context) {
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0, p.external.IP())
|
||||
uc.WriteTo(pkt, netaddr.IPPortFrom(p.gw, pcpPort).UDPAddr())
|
||||
uc.WriteTo(pkt, p.gw.UDPAddr())
|
||||
}
|
||||
|
||||
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
||||
@@ -95,6 +95,8 @@ func buildPCPRequestMappingPacket(
|
||||
return pkt
|
||||
}
|
||||
|
||||
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
|
||||
// In particular, its Client is not populated.
|
||||
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
if len(resp) < 60 {
|
||||
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -25,3 +26,37 @@ func TestParsePCPMapResponse(t *testing.T) {
|
||||
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
serverResponseBit = 1 << 7
|
||||
fakeLifetimeSec = 1<<31 - 1
|
||||
)
|
||||
|
||||
func buildPCPDiscoResponse(req []byte) []byte {
|
||||
out := make([]byte, 24)
|
||||
out[0] = pcpVersion
|
||||
out[1] = req[1] | serverResponseBit
|
||||
out[3] = 0
|
||||
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||
return out
|
||||
}
|
||||
|
||||
func buildPCPMapResponse(req []byte) []byte {
|
||||
out := make([]byte, 24+36)
|
||||
out[0] = pcpVersion
|
||||
out[1] = req[1] | serverResponseBit
|
||||
out[3] = 0
|
||||
binary.BigEndian.PutUint32(out[4:8], 1<<30)
|
||||
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||
mapResp := out[24:]
|
||||
mapReq := req[24:]
|
||||
// copy nonce, protocol and internal port
|
||||
copy(mapResp[:13], mapReq[:13])
|
||||
copy(mapResp[16:18], mapReq[16:18])
|
||||
// assign external port
|
||||
binary.BigEndian.PutUint16(mapResp[18:20], 4242)
|
||||
assignedIP := netaddr.IPv4(127, 0, 0, 1)
|
||||
assignedIP16 := assignedIP.As16()
|
||||
copy(mapResp[20:36], assignedIP16[:])
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,6 +56,8 @@ type Client struct {
|
||||
logf logger.Logf
|
||||
ipAndGateway func() (gw, ip netaddr.IP, ok bool)
|
||||
onChange func() // or nil
|
||||
testPxPPort uint16 // if non-zero, pxpPort to use for tests
|
||||
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
|
||||
|
||||
mu sync.Mutex // guards following, and all fields thereof
|
||||
|
||||
@@ -113,7 +116,8 @@ func (c *Client) HaveMapping() bool {
|
||||
//
|
||||
// All fields are immutable once created.
|
||||
type pmpMapping struct {
|
||||
gw netaddr.IP
|
||||
c *Client
|
||||
gw netaddr.IPPort
|
||||
external netaddr.IPPort
|
||||
internal netaddr.IPPort
|
||||
renewAfter time.Time // the time at which we want to renew the mapping
|
||||
@@ -132,13 +136,13 @@ func (p *pmpMapping) External() netaddr.IPPort { return p.external }
|
||||
|
||||
// Release does a best effort fire-and-forget release of the PMP mapping m.
|
||||
func (m *pmpMapping) Release(ctx context.Context) {
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := m.c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
|
||||
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
|
||||
uc.WriteTo(pkt, m.gw.UDPAddr())
|
||||
}
|
||||
|
||||
// NewClient returns a new portmapping client.
|
||||
@@ -213,6 +217,32 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// pxpPort returns the NAT-PMP and PCP port number.
|
||||
// It returns 5351, except for in tests where it varies by run.
|
||||
func (c *Client) pxpPort() uint16 {
|
||||
if c.testPxPPort != 0 {
|
||||
return c.testPxPPort
|
||||
}
|
||||
return pmpDefaultPort
|
||||
}
|
||||
|
||||
// upnpPort returns the UPnP discovery port number.
|
||||
// It returns 1900, except for in tests where it varies by run.
|
||||
func (c *Client) upnpPort() uint16 {
|
||||
if c.testUPnPPort != 0 {
|
||||
return c.testUPnPPort
|
||||
}
|
||||
return upnpDefaultPort
|
||||
}
|
||||
|
||||
func (c *Client) listenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) {
|
||||
if (c.testPxPPort != 0 || c.testUPnPPort != 0) && os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
var lc net.ListenConfig
|
||||
return lc.ListenPacket(ctx, network, addr)
|
||||
}
|
||||
return netns.Listener().ListenPacket(ctx, network, addr)
|
||||
}
|
||||
|
||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||
if c.mapping != nil {
|
||||
if releaseOld {
|
||||
@@ -399,7 +429,8 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// PCP returns all the information necessary for a mapping in a single packet, so we can
|
||||
// construct it upon receiving that packet.
|
||||
m := &pmpMapping{
|
||||
gw: gw,
|
||||
c: c,
|
||||
gw: netaddr.IPPortFrom(gw, c.pxpPort()),
|
||||
internal: internalAddr,
|
||||
}
|
||||
if haveRecentPMP {
|
||||
@@ -415,7 +446,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
@@ -424,7 +455,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pxpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
||||
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort())
|
||||
pxpAddru := pxpAddr.UDPAddr()
|
||||
|
||||
preferPCP := !DisablePCP && (DisablePMP || (!haveRecentPMP && haveRecentPCP))
|
||||
@@ -499,8 +530,9 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// PCP should only have a single packet response
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
pcpMapping.c = c
|
||||
pcpMapping.internal = m.internal
|
||||
pcpMapping.gw = gw
|
||||
pcpMapping.gw = netaddr.IPPortFrom(gw, c.pxpPort())
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mapping = pcpMapping
|
||||
@@ -524,7 +556,7 @@ type pmpResultCode uint16
|
||||
|
||||
// NAT-PMP constants.
|
||||
const (
|
||||
pmpPort = 5351
|
||||
pmpDefaultPort = 5351
|
||||
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
||||
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
||||
|
||||
@@ -622,7 +654,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
uc, err := c.listenPacket(context.Background(), "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
@@ -632,9 +664,8 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
||||
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort()).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, c.upnpPort()).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
@@ -642,12 +673,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else if !DisablePMP {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else if !DisablePCP {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
@@ -669,9 +700,9 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
port := addr.(*net.UDPAddr).Port
|
||||
port := uint16(addr.(*net.UDPAddr).Port)
|
||||
switch port {
|
||||
case upnpPort:
|
||||
case c.upnpPort():
|
||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||
if err != nil {
|
||||
@@ -683,10 +714,13 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.uPnPMeta = meta
|
||||
if c.uPnPMeta != meta {
|
||||
c.logf("UPnP meta changed: %+v", meta)
|
||||
c.uPnPMeta = meta
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case pcpPort: // same as pmpPort
|
||||
case c.pxpPort(): // same value for PMP and PCP
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
@@ -729,7 +763,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
|
||||
|
||||
const (
|
||||
upnpPort = 1900 // for UDP discovery only; TCP port discovered later
|
||||
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
|
||||
)
|
||||
|
||||
// uPnPPacket is the UPnP UDP discovery packet's request body.
|
||||
|
||||
@@ -7,12 +7,10 @@ package portmapper
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestCreateOrGetMapping(t *testing.T) {
|
||||
@@ -60,28 +58,68 @@ func TestClientProbeThenMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProbeIntegration(t *testing.T) {
|
||||
igd, err := NewTestIGD()
|
||||
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: true, PCP: true, UPnP: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer igd.Close()
|
||||
|
||||
logf := t.Logf
|
||||
var c *Client
|
||||
c = NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
})
|
||||
|
||||
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
|
||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||
})
|
||||
c := newTestClient(t, igd)
|
||||
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Probe(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Probe: %v", err)
|
||||
}
|
||||
if !res.UPnP {
|
||||
t.Errorf("didn't detect UPnP")
|
||||
}
|
||||
st := igd.stats()
|
||||
want := igdCounters{
|
||||
numUPnPDiscoRecv: 1,
|
||||
numPMPRecv: 1,
|
||||
numPCPRecv: 1,
|
||||
numPCPDiscoRecv: 1,
|
||||
numPMPPublicAddrRecv: 1,
|
||||
}
|
||||
if !reflect.DeepEqual(st, want) {
|
||||
t.Errorf("unexpected stats:\n got: %+v\nwant: %+v", st, want)
|
||||
}
|
||||
|
||||
t.Logf("Probe: %+v", res)
|
||||
t.Logf("IGD stats: %+v", igd.stats())
|
||||
t.Logf("IGD stats: %+v", st)
|
||||
// TODO(bradfitz): finish
|
||||
}
|
||||
|
||||
func TestPCPIntegration(t *testing.T) {
|
||||
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: false, PCP: true, UPnP: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer igd.Close()
|
||||
|
||||
c := newTestClient(t, igd)
|
||||
defer c.Close()
|
||||
res, err := c.Probe(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("probe failed: %v", err)
|
||||
}
|
||||
if res.UPnP || res.PMP {
|
||||
t.Errorf("probe unexpectedly saw upnp or pmp: %+v", res)
|
||||
}
|
||||
if !res.PCP {
|
||||
t.Fatalf("probe did not see pcp: %+v", res)
|
||||
}
|
||||
|
||||
external, err := c.createOrGetMapping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get mapping: %v", err)
|
||||
}
|
||||
if external.IsZero() {
|
||||
t.Errorf("got zero IP, expected non-zero")
|
||||
}
|
||||
if c.mapping == nil {
|
||||
t.Errorf("got nil mapping after successful createOrGetMapping")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,11 @@ func (c *Client) getUPnPPortMapping(
|
||||
|
||||
type uPnPDiscoResponse struct {
|
||||
Location string
|
||||
// Server describes what version the UPnP is, such as MiniUPnPd/2.x.x
|
||||
Server string
|
||||
// USN is the serial number of the device, which also contains
|
||||
// what kind of UPnP service is being offered, i.e. InternetGatewayDevice:2
|
||||
USN string
|
||||
}
|
||||
|
||||
// parseUPnPDiscoResponse parses a UPnP HTTP-over-UDP discovery response.
|
||||
@@ -321,5 +326,7 @@ func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
|
||||
return r, err
|
||||
}
|
||||
r.Location = res.Header.Get("Location")
|
||||
r.Server = res.Header.Get("Server")
|
||||
r.USN = res.Header.Get("Usn")
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -43,9 +43,13 @@ func TestParseUPnPDiscoResponse(t *testing.T) {
|
||||
}{
|
||||
{"google", googleWifiUPnPDisco, uPnPDiscoResponse{
|
||||
Location: "http://192.168.86.1:5000/rootDesc.xml",
|
||||
Server: "Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9",
|
||||
USN: "uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
||||
}},
|
||||
{"pfsense", pfSenseUPnPDisco, uPnPDiscoResponse{
|
||||
Location: "http://192.168.1.1:2189/rootDesc.xml",
|
||||
Server: "FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1",
|
||||
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -177,11 +177,12 @@ func (s *Server) start() error {
|
||||
err = lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
UpdatePrefs: prefs,
|
||||
AuthKey: os.Getenv("TS_AUTHKEY"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting backend: %w", err)
|
||||
}
|
||||
if os.Getenv("TS_LOGIN") == "1" {
|
||||
if os.Getenv("TS_LOGIN") == "1" || os.Getenv("TS_AUTHKEY") != "" {
|
||||
s.lb.StartLoginInteractive()
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -364,18 +364,25 @@ func VarzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var dump func(prefix string, kv expvar.KeyValue)
|
||||
dump = func(prefix string, kv expvar.KeyValue) {
|
||||
name := prefix + kv.Key
|
||||
|
||||
key := kv.Key
|
||||
var typ string
|
||||
var label string
|
||||
switch {
|
||||
case strings.HasPrefix(kv.Key, "gauge_"):
|
||||
typ = "gauge"
|
||||
name = prefix + strings.TrimPrefix(kv.Key, "gauge_")
|
||||
key = strings.TrimPrefix(kv.Key, "gauge_")
|
||||
|
||||
case strings.HasPrefix(kv.Key, "counter_"):
|
||||
typ = "counter"
|
||||
name = prefix + strings.TrimPrefix(kv.Key, "counter_")
|
||||
key = strings.TrimPrefix(kv.Key, "counter_")
|
||||
}
|
||||
if strings.HasPrefix(key, "labelmap_") {
|
||||
key = strings.TrimPrefix(key, "labelmap_")
|
||||
if i := strings.Index(key, "_"); i != -1 {
|
||||
label, key = key[:i], key[i+1:]
|
||||
}
|
||||
}
|
||||
name := prefix + key
|
||||
|
||||
switch v := kv.Value.(type) {
|
||||
case *expvar.Int:
|
||||
@@ -422,13 +429,24 @@ func VarzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, v.Label, kv.Key, kv.Value)
|
||||
})
|
||||
case *expvar.Map:
|
||||
if label != "" && typ != "" {
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, label, kv.Key, kv.Value)
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintf(w, "# skipping expvar.Map %q with incomplete metadata: label %q, Prometheus type %q\n", name, label, typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
expvarDo(func(kv expvar.KeyValue) {
|
||||
dump("", kv)
|
||||
})
|
||||
}
|
||||
|
||||
var expvarDo = expvar.Do // pulled out for tests
|
||||
|
||||
func writeMemstats(w io.Writer, ms *runtime.MemStats) {
|
||||
out := func(name, typ string, v uint64, help string) {
|
||||
if help != "" {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"expvar"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -300,3 +302,128 @@ func BenchmarkLog(b *testing.B) {
|
||||
h.ServeHTTP(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarzHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
k string // key name
|
||||
v expvar.Var
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"int",
|
||||
"foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo counter\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"int_with_type_counter",
|
||||
"counter_foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo counter\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"int_with_type_gauge",
|
||||
"gauge_foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo gauge\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"metrics_set",
|
||||
"s",
|
||||
&metrics.Set{
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE s_bar counter\ns_bar 2\n# TYPE s_foo counter\ns_foo 1\n",
|
||||
},
|
||||
{
|
||||
"metrics_set_TODO_gauge_type",
|
||||
"gauge_s", // TODO(bradfitz): arguably a bug; should pass down type
|
||||
&metrics.Set{
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE s_bar counter\ns_bar 2\n# TYPE s_foo counter\ns_foo 1\n",
|
||||
},
|
||||
{
|
||||
"func_float64",
|
||||
"counter_x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"# TYPE x counter\nx 1.2\n",
|
||||
},
|
||||
{
|
||||
"func_float64_gauge",
|
||||
"gauge_x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"# TYPE x gauge\nx 1.2\n",
|
||||
},
|
||||
{
|
||||
"func_float64_untyped",
|
||||
"x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"# skipping expvar \"x\" (Go type expvar.Func returning float64) with undeclared Prometheus type\n",
|
||||
},
|
||||
{
|
||||
"metrics_label_map",
|
||||
"counter_m",
|
||||
&metrics.LabelMap{
|
||||
Label: "label",
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE m counter\nm{label=\"bar\"} 2\nm{label=\"foo\"} 1\n",
|
||||
},
|
||||
{
|
||||
"expvar_label_map",
|
||||
"counter_labelmap_keyname_m",
|
||||
func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
}(),
|
||||
"# TYPE m counter\nm{keyname=\"bar\"} 2\nm{keyname=\"foo\"} 1\n",
|
||||
},
|
||||
{
|
||||
"expvar_label_map_malformed",
|
||||
"counter_labelmap_lackslabel",
|
||||
func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
return m
|
||||
}(),
|
||||
"# skipping expvar.Map \"lackslabel\" with incomplete metadata: label \"\", Prometheus type \"counter\"\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() { expvarDo = expvar.Do }()
|
||||
expvarDo = func(f func(expvar.KeyValue)) {
|
||||
f(expvar.KeyValue{Key: tt.k, Value: tt.v})
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
VarzHandler(rec, httptest.NewRequest("GET", "/", nil))
|
||||
if got := rec.Body.Bytes(); string(got) != tt.want {
|
||||
t.Errorf("mismatch\n got: %q\nwant: %q\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"rsc.io/goversion/version"
|
||||
)
|
||||
|
||||
// CmdName returns either the base name of the current binary
|
||||
@@ -30,13 +32,13 @@ func CmdName() string {
|
||||
fallbackName := filepath.Base(strings.TrimSuffix(strings.ToLower(e), ".exe"))
|
||||
|
||||
var ret string
|
||||
v, err := version.ReadExe(e)
|
||||
info, err := findModuleInfo(e)
|
||||
if err != nil {
|
||||
return fallbackName
|
||||
}
|
||||
// v is like:
|
||||
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
|
||||
for _, line := range strings.Split(v.ModuleInfo, "\n") {
|
||||
for _, line := range strings.Split(info, "\n") {
|
||||
if strings.HasPrefix(line, "path\t") {
|
||||
goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale"
|
||||
ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
|
||||
@@ -48,3 +50,84 @@ func CmdName() string {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// findModuleInfo returns the Go module info from the executable file.
|
||||
func findModuleInfo(file string) (s string, err error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
// Scan through f until we find infoStart.
|
||||
buf := make([]byte, 65536)
|
||||
start, err := findOffset(f, buf, infoStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
start += int64(len(infoStart))
|
||||
// Seek to the end of infoStart and scan for infoEnd.
|
||||
_, err = f.Seek(start, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
end, err := findOffset(f, buf, infoEnd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
length := end - start
|
||||
// As of Aug 2021, tailscaled's mod info was about 2k.
|
||||
if length > int64(len(buf)) {
|
||||
return "", errors.New("mod info too large")
|
||||
}
|
||||
// We have located modinfo. Read it into buf.
|
||||
buf = buf[:length]
|
||||
_, err = f.Seek(start, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = io.ReadFull(f, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// findOffset finds the absolute offset of needle in f,
|
||||
// starting at f's current read position,
|
||||
// using temporary buffer buf.
|
||||
func findOffset(f *os.File, buf, needle []byte) (int64, error) {
|
||||
for {
|
||||
// Fill buf and look within it.
|
||||
n, err := f.Read(buf)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
i := bytes.Index(buf[:n], needle)
|
||||
if i < 0 {
|
||||
// Not found. Rewind a little bit in case we happened to end halfway through needle.
|
||||
rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
// If we're at EOF and rewound exactly len(needle) bytes, return io.EOF.
|
||||
_, err = f.ReadAt(buf[:1], rewind+int64(len(needle)))
|
||||
if err == io.EOF {
|
||||
return -1, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Found! Figure out exactly where.
|
||||
cur, err := f.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return cur - int64(n) + int64(i), nil
|
||||
}
|
||||
}
|
||||
|
||||
// These constants are taken from rsc.io/goversion.
|
||||
|
||||
var (
|
||||
infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
|
||||
infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2")
|
||||
)
|
||||
|
||||
29
version/modinfo_test.go
Normal file
29
version/modinfo_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindModuleInfo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
name := filepath.Join(dir, "tailscaled-version-test")
|
||||
out, err := exec.Command("go", "build", "-o", name, "tailscale.com/cmd/tailscaled").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build tailscaled: %v\n%s", err, out)
|
||||
}
|
||||
modinfo, err := findModuleInfo(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := "path\ttailscale.com/cmd/tailscaled\nmod\ttailscale.com"
|
||||
if !strings.HasPrefix(modinfo, prefix) {
|
||||
t.Errorf("unexpected modinfo contents %q", modinfo)
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,9 @@ type Conn struct {
|
||||
// havePrivateKey is whether privateKey is non-zero.
|
||||
havePrivateKey syncs.AtomicBool
|
||||
|
||||
// DERPCount is the number of DERP connections created.
|
||||
DERPCount uint32
|
||||
|
||||
// port is the preferred port from opts.Port; 0 means auto.
|
||||
port syncs.AtomicUint32
|
||||
|
||||
@@ -350,6 +353,7 @@ func (c *Conn) addDerpPeerRoute(peer key.Public, derpID int, dc *derphttp.Client
|
||||
if c.derpRoute == nil {
|
||||
c.derpRoute = make(map[key.Public]derpRoute)
|
||||
}
|
||||
atomic.AddUint32(&c.DERPCount, 1)
|
||||
r := derpRoute{derpID, dc}
|
||||
c.derpRoute[peer] = r
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -122,6 +123,9 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wra
|
||||
}
|
||||
}
|
||||
|
||||
// Accepted a flow, log the flow kind
|
||||
atomic.AddUint32(&e.connMetrics.direct, 1)
|
||||
|
||||
timer := time.AfterFunc(tcpTimeoutBeforeDebug, func() {
|
||||
e.onOpenTimeout(flow)
|
||||
})
|
||||
@@ -153,6 +157,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
e.mu.Unlock()
|
||||
|
||||
if !problem.IsZero() {
|
||||
atomic.AddUint32(&e.connMetrics.unreachable, 1)
|
||||
e.logf("open-conn-track: timeout opening %v; peer reported problem: %v", flow, problem)
|
||||
}
|
||||
|
||||
@@ -160,14 +165,17 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
n, err := e.peerForIP(flow.Dst.IP())
|
||||
if err != nil {
|
||||
e.logf("open-conn-track: timeout opening %v; peerForIP: %v", flow, err)
|
||||
atomic.AddUint32(&e.connMetrics.unreachable, 1)
|
||||
return
|
||||
}
|
||||
if n == nil {
|
||||
e.logf("open-conn-track: timeout opening %v; no associated peer node", flow)
|
||||
atomic.AddUint32(&e.connMetrics.unreachable, 1)
|
||||
return
|
||||
}
|
||||
if n.DiscoKey.IsZero() {
|
||||
e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key.ShortString())
|
||||
atomic.AddUint32(&e.connMetrics.unreachable, 1)
|
||||
return
|
||||
}
|
||||
if n.DERP == "" {
|
||||
|
||||
@@ -119,6 +119,8 @@ type userspaceEngine struct {
|
||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||
lastStatusPollTime mono.Time // last time we polled the engine status
|
||||
|
||||
connMetrics metrics // counts for success/failure/kinds of connections.
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
netMap *netmap.NetworkMap // or nil
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
@@ -133,6 +135,19 @@ type userspaceEngine struct {
|
||||
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
// offline is the number of flows magicsock attempted to connect to which are offline.
|
||||
offline uint32
|
||||
// relayed is the number of flows magicsock connected thru over DERP.
|
||||
relayed uint32
|
||||
// direct is the number of flows connected to directly.
|
||||
direct uint32
|
||||
// unreachable is the number of flows which were expected to be online but could not be
|
||||
// connected to. This is expected to be small, as there is only a brief window where flows are
|
||||
// offline but have not yet notified control.
|
||||
unreachable uint32
|
||||
}
|
||||
|
||||
// InternalsGetter is implemented by Engines that can export their internals.
|
||||
type InternalsGetter interface {
|
||||
GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, ok bool)
|
||||
|
||||
Reference in New Issue
Block a user