mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 12:58:37 +00:00
net/portmapper: add stateful prober
Previously, the prober was stateless, and probe needed to be called manually whenever additional probes were required. This adds a stateful prober, which can theoretically reuse clients between runs and have smarter strategies for delays between retries, which may be crucial depending on how slow UPnP is. Signed-off-by: julianknodt <julianknodt@gmail.com>
This commit is contained in:
parent
525eb5ce41
commit
caceeff374
@ -3,6 +3,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/huin/goupnp from github.com/huin/goupnp/dcps/internetgateway2
|
||||
github.com/huin/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/huin/goupnp/httpu from github.com/huin/goupnp+
|
||||
github.com/huin/goupnp/scpd from github.com/huin/goupnp
|
||||
github.com/huin/goupnp/soap from github.com/huin/goupnp+
|
||||
github.com/huin/goupnp/ssdp from github.com/huin/goupnp
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
@ -70,18 +76,32 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/html from golang.org/x/net/html/charset
|
||||
golang.org/x/net/html/atom from golang.org/x/net/html
|
||||
golang.org/x/net/html/charset from github.com/huin/goupnp
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
golang.org/x/text/encoding from golang.org/x/net/html/charset+
|
||||
golang.org/x/text/encoding/charmap from golang.org/x/net/html/charset+
|
||||
golang.org/x/text/encoding/htmlindex from golang.org/x/net/html/charset
|
||||
golang.org/x/text/encoding/internal from golang.org/x/text/encoding/charmap+
|
||||
golang.org/x/text/encoding/japanese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/korean from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/simplifiedchinese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/traditionalchinese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/unicode from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/language from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/runes from golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
@ -126,7 +146,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
|
@ -10,6 +10,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
github.com/huin/goupnp from github.com/huin/goupnp/dcps/internetgateway2
|
||||
github.com/huin/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/huin/goupnp/httpu from github.com/huin/goupnp+
|
||||
github.com/huin/goupnp/scpd from github.com/huin/goupnp
|
||||
github.com/huin/goupnp/soap from github.com/huin/goupnp+
|
||||
github.com/huin/goupnp/ssdp from github.com/huin/goupnp
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
@ -170,6 +176,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/html from golang.org/x/net/html/charset
|
||||
golang.org/x/net/html/atom from golang.org/x/net/html
|
||||
golang.org/x/net/html/charset from github.com/huin/goupnp
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
@ -178,7 +187,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
@ -187,6 +196,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/encoding from golang.org/x/net/html/charset+
|
||||
golang.org/x/text/encoding/charmap from golang.org/x/net/html/charset+
|
||||
golang.org/x/text/encoding/htmlindex from golang.org/x/net/html/charset
|
||||
golang.org/x/text/encoding/internal from golang.org/x/text/encoding/charmap+
|
||||
golang.org/x/text/encoding/japanese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/korean from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/simplifiedchinese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/traditionalchinese from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/unicode from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/language from golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/runes from golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
@ -232,6 +252,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/huin/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
|
3
go.mod
3
go.mod
@ -16,6 +16,7 @@ require (
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/huin/goupnp v1.0.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.12.2
|
||||
@ -32,7 +33,7 @@ require (
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
|
6
go.sum
6
go.sum
@ -296,6 +296,9 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo=
|
||||
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
@ -685,6 +688,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -718,6 +722,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
@ -377,6 +376,12 @@ const (
|
||||
pmpCodeUnsupportedOpcode pmpResultCode = 5
|
||||
)
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
|
||||
pkt = make([]byte, 12)
|
||||
|
||||
@ -433,127 +438,6 @@ func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
|
||||
return res, true
|
||||
}
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
// Probe returns a summary of which port mapping services are
|
||||
// available on the network.
|
||||
//
|
||||
// If a probe has run recently and there haven't been any network changes since,
|
||||
// the returned result might be server from the Client's cache, without
|
||||
// sending any network traffic.
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return res, ErrGatewayNotFound
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastProbe = time.Now()
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
}
|
||||
defer uc.Close()
|
||||
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
} else {
|
||||
uc.WriteTo(uPnPPacket, upnpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && res.PMP && res.UPnP {
|
||||
// Nothing more to discover.
|
||||
return res, nil
|
||||
}
|
||||
n, addr, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
port := addr.(*net.UDPAddr).Port
|
||||
switch port {
|
||||
case upnpPort:
|
||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case pcpPort: // same as pmpPort
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
res.PCP = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
res.PCP = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
res.PMP = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
pcpVersion = 2
|
||||
pcpPort = 5351
|
||||
@ -627,14 +511,4 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
||||
return res, true
|
||||
}
|
||||
|
||||
const (
|
||||
upnpPort = 1900
|
||||
)
|
||||
|
||||
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|
||||
|
@ -32,12 +32,13 @@ func TestClientProbe(t *testing.T) {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(t.Logf)
|
||||
for i := 0; i < 2; i++ {
|
||||
c.NewProber(context.Background())
|
||||
for i := 0; i < 30; i++ {
|
||||
if i > 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
res, err := c.Probe(context.Background())
|
||||
t.Logf("Got: %+v, %v", res, err)
|
||||
res, err := c.Prober.CurrentStatus()
|
||||
t.Logf("Got(t=%dms): %+v, %v", i*100, res, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +48,8 @@ func TestClientProbeThenMap(t *testing.T) {
|
||||
}
|
||||
c := NewClient(t.Logf)
|
||||
c.SetLocalPort(1234)
|
||||
res, err := c.Probe(context.Background())
|
||||
c.NewProber(context.Background())
|
||||
res, err := c.Prober.StatusBlock()
|
||||
t.Logf("Probe: %+v, %v", res, err)
|
||||
ext, err := c.CreateOrGetMapping(context.Background())
|
||||
t.Logf("CreateOrGetMapping: %v, %v", ext, err)
|
||||
|
447
net/portmapper/probe.go
Normal file
447
net/portmapper/probe.go
Normal file
@ -0,0 +1,447 @@
|
||||
// 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 portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
// Probe returns a summary of which port mapping services are
|
||||
// available on the network.
|
||||
//
|
||||
// If a probe has run recently and there haven't been any network changes since,
|
||||
// the returned result might be served from the Client's cache, without
|
||||
// sending any network traffic.
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return res, ErrGatewayNotFound
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.mu.Lock()
|
||||
c.lastProbe = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
}
|
||||
defer uc.Close()
|
||||
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
wg := sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
} else {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
// TODO(jknodt) this is expensive, maybe it's worth caching it and just reusing it
|
||||
// more aggressively
|
||||
hasUPnP, _ := probeUPnP(ctx)
|
||||
if hasUPnP {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && res.PMP {
|
||||
// Nothing more to discover.
|
||||
return res, nil
|
||||
}
|
||||
n, _, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
res.PCP = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
res.PCP = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
res.PMP = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Prober struct {
|
||||
// pause signals the probe to either pause temporarily (true), or stop entirely (false)
|
||||
// to restart the probe, send another pause to it.
|
||||
pause chan<- bool
|
||||
|
||||
PMP *ProbeSubResult
|
||||
PCP *ProbeSubResult
|
||||
|
||||
upnpClient upnpClient
|
||||
UPnP *ProbeSubResult
|
||||
}
|
||||
|
||||
// NewProber creates a new prober for a given client.
|
||||
func (c *Client) NewProber(ctx context.Context) (p *Prober) {
|
||||
stop := make(chan bool)
|
||||
p = &Prober{
|
||||
stop: stop,
|
||||
|
||||
PMP: NewProbeSubResult(),
|
||||
PCP: NewProbeSubResult(),
|
||||
UPnP: NewProbeSubResult(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
pmp_ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
hasPCP, hasPMP, err := c.probePMPAndPCP(pmp_ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
// the global context has passed, exit cleanly
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if pmp_ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
p.PMP.Set(hasPMP, err)
|
||||
p.PCP.Set(hasPCP, err)
|
||||
|
||||
t := time.NewTimer(trustServiceStillAvailableDuration * 3 / 4)
|
||||
|
||||
select {
|
||||
case should_pause := <-pause:
|
||||
if !should_pause {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
restart := <-pause
|
||||
if !restart {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
case <-t.C: // break through and retry the connection
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// Do not timeout on getting an initial client, as we can reuse it so paying an initial cost
|
||||
// is fine.
|
||||
upnpClient, err := getUPnPClient(ctx)
|
||||
if upnpClient == nil || err != nil {
|
||||
p.UPnP.Set(false, err)
|
||||
return
|
||||
}
|
||||
p.upnpClient = upnpClient
|
||||
defer func() {
|
||||
// unset client when no longer using it.
|
||||
p.upnpClient = nil
|
||||
upnpClient.RequestTermination()
|
||||
}()
|
||||
// TODO maybe do something fancy/dynamic with more delay (exponential back-off)
|
||||
for {
|
||||
upnp_ctx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
||||
retries := 0
|
||||
hasUPnP := false
|
||||
const num_connect_retries = 5
|
||||
for retries < num_connect_retries {
|
||||
status, _, _, statusErr := p.upnpClient.GetStatusInfo()
|
||||
if statusErr != nil {
|
||||
err = statusErr
|
||||
break
|
||||
}
|
||||
hasUPnP = hasUPnP || status == "Connected"
|
||||
if status == "Disconnected" {
|
||||
upnpClient.RequestConnection()
|
||||
}
|
||||
retries += 1
|
||||
}
|
||||
// need to manually check these since GetStatusInfo doesn't take a context
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
// the global context has passed, exit cleanly
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if upnp_ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
cancel()
|
||||
p.UPnP.Set(hasUPnP, err)
|
||||
|
||||
t := time.NewTimer(trustServiceStillAvailableDuration * 3 / 4)
|
||||
|
||||
select {
|
||||
case should_pause := <-pause:
|
||||
if !should_pause {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
restart := <-pause
|
||||
if !restart {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
case <-t.C: // break through and retry the connection
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Stop gracefully turns the Prober off.
|
||||
func (p *Prober) Stop() {
|
||||
close(p.pause)
|
||||
}
|
||||
|
||||
// Pauses the prober if currently running, or starts if it was previously paused
|
||||
func (p *Prober) Toggle() {
|
||||
p.pause <- true
|
||||
}
|
||||
|
||||
// CurrentStatus returns the current results of the prober, regardless of whether they have
|
||||
// completed or not.
|
||||
func (p *Prober) CurrentStatus() (res ProbeResult, err error) {
|
||||
hasPMP, errPMP := p.PMP.PresentCurrent()
|
||||
res.PMP = hasPMP
|
||||
err = errPMP
|
||||
|
||||
hasUPnP, errUPnP := p.UPnP.PresentCurrent()
|
||||
res.UPnP = hasUPnP
|
||||
if err == nil {
|
||||
err = errUPnP
|
||||
}
|
||||
|
||||
hasPCP, errPCP := p.PCP.PresentCurrent()
|
||||
res.PCP = hasPCP
|
||||
if err == nil {
|
||||
err = errPCP
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Prober) StatusBlock() (res ProbeResult, err error) {
|
||||
hasPMP, errPMP := p.PMP.PresentBlock()
|
||||
res.PMP = hasPMP
|
||||
err = errPMP
|
||||
|
||||
hasUPnP, errUPnP := p.UPnP.PresentBlock()
|
||||
res.UPnP = hasUPnP
|
||||
if err == nil {
|
||||
err = errUPnP
|
||||
}
|
||||
|
||||
hasPCP, errPCP := p.PCP.PresentBlock()
|
||||
res.PCP = hasPCP
|
||||
if err == nil {
|
||||
err = errPCP
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ProbeSubResult struct {
|
||||
cond *sync.Cond
|
||||
// If this probe has finished, regardless of success or failure
|
||||
completed bool
|
||||
|
||||
// whether or not this feature is present
|
||||
present bool
|
||||
// most recent error
|
||||
err error
|
||||
|
||||
// time we last saw it to be available.
|
||||
sawTime time.Time
|
||||
}
|
||||
|
||||
func NewProbeSubResult() *ProbeSubResult {
|
||||
return &ProbeSubResult{
|
||||
cond: &sync.Cond{
|
||||
L: &sync.Mutex{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PresentBlock blocks until the probe completes, then returns the result.
|
||||
func (psr *ProbeSubResult) PresentBlock() (bool, error) {
|
||||
psr.cond.L.Lock()
|
||||
defer psr.cond.L.Unlock()
|
||||
for !psr.completed {
|
||||
psr.cond.Wait()
|
||||
}
|
||||
return psr.present, psr.err
|
||||
}
|
||||
|
||||
// PresentCurrent returns the current state, regardless whether or not the probe has completed.
|
||||
func (psr *ProbeSubResult) PresentCurrent() (bool, error) {
|
||||
psr.cond.L.Lock()
|
||||
defer psr.cond.L.Unlock()
|
||||
present := psr.present && psr.sawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
return present, psr.err
|
||||
}
|
||||
|
||||
func (psr *ProbeSubResult) Set(present bool, err error) {
|
||||
saw := time.Now()
|
||||
psr.cond.L.Lock()
|
||||
psr.sawTime = saw
|
||||
psr.completed = true
|
||||
psr.err = err
|
||||
psr.present = present
|
||||
psr.cond.L.Unlock()
|
||||
|
||||
psr.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (c *Client) probePMPAndPCP(ctx context.Context) (pcp bool, pmp bool, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return false, false, ErrGatewayNotFound
|
||||
}
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP/PMP: %v", err)
|
||||
return false, false, err
|
||||
}
|
||||
defer uc.Close()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
if c.sawPMPRecently() {
|
||||
pmp = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
pcp = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && pmp {
|
||||
// Nothing more to discover.
|
||||
return
|
||||
}
|
||||
n, _, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return pcp, pmp, err
|
||||
}
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
pcp = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
pcp = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
pmp = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
26
net/portmapper/probe_test.go
Normal file
26
net/portmapper/probe_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClientProber(t *testing.T) {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(t.Logf)
|
||||
ctx := context.Background()
|
||||
prober := c.NewProber(ctx)
|
||||
time.Sleep(3 * time.Second)
|
||||
prober.Stop()
|
||||
res, err := prober.CurrentStatus()
|
||||
t.Logf("Got: %+v, %v", res, err)
|
||||
}
|
120
net/portmapper/upnp.go
Normal file
120
net/portmapper/upnp.go
Normal file
@ -0,0 +1,120 @@
|
||||
// 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 portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/huin/goupnp/dcps/internetgateway2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// probeUPnP returns true if there are any upnp clients, or false with an error if none can be
|
||||
// found.
|
||||
func probeUPnP(ctx context.Context) (bool, error) {
|
||||
wg := sync.WaitGroup{}
|
||||
any := make(chan bool)
|
||||
errChan := make(chan error)
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
ip1Clients, _, err := internetgateway2.NewWANIPConnection1Clients()
|
||||
if len(ip1Clients) > 0 {
|
||||
any <- true
|
||||
}
|
||||
wg.Done()
|
||||
wg.Wait()
|
||||
errChan <- err
|
||||
}()
|
||||
go func() {
|
||||
ip2Clients, _, err := internetgateway2.NewWANIPConnection2Clients()
|
||||
if len(ip2Clients) > 0 {
|
||||
any <- true
|
||||
}
|
||||
wg.Done()
|
||||
wg.Wait()
|
||||
errChan <- err
|
||||
}()
|
||||
go func() {
|
||||
ppp1Clients, _, err := internetgateway2.NewWANPPPConnection1Clients()
|
||||
if len(ppp1Clients) > 0 {
|
||||
any <- true
|
||||
}
|
||||
wg.Done()
|
||||
wg.Wait()
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-any:
|
||||
return true, nil
|
||||
case err := <-errChan:
|
||||
// TODO probably want to take the non-nil of all the errors? Or something.
|
||||
return false, err
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
type upnpClient interface {
|
||||
// http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
|
||||
// Implicitly assume that the calls for all these are uniform, which might be a dangerous
|
||||
// assumption.
|
||||
AddPortMapping(
|
||||
NewRemoteHost string,
|
||||
NewExternalPort uint16,
|
||||
NewProtocol string,
|
||||
NewInternalPort uint16,
|
||||
NewInternalClient string,
|
||||
NewEnabled bool,
|
||||
NewPortMappingDescription string,
|
||||
NewLeaseDuration uint32,
|
||||
) (err error)
|
||||
|
||||
DeletePortMapping(NewRemoteHost string, NewExternalPort uint16, NewProtocol string) error
|
||||
GetStatusInfo() (status string, lastErr string, uptime uint32, err error)
|
||||
|
||||
RequestTermination() error
|
||||
RequestConnection() error
|
||||
}
|
||||
|
||||
// getUPnPClients gets a client for interfacing with UPnP, ignoring the underlying protocol for
|
||||
// now.
|
||||
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
|
||||
func getUPnPClient(ctx context.Context) (upnpClient, error) {
|
||||
tasks, _ := errgroup.WithContext(ctx)
|
||||
var ip1Clients []*internetgateway2.WANIPConnection1
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip1Clients, _, err = internetgateway2.NewWANIPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
var ip2Clients []*internetgateway2.WANIPConnection2
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip2Clients, _, err = internetgateway2.NewWANIPConnection2Clients()
|
||||
return err
|
||||
})
|
||||
var ppp1Clients []*internetgateway2.WANPPPConnection1
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
|
||||
err := tasks.Wait()
|
||||
|
||||
switch {
|
||||
case len(ip2Clients) > 0:
|
||||
return ip2Clients[0], nil
|
||||
case len(ip1Clients) > 0:
|
||||
return ip1Clients[0], nil
|
||||
case len(ppp1Clients) > 0:
|
||||
return ppp1Clients[0], nil
|
||||
default:
|
||||
// Didn't get any outputs, report if there was an error or nil if
|
||||
// just no clients.
|
||||
return nil, err
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user