Compare commits

...

10 Commits

Author SHA1 Message Date
julianknodt
4f1e783ad8 wgengine: log connection metrics
Adds counter for connection types, which aren't currently bubbled up anywhere but can be easily
in the future.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-20 12:04:28 -07:00
julianknodt
b9bd7dbc5d net/portmapper: log upnp information
This logs some basic statistics for UPnP, so that tailscale can better understand what routers
are being used and how to connect to them.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-10 22:45:00 -07:00
julianknodt
26b6fe7f02 net/portmapper: add PCP integration test
This adds a PCP test to the IGD test server, by hardcoding in a few observed packets from
Denton's box.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-10 15:14:46 -07:00
Brad Fitzpatrick
3700cf9ea4 tsweb: also support LabelMaps from expvar.Map, without metrics
We want to use tsweb to format Prometheus-style metrics from
our temporary golang.org/x/net/http2 fork, but we don't want http2
to depend on the tailscale.com module to use the concrete type
tailscale.com/metrics.LabelMap. Instead, let a expvar.Map be used
instead of it's annotated sufficiently in its name.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-10 14:31:54 -07:00
Brad Fitzpatrick
5f45d8f8e6 tsweb: add VarzHandler tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-10 13:41:16 -07:00
Josh Bleecher Snyder
a4e19f2233 version: remove rsc.io/goversion dependency
rsc.io/goversion is really expensive.
Running version.ReadExe on tailscaled on darwin
allocates 47k objects, almost 11mb.

All we want is the module info. For that, all we need to do
is scan through the binary looking for the magic start/end strings
and then grab the bytes in between them.

We can do that easily and quickly with nothing but a 64k buffer.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-09 22:46:01 -07:00
Brad Fitzpatrick
bdb93c5942 net/portmapper: actually test something in TestProbeIntegration
And use dynamic port numbers in tests, as Linux on GitHub Actions and
Windows in general have things running on these ports.

Co-Author: Julian Knodt <julianknodt@gmail.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-09 19:49:02 -07:00
Denton Gentry
26c1183941 hostinfo: add fly.io detection
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-09 09:54:24 -07:00
Denton Gentry
0796c53404 tsnet: add AuthKey support.
Set a TS_AUTHKEY environment variable to "tskey-01234..."

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-09 09:30:09 -07:00
Adrian Dewhurst
8bdf878832 net/dns/resolver: use forwarded dns txid directly
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. This includes a
new test to cover that situation.

Fixes #2597

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-08-06 14:56:11 -04:00
22 changed files with 651 additions and 120 deletions

View File

@@ -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+

View File

@@ -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
View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 != "" {

View File

@@ -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)
}
})
}
}

View File

@@ -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
View 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)
}
}

View File

@@ -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
}

View File

@@ -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 == "" {

View File

@@ -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)