Compare commits

...

23 Commits

Author SHA1 Message Date
Brad Fitzpatrick
850a603caa tailcfg: add Hostinfo.BasicallyEqual
Change-Id: I4fd82dea9bb3618f3a93d8b2d2486f3eabbbd915
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-22 12:56:27 -08:00
Brad Fitzpatrick
0aa4c6f147 net/dns/resolver: add debug HTML handler to see what DNS traffic was forwarded
Change-Id: I6b790e92dcc608515ac8b178f2271adc9fd98f78
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-21 14:32:36 -08:00
Brad Fitzpatrick
ae319b4636 wgengine/magicsock: add HTML debug handler to see magicsock state
Change-Id: Ibc46f4e9651e1c86ec6f5d139f5e9bdc7a488415
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-21 14:26:52 -08:00
Brad Fitzpatrick
c7f5bc0f69 wgengine/magicsock: add metrics for sent disco messages
We only tracked the transport type (UDP vs DERP), not what they were.

Change-Id: Ia4430c1c53afd4634e2d9893d96751a885d77955
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-20 09:39:38 -08:00
Brad Fitzpatrick
81bc812402 Dockerfile: remove long-obsolete -tags=xversion
That build tag hasn't been used since 5088af68cf
(June 2nd, 2021, for 1.10.0)

Change-Id: Ib6093a975505339872a0dc663fff9fc09c13bbc0
2021-12-19 18:15:09 -08:00
Brad Fitzpatrick
0848b36dd2 net/dns/resolver: add metrics to PTR lookup misses
Updates tailscale/corp#3326

Change-Id: I58077d889a3b58ef0633267c92ffb265686ce152
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-19 09:50:16 -08:00
Brad Fitzpatrick
39f22a357d net/dns/resolver: send NXDOMAIN to iOS DNS-SD/Bonjour queries
Don't just ignore them. See if this makes them calm down.

Updates #3363

Change-Id: Id1d66308e26660d26719b2538b577522a1e36b63
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 19:24:19 -08:00
Brad Fitzpatrick
394c9de02b net/dns/resolver: add nameFromQuery benchmark
To convince me it's not as alloc-y as it looks.

Change-Id: I503a0cc267268a23d2973dfde9833c420be4e868
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 19:03:45 -08:00
Brad Fitzpatrick
c7052154d5 net/dns/resolver: fix the subject in a func comment
Change-Id: I519268c20dbd2c2da92da565839d3c1c84612dcc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 15:11:01 -08:00
Brad Fitzpatrick
3dedcd1640 logpolicy, ipn/ipnserver: connect to logtail via tailscaled when needed
This is for use by the Windows GUI client to log via when an
exit node is in use, so the logs don't go out via the exit node and
instead go directly, like tailscaled's. The dialer tried to do that
in the unprivileged GUI by binding to a specific interface, but the
"Internet Kill Switch" installed by tailscaled for exit nodes
precludes that from working and instead the GUI fails to dial out.
So, go through tailscaled (with a CONNECT request) instead.

Fixes tailscale/corp#3169

Change-Id: I17a8efdc1d4b8fed53a29d1c19995592b651b215
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-17 14:40:26 -08:00
Brad Fitzpatrick
5a9914a92f wgengine/netstack: don't remove 255.255.255.255/32 from netstack
The intent of the updateIPs code is to add & remove IP addresses
to netstack based on what we get from the netmap.

But netstack itself adds 255.255.255.255/32 apparently and we always
fight it (and it adds it back?). So stop fighting it.

Updates #2642 (maybe fixes? maybe.)

Change-Id: I37cb23f8e3f07a42a1a55a585689ca51c2be7c60
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 14:15:07 -08:00
Joe Tsai
66164b9307 api: document API endpoint to manipulate keys (#3544)
The new /keys endpoint allows you to list API and machine auth keys.
You can also create machine auth key.
It currently does not support creating another API key.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-12-16 13:50:14 -08:00
Brad Fitzpatrick
40e2b312b6 ipn/ipnserver, logpolicy: move Windows disk logging up earlier
This moves the Windows-only initialization of the filelogger into
logpolicy. Previously we only did it when babysitting the tailscaled
subprocess, but this meant that log messages from the service itself
never made it to disk. Examples that weren't logged to disk:

* logtail unable to dial out,
* DNS flush messages from the service
* svc.ChangeRequest messages (#3581)

This is basically the same fix as #3571 but staying in the Logf type,
and avoiding build-tagged file (which wasn't quite a goal, but
happened and seemed nice)

Fixes #3570

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Change-Id: Iacd80c4720b7218365ec80ae143339d030842702
2021-12-16 12:33:04 -08:00
Brad Fitzpatrick
689426d6bc cmd/tailscaled: log Windows service change requests
And add a little comment.

Change-Id: If0bedf8aefd8d528149548fba829e7a9a8b2e114
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 12:24:05 -08:00
Josh Bleecher Snyder
add6dc8ccc ipn/ipnlocal: make TestShrinkDefaultRoute hermetic
Make shrinkDefaultRoute a pure function.
Instead of calling interfaceRoutes, accept that information as parameters.
Hard-code those parameters in TestShrinkDefaultRoute.

Fixes #3580

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-16 11:31:20 -08:00
Jay Stapleton
894693f352 scripts/installer.sh fix APT_SYSTEMCTL_START error
fix error: 'sh: 411: APT_SYSTEMCTL_START: parameter not set' on certain debian distributions

Signed-off-by: Jay Stapleton <jay@tailscale.com>
2021-12-16 13:55:11 -05:00
Josh Bleecher Snyder
4512e213d5 cmd/tailscale: improve ping error message when logged out
Refactor out the pretty status printing code from status, use it in ping.

Fixes #3549

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-16 10:46:18 -08:00
Brad Fitzpatrick
8f43ddf1a2 ipn/ipnlocal, health: populate self node's Online bit in tailscale status
One option was to just hide "offline" in the text output, but that
doesn't fix the JSON output.

The next option was to lie and say it's online in the JSON (which then
fixes the "offline" in the text output).

But instead, this sets the self node's "Online" to whether we're in an
active map poll.

Fixes #3564

Change-Id: I9b379989bd14655198959e37eec39bb570fb814a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 10:14:08 -08:00
Josh Bleecher Snyder
681d4897cc tstest/integration: remove t param in testNode methods
testNodes have a reference to a testing.TB via their env.
Use it instead of making the caller pass theirs.
We did this in some methods but not others; finish the job.
This simplifies the call sites.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 18:06:14 -08:00
Josh Bleecher Snyder
93ae11105d ipn/ipnlocal: clear magicsock's netmap on logout
magicsock was hanging onto its netmap on logout,
which caused tailscale status to display partial
information about a bunch of zombie peers.
After logout, there should be no peers.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 17:00:08 -08:00
Josh Bleecher Snyder
84a1106fa7 tstest/integration: make -verbose-tailscaled pass -verbose=2 to tailscaled
If you're using -verbose-tailscaled, you're doing in-the-weeds debugging,
so you probably want the verbose output.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 17:00:08 -08:00
Brad Fitzpatrick
aac974a5e5 ipn/ipnlocal: deflake (mostly) TestStateMachine
I'm sick of this flaking. Even if this isn't the right fix, it
stops the alert fatigue.

Updates #3020

Change-Id: I4001c127d78f1056302f7741adec34210a72ee61
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-15 13:41:06 -08:00
Brad Fitzpatrick
6590fc3a94 wgengine/netstack: remove some logging on forwarding connections
Change-Id: Ib1165b918cd5da38583f8e7d4be8cda54af3c81d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-15 11:38:25 -08:00
28 changed files with 1127 additions and 132 deletions

View File

@@ -50,7 +50,7 @@ ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \

158
api.md
View File

@@ -23,6 +23,11 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [Keys](#tailnet-keys)
- [GET tailnet keys](#tailnet-keys-get)
- [POST tailnet key](#tailnet-keys-post)
- [GET tailnet key](#tailnet-keys-key-get)
- [DELETE tailnet key](#tailnet-keys-key-delete)
- [DNS](#tailnet-dns)
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
@@ -670,6 +675,159 @@ Response
}
```
<a name=tailnet-keys></a>
### Keys
<a name=tailnet-keys-get></a>
#### `GET /api/v2/tailnet/:tailnet/keys` - list the keys for a tailnet
Returns a list of active keys for a tailnet
for the user who owns the API key used to perform this query.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Returns
Returns a JSON object with the IDs of all active keys.
This includes both API keys and also machine authentication keys.
In the future, this may provide more information about each key than just the ID.
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
-u "tskey-yourapikey123:"
```
Response:
```
{"keys": [
{"id": "kYKVU14CNTRL"},
{"id": "k68VdZ3CNTRL"},
{"id": "kJ9nq43CNTRL"},
{"id": "kkThgj1CNTRL"}
]}
```
<a name=tailnet-keys-post></a>
#### `POST /api/v2/tailnet/:tailnet/keys` - create a new key for a tailnet
Create a new key in a tailnet associated
with the user who owns the API key used to perform this request.
Supply the tailnet in the path.
##### Parameters
###### POST Body
`capabilities` - A mapping of resources to permissible actions.
```
{
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
}
}
}
}
```
##### Returns
Returns a JSON object with the provided capabilities in addition to the
generated key. The key should be recorded and kept safe and secure as it
wields the capabilities specified in the request. The identity of the key
is embedded in the key itself and can be used to perform operations on
the key (e.g., revoking it or retrieving information about it).
The full key can no longer be retrieved by the server.
##### Example
```
echo '{
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
}
}
}
}' | curl -X POST --data-binary @- https://api.tailscale.com/api/v2/tailnet/example.com/keys \
-u "tskey-yourapikey123:" \
-H "Content-Type: application/json" | jsonfmt
```
Response:
```
{
"id": "k123456CNTRL",
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
"created": "2021-12-09T23:22:39Z",
"expires": "2022-03-09T23:22:39Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
}
```
<a name=tailnet-keys-key-get></a>
#### `GET /api/v2/tailnet/:tailnet/keys/:keyid` - get information for a specific key
Returns a JSON object with information about specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
Returns a JSON object with information about the key such as
when it was created and when it expires.
It also lists the capabilities associated with the key.
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
-u "tskey-yourapikey123:"
```
Response:
```
{
"id": "k123456CNTRL",
"created": "2021-12-09T22:13:53Z",
"expires": "2022-03-09T22:13:53Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
}
```
<a name=tailnet-keys-key-delete></a>
#### `DELETE /api/v2/tailnet/:tailnet/keys/:keyid` - delete a specific key
Deletes a specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
This reports status 200 upon success.
##### Example
```
curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
-u "tskey-yourapikey123:"
```
<a name=tailnet-dns></a>
### DNS

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"log"
"net"
"os"
"strings"
"time"
@@ -64,6 +65,16 @@ var pingArgs struct {
}
func runPing(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
c, bc, ctx, cancel := connect(ctx)
defer cancel()

View File

@@ -121,24 +121,10 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
switch st.BackendState {
default:
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
os.Exit(1)
case ipn.Stopped.String():
outln("Tailscale is stopped.")
os.Exit(1)
case ipn.NeedsLogin.String():
outln("Logged out.")
if st.AuthURL != "" {
printf("\nLog in at: %s\n", st.AuthURL)
}
os.Exit(1)
case ipn.NeedsMachineAuth.String():
outln("Machine is not yet authorized by tailnet admin.")
os.Exit(1)
case ipn.Running.String(), ipn.Starting.String():
// Run below.
}
if len(st.Health) > 0 {
@@ -222,6 +208,27 @@ func runStatus(ctx context.Context, args []string) error {
return nil
}
// isRunningOrStarting reports whether st is in state Running or Starting.
// It also returns a description of the status suitable to display to a user.
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
switch st.BackendState {
default:
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
case ipn.Stopped.String():
return "Tailscale is stopped.", false
case ipn.NeedsLogin.String():
s := "Logged out."
if st.AuthURL != "" {
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
}
return s, false
case ipn.NeedsMachineAuth.String():
return "Machine is not yet authorized by tailnet admin.", false
case ipn.Running.String(), ipn.Starting.String():
return st.BackendState, true
}
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {

View File

@@ -181,9 +181,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
tailscale.com/kube from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/logpolicy+
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/filch from tailscale.com/logpolicy

View File

@@ -295,7 +295,6 @@ func run() error {
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()
go runDebugServer(debugMux, args.debug)
}
linkMon, err := monitor.New(logf)
@@ -314,6 +313,14 @@ func run() error {
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
if debugMux != nil {
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
}
go runDebugServer(debugMux, args.debug)
}
ns, err := newNetstack(logf, dialer, e)
if err != nil {

View File

@@ -55,6 +55,11 @@ func isWindowsService() bool {
return v
}
// runWindowsService starts running Tailscale under the Windows
// Service environment.
//
// At this point we're still the parent process that
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
return svc.Run(serviceName, &ipnService{Policy: pol})
}
@@ -93,6 +98,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
select {
case <-doneCh:
case cmd := <-r:
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
switch cmd.Cmd {
case svc.Stop:
cancel()
@@ -109,6 +115,42 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
return false, windows.NO_ERROR
}
func cmdName(c svc.Cmd) string {
switch c {
case svc.Stop:
return "Stop"
case svc.Pause:
return "Pause"
case svc.Continue:
return "Continue"
case svc.Interrogate:
return "Interrogate"
case svc.Shutdown:
return "Shutdown"
case svc.ParamChange:
return "ParamChange"
case svc.NetBindAdd:
return "NetBindAdd"
case svc.NetBindRemove:
return "NetBindRemove"
case svc.NetBindEnable:
return "NetBindEnable"
case svc.NetBindDisable:
return "NetBindDisable"
case svc.DeviceEvent:
return "DeviceEvent"
case svc.HardwareProfileChange:
return "HardwareProfileChange"
case svc.PowerEvent:
return "PowerEvent"
case svc.SessionChange:
return "SessionChange"
case svc.PreShutdown:
return "PreShutdown"
}
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
}
func beWindowsSubprocess() bool {
if beFirewallKillswitch() {
return true

View File

@@ -9,6 +9,7 @@ package health
import (
"errors"
"fmt"
"net/http"
"os"
"runtime"
"sort"
@@ -28,6 +29,8 @@ var (
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
timer *time.Timer
debugHandler = map[string]http.Handler{}
inMapPoll bool
inMapPollSince time.Time
lastMapPollEndedAt time.Time
@@ -116,6 +119,18 @@ func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
func RegisterDebugHandler(typ string, h http.Handler) {
mu.Lock()
defer mu.Unlock()
debugHandler[typ] = h
}
func DebugHandler(typ string) http.Handler {
mu.Lock()
defer mu.Unlock()
return debugHandler[typ]
}
func get(key Subsystem) error {
mu.Lock()
defer mu.Unlock()
@@ -168,7 +183,8 @@ func GotStreamedMapResponse() {
selfCheckLocked()
}
// SetInPollNetMap records that we're in
// SetInPollNetMap records whether the client has an open
// HTTP long poll open to the control plane.
func SetInPollNetMap(v bool) {
mu.Lock()
defer mu.Unlock()
@@ -183,6 +199,14 @@ func SetInPollNetMap(v bool) {
}
}
// GetInPollNetMap reports whether the client has an open
// HTTP long poll open to the control plane.
func GetInPollNetMap() bool {
mu.Lock()
defer mu.Unlock()
return inMapPoll
}
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
func SetMagicSockDERPHome(region int) {
mu.Lock()

View File

@@ -380,6 +380,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
}
})
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
ss.Online = health.GetInPollNetMap()
if b.netMap != nil {
ss.HostName = b.netMap.Hostinfo.Hostname
ss.DNSName = b.netMap.Name
@@ -536,6 +537,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Since st.NetMap==nil means "netmap is unchanged", there is
// no other way to represent this change.
b.setNetMapLocked(nil)
b.e.SetNetworkMap(new(netmap.NetworkMap))
}
prefs := b.prefs
@@ -1018,7 +1020,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
// wifi": you get internet access, but to additionally
// get LAN access the LAN(s) need to be offered
// explicitly as well.
s, err := shrinkDefaultRoute(r)
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
b.logf("getting local interface routes: %v", err)
continue
}
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
if err != nil {
b.logf("computing default route filter: %v", err)
continue
@@ -1162,17 +1169,14 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
}
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
// minus those in removeFromDefaultRoute and local interface subnets.
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
interfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
return nil, err
}
// minus those in removeFromDefaultRoute and localInterfaceRoutes,
// plus the IPs in hostIPs.
func shrinkDefaultRoute(route netaddr.IPPrefix, localInterfaceRoutes *netaddr.IPSet, hostIPs []netaddr.IP) (*netaddr.IPSet, error) {
var b netaddr.IPSetBuilder
// Add the default route.
b.AddPrefix(route)
// Remove the local interface routes.
b.RemoveSet(interfaceRoutes)
b.RemoveSet(localInterfaceRoutes)
// Having removed all the LAN subnets, re-add the hosts's own
// IPs. It's fine for clients to connect to an exit node's public

View File

@@ -178,9 +178,31 @@ func TestShrinkDefaultRoute(t *testing.T) {
},
}
// Construct a fake local network environment to make this test hermetic.
// localInterfaceRoutes and hostIPs would normally come from calling interfaceRoutes,
// and localAddresses would normally come from calling interfaces.LocalAddresses.
var b netaddr.IPSetBuilder
for _, c := range []string{"127.0.0.0/8", "192.168.9.0/24", "fe80::/32"} {
p := netaddr.MustParseIPPrefix(c)
b.AddPrefix(p)
}
localInterfaceRoutes, err := b.IPSet()
if err != nil {
t.Fatal(err)
}
hostIPs := []netaddr.IP{
netaddr.MustParseIP("127.0.0.1"),
netaddr.MustParseIP("192.168.9.39"),
netaddr.MustParseIP("fe80::1"),
netaddr.MustParseIP("fe80::437d:feff:feca:49a7"),
}
localAddresses := []netaddr.IP{
netaddr.MustParseIP("192.168.9.39"),
}
for _, test := range tests {
def := netaddr.MustParseIPPrefix(test.route)
got, err := shrinkDefaultRoute(def)
got, err := shrinkDefaultRoute(def, localInterfaceRoutes, hostIPs)
if err != nil {
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
}
@@ -194,11 +216,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
}
}
ips, _, err := interfaces.LocalAddresses()
if err != nil {
t.Fatal(err)
}
for _, ip := range ips {
for _, ip := range localAddresses {
want := test.localIPFn(ip)
if gotContains := got.Contains(ip); gotContains != want {
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)

View File

@@ -32,6 +32,7 @@ import (
"golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
@@ -553,6 +554,12 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
case "/v0/magicsock":
h.handleServeMagicsock(w, r)
return
case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r)
return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html>
@@ -781,6 +788,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(data)
}
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
eng := h.ps.b.e
if ig, ok := eng.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
mc.ServeHTTPDebug(w, r)
return
}
}
http.Error(w, "miswired", 500)
}
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
@@ -790,6 +812,19 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
dh := health.DebugHandler("dnsfwd")
if dh == nil {
http.Error(w, "not wired up", 500)
return
}
dh.ServeHTTP(w, r)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it

View File

@@ -87,8 +87,9 @@ func (nt *notifyThrottler) drain(count int) []ipn.Notify {
type mockControl struct {
tb testing.TB
opts controlclient.Options
logf logger.Logf
logfActual logger.Logf
statusFunc func(controlclient.Status)
preventLog syncs.AtomicBool
mu sync.Mutex
calls []string
@@ -104,6 +105,13 @@ func newMockControl(tb testing.TB) *mockControl {
}
}
func (cc *mockControl) logf(format string, args ...interface{}) {
if cc.preventLog.Get() || cc.logfActual == nil {
return
}
cc.logfActual(format, args...)
}
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
cc.statusFunc = fn
}
@@ -284,6 +292,7 @@ func TestStateMachine(t *testing.T) {
t.Cleanup(e.Close)
cc := newMockControl(t)
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
b, err := NewLocalBackend(logf, "logid", store, nil, e)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
@@ -291,7 +300,7 @@ func TestStateMachine(t *testing.T) {
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock()
cc.opts = opts
cc.logf = opts.Logf
cc.logfActual = opts.Logf
cc.authBlocked = true
cc.persist = cc.opts.Persist
cc.mu.Unlock()
@@ -305,6 +314,9 @@ func TestStateMachine(t *testing.T) {
notifies.expect(0)
b.SetNotifyCallback(func(n ipn.Notify) {
if cc.preventLog.Get() {
return
}
if n.State != nil ||
n.Prefs != nil ||
n.BrowseToURL != nil ||
@@ -315,6 +327,7 @@ func TestStateMachine(t *testing.T) {
logf("\n(ignored) %v\n\n", n)
}
})
t.Cleanup(func() { b.SetNotifyCallback(nil) }) // hacky way to pacify issue 3020
// Check that it hasn't called us right away.
// The state machine should be idle until we call Start().
@@ -948,7 +961,7 @@ func TestWGEngineStatusRace(t *testing.T) {
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.logf = opts.Logf
cc.logfActual = opts.Logf
return cc, nil
})

View File

@@ -0,0 +1,74 @@
// 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 ipnserver
import (
"bufio"
"context"
"io"
"net"
"net/http"
"time"
"tailscale.com/logpolicy"
"tailscale.com/types/logger"
)
// handleProxyConnectConn handles a CONNECT request to
// log.tailscale.io (or whatever the configured log server is). This
// is intended for use by the Windows GUI client to log via when an
// exit node is in use, so the logs don't go out via the exit node and
// instead go directly, like tailscaled's. The dialer tried to do that
// in the unprivileged GUI by binding to a specific interface, but the
// "Internet Kill Switch" installed by tailscaled for exit nodes
// precludes that from working and instead the GUI fails to dial out.
// So, go through tailscaled (with a CONNECT request) instead.
func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) {
defer c.Close()
c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers
req, err := http.ReadRequest(br)
if err != nil {
logf("ReadRequest: %v", err)
return
}
c.SetReadDeadline(time.Time{})
if req.Method != "CONNECT" {
logf("ReadRequest: unexpected method %q, not CONNECT", req.Method)
return
}
hostPort := req.RequestURI
logHost := logpolicy.LogHost()
allowed := net.JoinHostPort(logHost, "443")
if hostPort != allowed {
logf("invalid CONNECT target %q; want %q", hostPort, allowed)
io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n")
return
}
tr := logpolicy.NewLogtailTransport(logHost)
back, err := tr.DialContext(ctx, "tcp", hostPort)
if err != nil {
logf("error CONNECT dialing %v: %v", hostPort, err)
io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n")
return
}
defer back.Close()
io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n")
errc := make(chan error, 2)
go func() {
_, err := io.Copy(c, back)
errc <- err
}()
go func() {
_, err := io.Copy(back, br)
errc <- err
}()
<-errc
}

View File

@@ -35,7 +35,6 @@ import (
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/ipn/store/aws"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/net/tsdial"
@@ -239,12 +238,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
}
// bufferIsConnect reports whether br looks like it's likely an HTTP
// CONNECT request.
//
// Invariant: br has already had at least 4 bytes Peek'ed.
func bufferIsConnect(br *bufio.Reader) bool {
peek, _ := br.Peek(br.Buffered())
return mem.HasPrefix(mem.B(peek), mem.S("CONN"))
}
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request.
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
br.Peek(4)
c.SetReadDeadline(time.Time{})
// Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn)
if bufferIsConnect(br) {
s.handleProxyConnectConn(ctx, br, c, logf)
return
}
isHTTPReq := bufferHasHTTPRequest(br)
ci, err := s.addConn(c, isHTTPReq)
@@ -869,14 +884,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
panic("cannot determine executable: " + err.Error())
}
if runtime.GOOS == "windows" {
if len(args) != 2 && args[0] != "/subproc" {
panic(fmt.Sprintf("unexpected arguments %q", args))
}
logID := args[1]
logf = filelogger.New("tailscale-service", logID, logf)
}
var proc struct {
mu sync.Mutex
p *os.Process

View File

@@ -8,11 +8,14 @@
package logpolicy
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
@@ -29,6 +32,7 @@ import (
"golang.org/x/term"
"tailscale.com/atomicfile"
"tailscale.com/log/filelogger"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/net/dnscache"
@@ -38,6 +42,7 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -65,6 +70,15 @@ func getLogTarget() string {
return getLogTargetOnce.v
}
// LogHost returns the hostname only (without port) of the configured
// logtail server, or the default.
func LogHost() string {
if v := getLogTarget(); v != "" {
return v
}
return logtail.DefaultHost
}
// Config represents an instance of logs in a collection.
type Config struct {
Collection string
@@ -524,8 +538,20 @@ func New(collection string) *Policy {
}
}
lw := logtail.NewLogger(c, log.Printf)
var logOutput io.Writer = lw
if runtime.GOOS == "windows" && c.Collection == logtail.CollectionNode {
logID := newc.PublicID.String()
exe, _ := os.Executable()
if strings.EqualFold(filepath.Base(exe), "tailscaled.exe") {
diskLogf := filelogger.New("tailscale-service", logID, lw.Logf)
logOutput = logger.FuncWriter(diskLogf)
}
}
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.SetOutput(logOutput)
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long,
@@ -602,6 +628,24 @@ func NewLogtailTransport(host string) *http.Transport {
return c, nil
}
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil {
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
br := bufio.NewReader(c)
res, err := http.ReadResponse(br, nil)
if err == nil && res.StatusCode != 200 {
err = errors.New(res.Status)
}
if err != nil {
log.Printf("logtail: CONNECT response from tailscaled: %v", err)
c.Close()
} else {
log.Printf("logtail: connected via tailscaled")
return c, nil
}
}
}
// If we failed to dial, try again with bootstrap DNS.
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
dnsCache := &dnscache.Resolver{

View File

@@ -523,6 +523,11 @@ func (l *Logger) encode(buf []byte) []byte {
return b
}
// Logf logs to l using the provided fmt-style format and optional arguments.
func (l *Logger) Logf(format string, args ...interface{}) {
fmt.Fprintf(l, format, args...)
}
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,

78
net/dns/resolver/debug.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright (c) 2020 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 resolver
import (
"fmt"
"html"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"tailscale.com/health"
)
func init() {
health.RegisterDebugHandler("dnsfwd", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, _ := strconv.Atoi(r.FormValue("n"))
if n <= 0 {
n = 100
} else if n > 10000 {
n = 10000
}
fl, ok := fwdLogAtomic.Load().(*fwdLog)
if !ok || n != len(fl.ent) {
fl = &fwdLog{ent: make([]fwdLogEntry, n)}
fwdLogAtomic.Store(fl)
}
fl.ServeHTTP(w, r)
}))
}
var fwdLogAtomic atomic.Value // of *fwdLog
type fwdLog struct {
mu sync.Mutex
pos int // ent[pos] is next entry
ent []fwdLogEntry
}
type fwdLogEntry struct {
Domain string
Time time.Time
}
func (fl *fwdLog) addName(name string) {
if fl == nil {
return
}
fl.mu.Lock()
defer fl.mu.Unlock()
if len(fl.ent) == 0 {
return
}
fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()}
fl.pos++
if fl.pos == len(fl.ent) {
fl.pos = 0
}
}
func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fl.mu.Lock()
defer fl.mu.Unlock()
fmt.Fprintf(w, "<html><h1>DNS forwards</h1>")
now := time.Now()
for i := 0; i < len(fl.ent); i++ {
ent := fl.ent[(i+fl.pos)%len(fl.ent)]
if ent.Domain == "" {
continue
}
fmt.Fprintf(w, "%v ago: %v<br>\n", now.Sub(ent.Time).Round(time.Second), html.EscapeString(ent.Domain))
}
}

View File

@@ -576,8 +576,8 @@ func (f *forwarder) forward(query packet) error {
return f.forwardWithDestChan(ctx, query, f.responses)
}
// forward forwards the query to all upstream nameservers and waits
// for the first response.
// forwardWithDestChan forwards the query to all upstream nameservers
// and waits for the first response.
//
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
@@ -598,7 +598,21 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
// out, playing on Sonos still works.
if hasRDNSBonjourPrefix(domain) {
metricDNSFwdDropBonjour.Add(1)
return nil
res, err := nxDomainResponse(query)
if err != nil {
f.logf("error parsing bonjour query: %v", err)
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case responseChan <- res:
return nil
}
}
if fl, ok := fwdLogAtomic.Load().(*fwdLog); ok {
fl.addName(string(domain))
}
clampEDNSSize(query.bs, maxResponseBytes)
@@ -696,6 +710,28 @@ func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
return dnsname.ToFQDN(rawNameToLower(n))
}
// nxDomainResponse returns an NXDomain DNS reply for the provided request.
func nxDomainResponse(req packet) (res packet, err error) {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
if err := p.parseQuery(req.bs); err != nil {
return packet{}, err
}
h := p.Header
h.Response = true
h.RecursionAvailable = h.RecursionDesired
h.RCode = dns.RCodeNameError
b := dns.NewBuilder(nil, h)
// TODO(bradfitz): should we add an SOA record in the Authority
// section too? (for the nxdomain negative caching TTL)
// For which zone? Does iOS care?
res.bs, err = b.Finish()
res.addr = req.addr
return res, err
}
// closePool is a dynamic set of io.Closers to close as a group.
// It's intended to be Closed at most once.
//

View File

@@ -168,3 +168,25 @@ func TestMaxDoHInFlight(t *testing.T) {
})
}
}
func BenchmarkNameFromQuery(b *testing.B) {
builder := dns.NewBuilder(nil, dns.Header{})
builder.StartQuestions()
builder.Question(dns.Question{
Name: dns.MustNewName("foo.example."),
Type: dns.TypeA,
Class: dns.ClassINET,
})
msg, err := builder.Finish()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := nameFromQuery(msg)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -1087,11 +1087,13 @@ func rdnsNameToIPv6(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
// It is assumed that resp.Question is populated by respond before this is called.
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) {
metricDNSReverseMissBonjour.Add(1)
return nil, errNotOurName
}
resp.Name, resp.Header.RCode = r.resolveLocalReverse(name)
if resp.Header.RCode == dns.RCodeRefused {
metricDNSReverseMissOther.Add(1)
return nil, errNotOurName
}
@@ -1235,4 +1237,7 @@ var (
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
metricDNSReverseMissBonjour = clientmetric.NewCounter("dns_reverse_miss_bonjour")
metricDNSReverseMissOther = clientmetric.NewCounter("dns_reverse_miss_other")
)

View File

@@ -24,6 +24,7 @@ main() {
VERSION=""
PACKAGETYPE=""
APT_KEY_TYPE="" # Only for apt-based distros
APT_SYSTEMCTL_START=false # Only needs to be true for Kali
if [ -f /etc/os-release ]; then
# /etc/os-release populates a number of shell variables. We care about the following:

View File

@@ -571,12 +571,31 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
if h == nil && h2 == nil {
return true
}
if (h == nil) != (h2 == nil) {
if h == nil || h2 == nil {
return false
}
return reflect.DeepEqual(h, h2)
}
// BasicallyEqual reports whether h and h2 are equal other than the
// NetInfo DERP latency timing. (see NetInfo.BasicallyEqual).
func (h *Hostinfo) BasicallyEqual(h2 *Hostinfo) bool {
if h == nil && h2 == nil {
return true
}
if h == nil || h2 == nil {
return false
}
a := *h
b := *h2
if !a.NetInfo.BasicallyEqual(b.NetInfo) {
return false
}
a.NetInfo = nil
b.NetInfo = nil
return a.Equal(&b)
}
// SignatureType specifies a scheme for signing RegisterRequest messages. It
// specifies the crypto algorithms to use, the contents of what is signed, and
// any other relevant details. Historically, requests were unsigned so the zero

View File

@@ -190,6 +190,75 @@ func TestHostinfoEqual(t *testing.T) {
}
}
func TestHostinfoBasicallyEqual(t *testing.T) {
tests := []struct {
a, b *Hostinfo
want bool
}{
{
want: true,
},
{
a: new(Hostinfo),
b: new(Hostinfo),
want: true,
},
{
a: &Hostinfo{},
b: &Hostinfo{
NetInfo: &NetInfo{},
},
want: false, // one's nil, the other's not
},
{
a: &Hostinfo{
NetInfo: &NetInfo{},
},
b: &Hostinfo{
NetInfo: &NetInfo{},
},
want: true,
},
{
a: &Hostinfo{
NetInfo: &NetInfo{},
},
b: &Hostinfo{
NetInfo: &NetInfo{
DERPLatency: map[string]float64{ // ignored
"1": 1.0,
"2": 2.0,
},
},
},
want: true,
},
{
a: &Hostinfo{
NetInfo: &NetInfo{
PreferredDERP: 1,
},
},
b: &Hostinfo{
NetInfo: &NetInfo{
PreferredDERP: 2, // differs
DERPLatency: map[string]float64{ // ignored
"1": 1.0,
"2": 2.0,
},
},
},
want: false,
},
}
for i, tt := range tests {
got := tt.a.BasicallyEqual(tt.b)
if got != tt.want {
t.Errorf("%d. BasicallyEqual = %v; want %v", i, got, tt.want)
}
}
}
func TestNodeEqual(t *testing.T) {
nodeHandles := []string{
"ID", "StableID", "Name", "User", "Sharer",

View File

@@ -68,12 +68,12 @@ func TestOneNodeUpNoAuth(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
@@ -85,10 +85,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
nodes := env.Control.AllNodes()
if len(nodes) != 1 {
@@ -103,7 +103,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
cancel()
env.Control.SetExpireAllNodes(true)
n1.AwaitNeedsLogin(t)
n1.AwaitNeedsLogin()
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
@@ -111,7 +111,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
cancel()
env.Control.SetExpireAllNodes(false)
n1.AwaitRunning(t)
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
@@ -152,14 +152,14 @@ func TestStateSavedOnStart(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
p1 := n1.diskPrefs(t)
p1 := n1.diskPrefs()
t.Logf("Prefs1: %v", p1.Pretty())
// Bring it down, to prevent an EditPrefs call in the
@@ -172,7 +172,7 @@ func TestStateSavedOnStart(t *testing.T) {
t.Fatalf("up: %v", err)
}
p2 := n1.diskPrefs(t)
p2 := n1.diskPrefs()
if pretty := p1.Pretty(); pretty == p2.Pretty() {
t.Errorf("Prefs didn't change on disk after 'up', still: %s", pretty)
}
@@ -190,11 +190,11 @@ func TestOneNodeUpAuth(t *testing.T) {
}))
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
st := n1.MustStatus(t)
st := n1.MustStatus()
t.Logf("Status: %s", st.BackendState)
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
@@ -215,9 +215,9 @@ func TestOneNodeUpAuth(t *testing.T) {
if err := cmd.Run(); err != nil {
t.Fatalf("up: %v", err)
}
t.Logf("Got IP: %v", n1.AwaitIP(t))
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning(t)
n1.AwaitRunning()
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
t.Errorf("Auth URLs completed = %d; want 1", n)
@@ -233,26 +233,26 @@ func TestTwoNodes(t *testing.T) {
// Create two nodes:
n1 := newTestNode(t, env)
n1SocksAddrCh := n1.socks5AddrChan()
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n2 := newTestNode(t, env)
n2SocksAddrCh := n2.socks5AddrChan()
d2 := n2.StartDaemon(t)
d2 := n2.StartDaemon()
n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh)
n1Socks := n1.AwaitSocksAddr(n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(n2SocksAddrCh)
t.Logf("node1 SOCKS5 addr: %v", n1Socks)
t.Logf("node2 SOCKS5 addr: %v", n2Socks)
n1.AwaitListening(t)
n2.AwaitListening(t)
n1.AwaitListening()
n2.AwaitListening()
n1.MustUp()
n2.MustUp()
n1.AwaitRunning(t)
n2.AwaitRunning(t)
n1.AwaitRunning()
n2.AwaitRunning()
if err := tstest.WaitFor(2*time.Second, func() error {
st := n1.MustStatus(t)
st := n1.MustStatus()
if len(st.Peer) == 0 {
return errors.New("no peers")
}
@@ -276,11 +276,11 @@ func TestNodeAddressIPFields(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
testNodes := env.Control.AllNodes()
@@ -302,11 +302,11 @@ func TestAddPingRequest(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.StartDaemon(t)
n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
gotPing := make(chan bool, 1)
waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -357,28 +357,28 @@ func TestNoControlConnWhenDown(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
// Come up the first time.
n1.MustUp()
ip1 := n1.AwaitIP(t)
n1.AwaitRunning(t)
ip1 := n1.AwaitIP()
n1.AwaitRunning()
// Then bring it down and stop the daemon.
n1.MustDown()
d1.MustCleanShutdown(t)
env.LogCatcher.Reset()
d2 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d2 := n1.StartDaemon()
n1.AwaitResponding()
st := n1.MustStatus(t)
st := n1.MustStatus()
if got, want := st.BackendState, "Stopped"; got != want {
t.Fatalf("after restart, state = %q; want %q", got, want)
}
ip2 := n1.AwaitIP(t)
ip2 := n1.AwaitIP()
if ip1 != ip2 {
t.Errorf("IPs different: %q vs %q", ip1, ip2)
}
@@ -399,16 +399,66 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
n1 := newTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
n1.AwaitResponding(t)
d1 := n1.StartDaemonAsIPNGOOS("windows")
n1.AwaitResponding()
n1.MustUp("--unattended")
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestLogoutRemovesAllPeers(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
// Spin up some nodes.
nodes := make([]*testNode, 2)
for i := range nodes {
nodes[i] = newTestNode(t, env)
nodes[i].StartDaemon()
nodes[i].AwaitResponding()
nodes[i].MustUp()
nodes[i].AwaitIP()
nodes[i].AwaitRunning()
}
// Make every node ping every other node.
// This makes sure magicsock is fully populated.
for i := range nodes {
for j := range nodes {
if i <= j {
continue
}
if err := tstest.WaitFor(20*time.Second, func() error {
return nodes[i].Ping(nodes[j])
}); err != nil {
t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP(), nodes[j].AwaitIP(), err)
}
}
}
// wantNode0PeerCount waits until node[0] status includes exactly want peers.
wantNode0PeerCount := func(want int) {
if err := tstest.WaitFor(20*time.Second, func() error {
s := nodes[0].MustStatus()
if peers := s.Peers(); len(peers) != want {
return fmt.Errorf("want %d peer(s) in status, got %v", want, peers)
}
return nil
}); err != nil {
t.Fatal(err)
}
}
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers
nodes[0].MustLogOut()
wantNode0PeerCount(0) // node[0] is logged out, so it should not have any peers
nodes[0].MustUp()
nodes[0].AwaitIP()
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers again
}
// testEnv contains the test environment (set of servers) used by one
// or more nodes.
type testEnv struct {
@@ -508,7 +558,8 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
}
}
func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
func (n *testNode) diskPrefs() *ipn.Prefs {
t := n.env.t
t.Helper()
if _, err := ioutil.ReadFile(n.stateFile); err != nil {
t.Fatalf("reading prefs: %v", err)
@@ -530,11 +581,12 @@ func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
// AwaitResponding waits for n's tailscaled to be up enough to be
// responding, but doesn't wait for any particular state.
func (n *testNode) AwaitResponding(t testing.TB) {
func (n *testNode) AwaitResponding() {
t := n.env.t
t.Helper()
n.AwaitListening(t)
n.AwaitListening()
st := n.MustStatus(t)
st := n.MustStatus()
t.Logf("Status: %s", st.BackendState)
if err := tstest.WaitFor(20*time.Second, func() error {
@@ -575,7 +627,8 @@ func (n *testNode) socks5AddrChan() <-chan string {
return ch
}
func (n *testNode) AwaitSocksAddr(t testing.TB, ch <-chan string) string {
func (n *testNode) AwaitSocksAddr(ch <-chan string) string {
t := n.env.t
t.Helper()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
@@ -644,17 +697,21 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
// StartDaemon starts the node's tailscaled, failing if it fails to start.
// StartDaemon ensures that the process will exit when the test completes.
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
return n.StartDaemonAsIPNGOOS(t, runtime.GOOS)
func (n *testNode) StartDaemon() *Daemon {
return n.StartDaemonAsIPNGOOS(runtime.GOOS)
}
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
t := n.env.t
cmd := exec.Command(n.env.daemon,
"--tun=userspace-networking",
"--state="+n.stateFile,
"--socket="+n.sockFile,
"--socks5-server=localhost:0",
)
if *verboseTailscaled {
cmd.Args = append(cmd.Args, "-verbose=2")
}
cmd.Env = append(os.Environ(),
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
"HTTP_PROXY="+n.env.TrafficTrapServer.URL,
@@ -700,9 +757,25 @@ func (n *testNode) MustDown() {
}
}
func (n *testNode) MustLogOut() {
t := n.env.t
t.Logf("Running logout ...")
if err := n.Tailscale("logout").Run(); err != nil {
t.Fatalf("logout: %v", err)
}
}
func (n *testNode) Ping(otherNode *testNode) error {
t := n.env.t
ip := otherNode.AwaitIP().String()
t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP())
return n.Tailscale("ping", ip).Run()
}
// AwaitListening waits for the tailscaled to be serving local clients
// over its localhost IPC mechanism. (Unix socket, etc)
func (n *testNode) AwaitListening(t testing.TB) {
func (n *testNode) AwaitListening() {
t := n.env.t
s := safesocket.DefaultConnectionStrategy(n.sockFile)
s.UseFallback(false) // connect only to the tailscaled that we started
if err := tstest.WaitFor(20*time.Second, func() (err error) {
@@ -717,7 +790,8 @@ func (n *testNode) AwaitListening(t testing.TB) {
}
}
func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
func (n *testNode) AwaitIPs() []netaddr.IP {
t := n.env.t
t.Helper()
var addrs []netaddr.IP
if err := tstest.WaitFor(20*time.Second, func() error {
@@ -750,14 +824,16 @@ func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
}
// AwaitIP returns the IP address of n.
func (n *testNode) AwaitIP(t testing.TB) netaddr.IP {
func (n *testNode) AwaitIP() netaddr.IP {
t := n.env.t
t.Helper()
ips := n.AwaitIPs(t)
ips := n.AwaitIPs()
return ips[0]
}
// AwaitRunning waits for n to reach the IPN state "Running".
func (n *testNode) AwaitRunning(t testing.TB) {
func (n *testNode) AwaitRunning() {
t := n.env.t
t.Helper()
if err := tstest.WaitFor(20*time.Second, func() error {
st, err := n.Status()
@@ -774,7 +850,8 @@ func (n *testNode) AwaitRunning(t testing.TB) {
}
// AwaitNeedsLogin waits for n to reach the IPN state "NeedsLogin".
func (n *testNode) AwaitNeedsLogin(t testing.TB) {
func (n *testNode) AwaitNeedsLogin() {
t := n.env.t
t.Helper()
if err := tstest.WaitFor(20*time.Second, func() error {
st, err := n.Status()
@@ -822,7 +899,8 @@ func (n *testNode) Status() (*ipnstate.Status, error) {
return st, nil
}
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
func (n *testNode) MustStatus() *ipnstate.Status {
tb := n.env.t
tb.Helper()
st, err := n.Status()
if err != nil {

View File

@@ -62,3 +62,13 @@ func IsMacSysExt() bool {
isMacSysExt.Store(v)
return v
}
// IsWindowsGUI reports whether the current process is the Windows GUI.
func IsWindowsGUI() bool {
if runtime.GOOS != "windows" {
return false
}
exe, _ := os.Executable()
exe = filepath.Base(exe)
return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn")
}

View File

@@ -0,0 +1,202 @@
// 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 magicsock
import (
"fmt"
"html"
"io"
"net/http"
"sort"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
)
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
//
// It's accessible either from tailscaled's debug port (at
// /debug/magicsock) or via peerapi to a peer that's owned by the same
// user (so they can e.g. inspect their phones).
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "<h1>magicsock</h1>")
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
if c.derpMap != nil {
type D struct {
regionID int
lastWrite time.Time
createTime time.Time
}
ent := make([]D, 0, len(c.activeDerp))
for rid, ad := range c.activeDerp {
ent = append(ent, D{
regionID: rid,
lastWrite: *ad.lastWrite,
createTime: ad.createTime,
})
}
sort.Slice(ent, func(i, j int) bool {
return ent[i].regionID < ent[j].regionID
})
for _, e := range ent {
r, ok := c.derpMap.Regions[e.regionID]
if !ok {
continue
}
home := ""
if e.regionID == c.myDerp {
home = "🏠"
}
fmt.Fprintf(w, "<li>%s %d - %v: created %v ago, write %v ago</li>\n",
home, e.regionID, html.EscapeString(r.RegionCode),
now.Sub(e.createTime).Round(time.Second),
now.Sub(e.lastWrite).Round(time.Second),
)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
{
type kv struct {
ipp netaddr.IPPort
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byIPPort))
for k, v := range c.peerMap.byIPPort {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
for _, e := range ent {
ep := e.pi.ep
shortStr := ep.publicKey.ShortString()
fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
{
type kv struct {
pub key.NodePublic
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
for k, v := range c.peerMap.byNodeKey {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
peers := map[key.NodePublic]*tailcfg.Node{}
if c.netMap != nil {
for _, p := range c.netMap.Peers {
peers[p.Key] = p
}
}
for _, e := range ent {
ep := e.pi.ep
shortStr := e.pub.ShortString()
name := peerDebugName(peers[e.pub])
fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\n",
strings.Trim(shortStr, "[]"),
strings.Trim(shortStr, "[]"),
shortStr,
html.EscapeString(name))
printEndpointHTML(w, ep)
}
}
}
func printEndpointHTML(w io.Writer, ep *endpoint) {
lastRecv := ep.lastRecv.LoadAtomic()
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.lastSend == 0 && lastRecv == 0 {
return // no activity ever
}
now := time.Now()
mnow := mono.Now()
fmtMono := func(m mono.Time) string {
if m == 0 {
return "-"
}
return mnow.Sub(m).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSend))
fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\n", fmtMono(ep.lastFullPing))
eps := make([]netaddr.IPPort, 0, len(ep.endpointState))
for ipp := range ep.endpointState {
eps = append(eps, ipp)
}
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
io.WriteString(w, "<p>Endpoints:</p><ul>")
for _, ipp := range eps {
s := ep.endpointState[ipp]
if ipp == ep.bestAddr.IPPort {
fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
} else {
fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
}
fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
if s.lastGotPing.IsZero() {
fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
} else {
fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
}
fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\n", s.callMeMaybeTime)
for i := range s.recentPongs {
if i == 5 {
break
}
pos := (int(s.recentPong) - i) % len(s.recentPongs)
pr := s.recentPongs[pos]
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
pr.from, pr.pongSrc)
}
fmt.Fprintf(w, "</ul></li>\n")
}
io.WriteString(w, "</ul>")
}
func peerDebugName(p *tailcfg.Node) string {
if p == nil {
return ""
}
n := p.Name
if i := strings.Index(n, "."); i != -1 {
return n[:i]
}
return p.Hostinfo.Hostname
}
func ipPortLess(a, b netaddr.IPPort) bool {
if v := a.IP().Compare(b.IP()); v != 0 {
return v < 0
}
return a.Port() < b.Port()
}

View File

@@ -1801,6 +1801,14 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey key.NodePublic, dstDi
} else {
metricSentDiscoUDP.Add(1)
}
switch m.(type) {
case *disco.Ping:
metricSentDiscoPing.Add(1)
case *disco.Pong:
metricSentDiscoPong.Add(1)
case *disco.CallMeMaybe:
metricSentDiscoCallMeMaybe.Add(1)
}
} else if err == nil {
// Can't send. (e.g. no IPv6 locally)
} else {
@@ -4047,13 +4055,16 @@ var (
metricRecvDataIPv6 = clientmetric.NewCounter("magicsock_recv_data_ipv6")
// Disco packets
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")

View File

@@ -229,23 +229,34 @@ func ipPrefixToAddressWithPrefix(ipp netaddr.IPPrefix) tcpip.AddressWithPrefix {
}
}
var v4broadcast = netaddr.IPv4(255, 255, 255, 255)
func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nm.Addresses))
oldIPs := make(map[tcpip.AddressWithPrefix]bool)
for _, protocolAddr := range ns.ipstack.AllAddresses()[nicID] {
oldIPs[protocolAddr.AddressWithPrefix] = true
ap := protocolAddr.AddressWithPrefix
ip := netaddrIPFromNetstackIP(ap.Address)
if ip == v4broadcast && ap.PrefixLen == 32 {
// Don't delete this one later. It seems to be important.
// Related to Issue 2642? Likely.
continue
}
oldIPs[ap] = true
}
newIPs := make(map[tcpip.AddressWithPrefix]bool)
isAddr := map[netaddr.IPPrefix]bool{}
for _, ipp := range nm.SelfNode.Addresses {
isAddr[ipp] = true
}
for _, ipp := range nm.SelfNode.AllowedIPs {
local := isAddr[ipp]
if local && ns.ProcessLocalIPs || !local && ns.ProcessSubnets {
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
if nm.SelfNode != nil {
for _, ipp := range nm.SelfNode.Addresses {
isAddr[ipp] = true
}
for _, ipp := range nm.SelfNode.AllowedIPs {
local := isAddr[ipp]
if local && ns.ProcessLocalIPs || !local && ns.ProcessSubnets {
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
}
}
}
@@ -533,7 +544,9 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netaddr.IP, wq *waiter.Queue, dialAddr netaddr.IPPort) {
defer client.Close()
dialAddrStr := dialAddr.String()
ns.logf("[v2] netstack: forwarding incoming connection to %s", dialAddrStr)
if debugNetstack {
ns.logf("[v2] netstack: forwarding incoming connection to %s", dialAddrStr)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -609,7 +622,9 @@ func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) {
// proxy to it directly.
func (ns *Impl) forwardUDP(client *gonet.UDPConn, wq *waiter.Queue, clientAddr, dstAddr netaddr.IPPort) {
port, srcPort := dstAddr.Port(), clientAddr.Port()
ns.logf("[v2] netstack: forwarding incoming UDP connection on port %v", port)
if debugNetstack {
ns.logf("[v2] netstack: forwarding incoming UDP connection on port %v", port)
}
var backendListenAddr *net.UDPAddr
var backendRemoteAddr *net.UDPAddr