Compare commits

..

37 Commits

Author SHA1 Message Date
Andrew Lytvynov
776ab357b1 cmd/tailscale: add --json-docs flag
This prints all command and flag docs as JSON. To be used for generating
the contents of https://tailscale.com/kb/1080/cli.

Updates https://github.com/tailscale/tailscale-www/issues/4722
2024-08-08 08:06:18 -07:00
Jordan Whited
a93dc6cdb1 wgengine/magicsock: refactor batchingUDPConn to batchingConn interface (#13042)
This commit adds a batchingConn interface, and renames batchingUDPConn
to linuxBatchingConn. tryUpgradeToBatchingConn() may return a platform-
specific implementation of batchingConn. So far only a Linux
implementation of this interface exists, but this refactor is being
done in anticipation of a Windows implementation.

Updates tailscale/corp#21874

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-08-06 09:00:28 -07:00
Anton Tolchanov
7bac5dffcb control/controlhttp: extract the last network connection
The same context we use for the HTTP request here might be re-used by
the dialer, which could result in `GotConn` being called multiple times.
We only care about the last one.

Fixes #13009

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-06 11:42:06 +01:00
Anton Tolchanov
b3fc345aba cmd/derpprobe: use a status page from the prober library
Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-06 11:27:59 +01:00
Anton Tolchanov
9106187a95 prober: support JSON response in RunHandler
Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-06 11:27:59 +01:00
Anton Tolchanov
9b08399d9e prober: add a status page handler
This change adds an HTTP handler with a table showing a list of all
probes, their status, and a button that allows triggering a specific
probe.

Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-06 11:27:59 +01:00
Anton Tolchanov
153a476957 prober: add an HTTP endpoint for triggering a probe
- Keep track of the last 10 probe results and successful probe
  latencies;
- Add an HTTP handler that triggers a given probe by name and returns it
  result as a plaintext HTML page, showing recent probe results as a
  baseline

Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-06 11:27:59 +01:00
Anton Tolchanov
227509547f {control,net}: close idle connections of custom transports
I noticed a few places with custom http.Transport where we are not
closing idle connections when transport is no longer used.

Updates tailscale/corp#21609

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-05 17:28:15 +01:00
VimT
e3f047618b net/socks5: support UDP
Updates #7581

Signed-off-by: VimT <me@vimt.me>
2024-08-05 09:25:24 -07:00
Kot C
91d2e1772d words: raccoon dog, dog with the raccoon in 'im
Signed-off-by: Kot C <kot@yukata.dev>
2024-08-05 09:24:33 -07:00
License Updater
3b6849e362 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-08-05 08:45:07 -07:00
Anton Tolchanov
0fd73746dd cmd/tailscale/cli: fix revoke-keys command name in CLI output
During review of #8644 the `recover-compromised-key` command was renamed
to `revoke-key`, but the old name remained in some messages printed by
the command.

Fixes tailscale/corp#19446

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-08-05 14:49:48 +01:00
Jordan Whited
17c88a19be net/captivedetection: mark TestAllEndpointsAreUpAndReturnExpectedResponse flaky (#13021)
Updates #13019

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-08-03 22:08:55 +00:00
Jordan Whited
25f0a3fc8f wgengine/netstack: use build tags to exclude gVisor GRO importation on iOS (#13015)
Updates tailscale/corp#22125

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-08-03 15:03:44 -07:00
Maisem Ali
a7a394e7d9 tstest/integration: mark TestNATPing flaky
Updates #12169

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-08-03 15:02:17 -07:00
Maisem Ali
07e2487c1d wgengine/capture: fix v6 field typo in wireshark dissector
It was using a v4 field for a v6 address.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-08-03 14:56:17 -07:00
Maisem Ali
1dd9c44d51 tsweb: mark TestStdHandler_ConnectionClosedDuringBody flaky
Updates #13107

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-08-03 14:54:10 -07:00
Flakes Updater
0a6eb12f05 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-08-03 11:45:38 -07:00
Maisem Ali
f205efcf18 net/packet/checksum: fix v6 NAT
We were copying 12 out of the 16 bytes which meant that
the 1:1 NAT required would only work if the last 4 bytes
happened to match between the new and old address, something
that our tests accidentally had. Fix it by copying the full
16 bytes and make the tests also verify the addr and use rand
addresses.

Updates #9511

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-08-03 11:38:00 -07:00
Maisem Ali
a917718353 util/linuxfw: return nil interface not concrete type
It was returning a nil `*iptablesRunner` instead of a
nil `NetfilterRunner` interface which would then fail
checks later.

Fixes #13012

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-08-03 09:53:46 -07:00
Nick Khyl
4099a36468 util/winutil/gp: fix a busy loop bug
Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-02 20:16:41 -05:00
Jordan Whited
d9d9d525d9 wgengine/netstack: increase gVisor's TCP send and receive buffer sizes (#12994)
This commit increases gVisor's TCP max send (4->6MiB) and receive
(4->8MiB) buffer sizes on all platforms except iOS. These values are
biased towards higher throughput on high bandwidth-delay product paths.

The iperf3 results below demonstrate the effect of this commit between
two Linux computers with i5-12400 CPUs. 100ms of RTT latency is
introduced via Linux's traffic control network emulator queue
discipline.

The first set of results are from commit f0230ce prior to TCP buffer
resizing.

gVisor write direction:
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec   180 MBytes   151 Mbits/sec    0  sender
[  5]   0.00-10.10  sec   179 MBytes   149 Mbits/sec       receiver

gVisor read direction:
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.10  sec   337 MBytes   280 Mbits/sec   20 sender
[  5]   0.00-10.00  sec   323 MBytes   271 Mbits/sec         receiver

The second set of results are from this commit with increased TCP
buffer sizes.

gVisor write direction:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec   297 MBytes   249 Mbits/sec    0 sender
[  5]   0.00-10.10  sec   297 MBytes   247 Mbits/sec        receiver

gVisor read direction:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.10  sec   501 MBytes   416 Mbits/sec   17  sender
[  5]   0.00-10.00  sec   485 MBytes   407 Mbits/sec       receiver

Updates #9707
Updates tailscale/corp#22119

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-08-02 15:50:47 -07:00
Andrew Dunham
9939374c48 wgengine/magicsock: use cloud metadata to get public IPs
Updates #12774

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I1661b6a2da7966ab667b075894837afd96f4742f
2024-08-02 16:05:14 -04:00
Andrea Gottardo
4055b63b9b net/captivedetection: exclude cellular data interfaces (#13002)
Updates tailscale/tailscale#1634

This PR optimizes captive portal detection on Android and iOS by excluding cellular data interfaces (`pdp*` and `rmnet`). As cellular networks do not present captive portals, frequent network switches between Wi-Fi and cellular would otherwise trigger captive detection unnecessarily, causing battery drain.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-08-02 12:23:48 -07:00
Jordan Whited
f0230ce0b5 go.mod,net/tstun,wgengine/netstack: implement gVisor TCP GRO for Linux (#12921)
This commit implements TCP GRO for packets being written to gVisor on
Linux. Windows support will follow later. The wireguard-go dependency is
updated in order to make use of newly exported IP checksum functions.
gVisor is updated in order to make use of newly exported
stack.PacketBuffer GRO logic.

TCP throughput towards gVisor, i.e. TUN write direction, is dramatically
improved as a result of this commit. Benchmarks show substantial
improvement, sometimes as high as 2x. High bandwidth-delay product
paths remain receive window limited, bottlenecked by gVisor's default
TCP receive socket buffer size. This will be addressed in a  follow-on
commit.

The iperf3 results below demonstrate the effect of this commit between
two Linux computers with i5-12400 CPUs. There is roughly ~13us of round
trip latency between them.

The first result is from commit 57856fc without TCP GRO.

Starting Test: protocol: TCP, 1 streams, 131072 byte blocks
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  4.77 GBytes  4.10 Gbits/sec   20 sender
[  5]   0.00-10.00  sec  4.77 GBytes  4.10 Gbits/sec      receiver

The second result is from this commit with TCP GRO.

Starting Test: protocol: TCP, 1 streams, 131072 byte blocks
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  10.6 GBytes  9.14 Gbits/sec   20 sender
[  5]   0.00-10.00  sec  10.6 GBytes  9.14 Gbits/sec      receiver

Updates #6816

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-08-02 10:41:10 -07:00
Brad Fitzpatrick
cc370314e7 health: don't show login error details with context cancelations
Fixes #12991

Change-Id: I2a5e109395761b720ecf1069d0167cf0caf72876
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-08-01 08:29:27 -07:00
Aaron Klotz
655b4f8fc5 net/netns: remove some logspam by avoiding logging parse errors due to unspecified addresses
I updated the address parsing stuff to return a specific error for
unspecified hosts passed as empty strings, and look for that
when logging errors. I explicitly did not make parseAddress return a
netip.Addr containing an unspecified address because at this layer,
in the absence of any host, we don't necessarily know the address
family we're dealing with.

For the purposes of this code I think this is fine, at least until
we implement #12588.

Fixes #12979

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-07-31 12:34:16 -06:00
Brad Fitzpatrick
004dded0a8 net/tlsdial: relax self-signed cert health warning
It seems some security software or macOS itself might be MITMing TLS
(for ScreenTime?), so don't warn unless it fails x509 validation
against system roots.

Updates #3198

Change-Id: I6ea381b5bb6385b3d51da4a1468c0d803236b7bf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-31 10:03:48 -07:00
Aaron Klotz
0def4f8e38 net/netns: on Windows, fall back to default interface index when unspecified address is passed to ControlC and bindToInterfaceByRoute is enabled
We were returning an error instead of binding to the default interface.

Updates #12979

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-07-31 10:58:45 -06:00
Jordan Whited
7bc2ddaedc go.mod,net/tstun,wgengine/netstack: implement gVisor TCP GSO for Linux (#12869)
This commit implements TCP GSO for packets being read from gVisor on
Linux. Windows support will follow later. The wireguard-go dependency is
updated in order to make use of newly exported GSO logic from its tun
package.

A new gVisor stack.LinkEndpoint implementation has been established
(linkEndpoint) that is loosely modeled after its predecessor
(channel.Endpoint). This new implementation supports GSO of monster TCP
segments up to 64K in size, whereas channel.Endpoint only supports up to
32K. linkEndpoint will also be required for GRO, which will be
implemented in a follow-on commit.

TCP throughput from gVisor, i.e. TUN read direction, is dramatically
improved as a result of this commit. Benchmarks show substantial
improvement through a wide range of RTT and loss conditions, sometimes
as high as 5x.

The iperf3 results below demonstrate the effect of this commit between
two Linux computers with i5-12400 CPUs. There is roughly ~13us of round
trip latency between them.

The first result is from commit 57856fc without TCP GSO.

Starting Test: protocol: TCP, 1 streams, 131072 byte blocks
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  2.51 GBytes  2.15 Gbits/sec  154 sender
[  5]   0.00-10.00  sec  2.49 GBytes  2.14 Gbits/sec      receiver

The second result is from this commit with TCP GSO.

Starting Test: protocol: TCP, 1 streams, 131072 byte blocks
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  12.6 GBytes  10.8 Gbits/sec    6 sender
[  5]   0.00-10.00  sec  12.6 GBytes  10.8 Gbits/sec      receiver

Updates #6816

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-07-31 09:42:11 -07:00
Andrea Gottardo
949b15d858 net/captivedetection: call SetHealthy once connectivity restored (#12974)
Fixes tailscale/tailscale#12973
Updates tailscale/tailscale#1634

There was a logic issue in the captive detection code we shipped in https://github.com/tailscale/tailscale/pull/12707.

Assume a captive portal has been detected, and the user notified. Upon switching to another Wi-Fi that does *not* have a captive portal, we were issuing a signal to interrupt any pending captive detection attempt. However, we were not also setting the `captive-portal-detected` warnable to healthy. The result was that any "captive portal detected" alert would not be cleared from the UI.

Also fixes a broken log statement value.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-07-30 13:39:25 -07:00
Jonathan Nobels
8a8ecac6a7 net/dns, cmd/tailscaled: plumb system health tracker into dns cleanup (#12969)
fixes tailscale#12968

The dns manager cleanup func was getting passed a nil
health tracker, which will panic.  Fixed to pass it
the system health tracker.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-07-30 12:54:03 -04:00
Irbe Krumina
eead25560f build_docker.sh: update script comment (#12970)
It is no longer correct to state that we don't support running Tailscale in containers or on Kubernetes.

Updates tailscale/tailscale#12842

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-07-30 15:12:43 +01:00
dependabot[bot]
1b64961320 build(deps): bump github.com/docker/docker (#12966)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 25.0.5+incompatible to 26.1.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v25.0.5...v26.1.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-30 12:46:14 +01:00
Irbe Krumina
32308fcf71 Dockerfile: add a warning that this is not used to build our published images (#12955)
Add a warning that the Dockerfile in the OSS repo is not the
currently used mechanism to build the images we publish - for folks
who want to contribute to image build scripts or otherwise need to
understand the image build process that we use.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-07-30 12:22:53 +01:00
Flakes Updater
34de96d06e go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-07-29 19:40:24 -07:00
Brad Fitzpatrick
575feb486f util/osuser: turn wasm check into a const expression
All wasi* are GOARCH wasm, so check that instead.

Updates #12732

Change-Id: Id3cc346295c1641bcf80a6c5eb1ad65488509656
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-29 19:39:55 -07:00
96 changed files with 3306 additions and 4767 deletions

4
.gitignore vendored
View File

@@ -43,7 +43,3 @@ client/web/build/assets
/gocross
/dist
# Ignore xcode userstate and workspace data
*.xcuserstate
*.xcworkspacedata

View File

@@ -1,6 +1,13 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
# Note that this Dockerfile is currently NOT used to build any of the published
# Tailscale container images and may have drifted from the image build mechanism
# we use.
# Tailscale images are currently built using https://github.com/tailscale/mkctr,
# and the build script can be found in ./build_docker.sh.
#
#
# This Dockerfile includes all the tailscale binaries.
#
# To build the Dockerfile:

View File

@@ -1,21 +1,11 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for docker distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries inside docker, so that we can track down user
# issues.
#
############################################################################
#
# WARNING: Tailscale is not yet officially supported in container
# environments, such as Docker and Kubernetes. Though it should work, we
# don't regularly test it, and we know there are some feature limitations.
#
# See current bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
# This script builds Tailscale container images using
# github.com/tailscale/mkctr.
# By default the images will be tagged with the current version and git
# hash of this repository as produced by ./cmd/mkversion.
# This is the image build mechanim used to build the official Tailscale
# container images.
set -eu

View File

@@ -7,8 +7,6 @@ package main
import (
"flag"
"fmt"
"html"
"io"
"log"
"net/http"
"sort"
@@ -70,8 +68,13 @@ func main() {
}
mux := http.NewServeMux()
tsweb.Debugger(mux)
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
d := tsweb.Debugger(mux)
d.Handle("probe-run", "Run a probe", tsweb.StdHandler(tsweb.ReturnHandlerFunc(p.RunHandler), tsweb.HandlerOptions{Logf: log.Printf}))
mux.Handle("/", tsweb.StdHandler(p.StatusHandler(
prober.WithTitle("DERP Prober"),
prober.WithPageLink("Prober metrics", "/debug/varz"),
prober.WithProbeLink("Run Probe", "/debug/probe-run?name={{.Name}}"),
), tsweb.HandlerOptions{Logf: log.Printf}))
log.Printf("Listening on %s", *listen)
log.Fatal(http.ListenAndServe(*listen, mux))
}
@@ -105,26 +108,3 @@ func getOverallStatus(p *prober.Prober) (o overallStatus) {
sort.Strings(o.good)
return
}
func serveFunc(p *prober.Prober) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
st := getOverallStatus(p)
summary := "All good"
if (float64(len(st.bad)) / float64(len(st.bad)+len(st.good))) > 0.25 {
// Returning a 500 allows monitoring this server externally and configuring
// an alert on HTTP response code.
w.WriteHeader(500)
summary = fmt.Sprintf("%d problems", len(st.bad))
}
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
for _, s := range st.bad {
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
}
for _, s := range st.good {
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
}
io.WriteString(w, "</ul></body></html>\n")
}
}

View File

@@ -301,7 +301,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -311,6 +310,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/stack/gro from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+

View File

@@ -7,6 +7,7 @@ package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -159,8 +160,10 @@ func newRootCmd() *ffcli.Command {
return nil
})
rootfs.Lookup("socket").DefValue = localClient.Socket
jsonDocs := rootfs.Bool("json-docs", false, hidden+"print JSON-encoded docs for all subcommands and flags")
rootCmd := &ffcli.Command{
var rootCmd *ffcli.Command
rootCmd = &ffcli.Command{
Name: "tailscale",
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
ShortHelp: "The easiest, most secure way to use WireGuard.",
@@ -202,6 +205,9 @@ change in the future.
},
FlagSet: rootfs,
Exec: func(ctx context.Context, args []string) error {
if *jsonDocs {
return printJSONDocs(rootCmd)
}
if len(args) > 0 {
return fmt.Errorf("tailscale: unknown subcommand: %s", args[0])
}
@@ -401,3 +407,54 @@ func colorableOutput() (w io.Writer, ok bool) {
}
return colorable.NewColorableStdout(), true
}
type commandDoc struct {
Name string
Desc string
Subcommands []commandDoc `json:",omitempty"`
Flags []flagDoc `json:",omitempty"`
}
type flagDoc struct {
Name string
Desc string
}
func printJSONDocs(root *ffcli.Command) error {
docs := jsonDocsWalk(root)
return json.NewEncoder(os.Stdout).Encode(docs)
}
func jsonDocsWalk(cmd *ffcli.Command) *commandDoc {
res := &commandDoc{
Name: cmd.Name,
}
if cmd.LongHelp != "" {
res.Desc = cmd.LongHelp
} else if cmd.ShortHelp != "" {
res.Desc = cmd.ShortHelp
} else {
res.Desc = cmd.ShortUsage
}
if strings.HasPrefix(res.Desc, hidden) {
return nil
}
if cmd.FlagSet != nil {
cmd.FlagSet.VisitAll(func(f *flag.Flag) {
if strings.HasPrefix(f.Usage, hidden) {
return
}
res.Flags = append(res.Flags, flagDoc{
Name: f.Name,
Desc: f.Usage,
})
})
}
for _, sub := range cmd.Subcommands {
subj := jsonDocsWalk(sub)
if subj != nil {
res.Subcommands = append(res.Subcommands, *subj)
}
}
return res
}

View File

@@ -789,7 +789,7 @@ func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
}
fmt.Printf(`Run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
%s lock revoke-keys --cosign %X
`, os.Args[0], aumBytes)
return nil
}
@@ -813,10 +813,10 @@ func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
fmt.Printf(`Co-signing completed successfully.
To accumulate an additional signature, run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
%s lock revoke-keys --cosign %X
Alternatively if you are done with co-signing, complete recovery by running the following command:
%s lock recover-compromised-key --finish %X
%s lock revoke-keys --finish %X
`, os.Args[0], aumBytes, os.Args[0], aumBytes)
}

View File

@@ -212,7 +212,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -222,6 +221,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/stack/gro from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+

View File

@@ -394,7 +394,7 @@ func run() (err error) {
// Always clean up, even if we're going to run the server. This covers cases
// such as when a system was rebooted without shutting down, or tailscaled
// crashed, and would for example restore system DNS configuration.
dns.CleanUp(logf, netMon, args.tunname)
dns.CleanUp(logf, netMon, sys.HealthTracker(), args.tunname)
router.CleanUp(logf, netMon, args.tunname)
// If the cleanUp flag was passed, then exit.
if args.cleanUp {

View File

@@ -333,6 +333,9 @@ func (c *Direct) Close() error {
}
}
c.noiseClient = nil
if tr, ok := c.httpc.Transport.(*http.Transport); ok {
tr.CloseIdleConnections()
}
return nil
}

View File

@@ -46,6 +46,7 @@ import (
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/util/multierr"
@@ -497,11 +498,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the
// transport. We make exactly 1 request using this transport, so
// there will be exactly 1 GotConn call. Additionally, the
// transport handles 101 Switching Protocols correctly, such that
// the Conn will not be reused or kept alive by the transport once
// the response has been handed back from RoundTrip.
// transport. The transport handles 101 Switching Protocols correctly,
// such that the Conn will not be reused or kept alive by the transport
// once the response has been handed back from RoundTrip.
//
// In theory, the machinery of net/http should make it such that
// the trace callback happens-before we get the response, but
@@ -517,10 +516,16 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
// unexpected EOFs...), and we're bound to forget someday and
// introduce a protocol optimization at a higher level that starts
// eagerly transmitting from the server.
connCh := make(chan net.Conn, 1)
var lastConn syncs.AtomicValue[net.Conn]
trace := httptrace.ClientTrace{
// Even though we only make a single HTTP request which should
// require a single connection, the context (with the attached
// trace configuration) might be used by our custom dialer to
// make other HTTP requests (e.g. BootstrapDNS). We only care
// about the last connection made, which should be the one to
// the control server.
GotConn: func(info httptrace.GotConnInfo) {
connCh <- info.Conn
lastConn.Store(info.Conn)
},
}
ctx = httptrace.WithClientTrace(ctx, &trace)
@@ -548,11 +553,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
// is still a read buffer attached to it within resp.Body. So, we
// must direct I/O through resp.Body, but we can still use the
// underlying net.Conn for stuff like deadlines.
var switchedConn net.Conn
select {
case switchedConn = <-connCh:
default:
}
switchedConn := lastConn.Load()
if switchedConn == nil {
resp.Body.Close()
return nil, fmt.Errorf("httptrace didn't provide a connection")

View File

@@ -11,10 +11,12 @@ import (
"log"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/netip"
"net/url"
"runtime"
"slices"
"strconv"
"sync"
"testing"
@@ -41,6 +43,8 @@ type httpTestParam struct {
makeHTTPHangAfterUpgrade bool
doEarlyWrite bool
httpInDial bool
}
func TestControlHTTP(t *testing.T) {
@@ -120,6 +124,12 @@ func TestControlHTTP(t *testing.T) {
name: "early_write",
doEarlyWrite: true,
},
// Dialer needed to make another HTTP request along the way (e.g. to
// resolve the hostname via BootstrapDNS).
{
name: "http_request_in_dial",
httpInDial: true,
},
}
for _, test := range tests {
@@ -217,6 +227,29 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
Clock: clock,
}
if param.httpInDial {
// Spin up a separate server to get a different port on localhost.
secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
defer secondServer.Close()
prev := a.Dialer
a.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", secondServer.URL, nil)
if err != nil {
t.Errorf("http.NewRequest: %v", err)
}
r, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("http.Get: %v", err)
}
r.Body.Close()
return prev(ctx, network, addr)
}
}
if proxy != nil {
proxyEnv := proxy.Start(t)
defer proxy.Close()
@@ -238,6 +271,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
t.Fatalf("dialing controlhttp: %v", err)
}
defer conn.Close()
si := <-sch
if si.conn != nil {
defer si.conn.Close()
@@ -266,6 +300,19 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
t.Errorf("early write = %q; want %q", buf, earlyWriteMsg)
}
}
// When no proxy is used, the RemoteAddr of the returned connection should match
// one of the listeners of the test server.
if proxy == nil {
var expectedAddrs []string
for _, ln := range []net.Listener{httpLn, httpsLn} {
expectedAddrs = append(expectedAddrs, fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port))
expectedAddrs = append(expectedAddrs, fmt.Sprintf("[::1]:%d", ln.Addr().(*net.TCPAddr).Port))
}
if !slices.Contains(expectedAddrs, conn.RemoteAddr().String()) {
t.Errorf("unexpected remote addr: %s, want %s", conn.RemoteAddr(), expectedAddrs)
}
}
}
type serverResult struct {

View File

@@ -120,4 +120,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-2x9Ns5o6oenCcsHkOFjoCz/R5YjPwJEImK0a1valYBE=
# nix-direnv cache busting line: sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=

8
go.mod
View File

@@ -80,7 +80,7 @@ require (
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e
github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0
@@ -104,7 +104,7 @@ require (
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
gopkg.in/square/go-jose.v2 v2.6.0
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
honnef.co/go/tools v0.4.6
k8s.io/api v0.30.3
k8s.io/apimachinery v0.30.3
@@ -131,9 +131,9 @@ require (
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/goccy/go-yaml v1.12.0 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
@@ -198,7 +198,7 @@ require (
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/docker/cli v25.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/docker v26.1.4+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect

View File

@@ -1 +1 @@
sha256-2x9Ns5o6oenCcsHkOFjoCz/R5YjPwJEImK0a1valYBE=
sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=

16
go.sum
View File

@@ -262,8 +262,8 @@ github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpF
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -477,8 +477,6 @@ github.com/google/go-containerregistry v0.18.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4=
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -701,6 +699,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -934,8 +934,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:t
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@@ -1491,8 +1491,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1,5 +1,3 @@
# ./qemu-wrapper /tmp/qemu.sock qemu-system-x86_64 [...] -netdev socket,id=vlan,fd=3 -device virtio-net-pci,netdev=vlan
help:
echo "See Makefile"
@@ -8,6 +6,3 @@ image:
qemu: image
qemu-system-x86_64 -m 1G -drive file=tsapp.img,format=raw -boot d -netdev user,id=user.0 -device virtio-net-pci,netdev=user.0 -serial mon:stdio -audio none
qemuwrap: image
go run ./cmd/vnetwrap qemu-system-x86_64 -m 1G -drive file=tsapp.img,format=raw -boot d -netdev socket,fd=3,id=user.0 -device virtio-net-pci,netdev=user.0 -serial mon:stdio -audio none

View File

@@ -1,68 +0,0 @@
package main
import (
"encoding/binary"
"io"
"log"
"net"
"os"
"os/exec"
)
const path = "/tmp/vnet.sock"
func serve(ln net.Listener) {
for {
c, err := ln.Accept()
if err != nil {
log.Printf("Accept: %v", err)
continue
}
go serveConn(c)
}
}
func serveConn(c net.Conn) {
log.Printf("Got conn")
defer c.Close()
buf := make([]byte, 4<<10)
for {
if _, err := io.ReadFull(c, buf[:4]); err != nil {
log.Printf("ReadFull header: %v", err)
return
}
n := binary.BigEndian.Uint32(buf[:4])
if _, err := io.ReadFull(c, buf[:n]); err != nil {
log.Printf("ReadFull pkt: %v", err)
return
}
log.Printf("pkt %d bytes: % 02x", n, buf[:n])
}
}
func main() {
srv, err := net.Listen("unix", path)
if err != nil {
log.Fatal(err)
}
go serve(srv)
conn, err := net.Dial("unix", path)
if err != nil {
log.Fatal(err)
}
fd, err := conn.(*net.UnixConn).File()
if err != nil {
log.Fatal(err)
}
cmd := exec.Command(os.Args[1], os.Args[2:]...) // #nosec G204
cmd.ExtraFiles = append(cmd.ExtraFiles, fd)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}

View File

@@ -6,6 +6,7 @@
package health
import (
"context"
"errors"
"fmt"
"maps"
@@ -987,8 +988,12 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
}
if t.lastLoginErr != nil {
var errMsg string
if !errors.Is(t.lastLoginErr, context.Canceled) {
errMsg = t.lastLoginErr.Error()
}
t.setUnhealthyLocked(LoginStateWarnable, Args{
ArgError: t.lastLoginErr.Error(),
ArgError: errMsg,
})
return
} else {

View File

@@ -780,6 +780,9 @@ func (b *LocalBackend) onHealthChange(w *health.Warnable, us *health.UnhealthySt
case <-ctx.Done():
}
} else {
// If connectivity is not impacted, we know for sure we're not behind a captive portal,
// so drop any warning, and signal that we don't need captive portal detection.
b.health.SetHealthy(captivePortalWarnable)
select {
case b.needsCaptiveDetection <- false:
case <-ctx.Done():

View File

@@ -65,8 +65,8 @@ See also the dependencies in the [Tailscale CLI][].
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f5d148bcfe1/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/71393c576b98/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
@@ -82,7 +82,7 @@ See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))

View File

@@ -84,8 +84,8 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5db17b287bf1/LICENSE))
- [github.com/tailscale/wf](https://pkg.go.dev/github.com/tailscale/wf) ([BSD-3-Clause](https://github.com/tailscale/wf/blob/6fbb0a674ee6/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f5d148bcfe1/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/71393c576b98/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/v0.12.0/LICENSE))
@@ -95,19 +95,19 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.24.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.16.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.21.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.1/LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.3/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/LICENSE))
- [sigs.k8s.io/yaml/goyaml.v2](https://pkg.go.dev/sigs.k8s.io/yaml/goyaml.v2) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/goyaml.v2/LICENSE))

View File

@@ -57,9 +57,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/7601212d8e23/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/4327221bd339/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/6580b55d49ca/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
@@ -69,7 +69,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.18.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.18.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.19.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))

View File

@@ -1,229 +0,0 @@
package main
import (
"errors"
"math/rand/v2"
"net/netip"
"time"
"tailscale.com/util/mak"
)
// IPPool is the interface that a NAT implementation uses to get information
// about a network.
//
// Outside of tests, this is typically a *network.
type IPPool interface {
// WANIP returns the primary WAN IP address.
//
// TODO: add another method for networks with multiple WAN IP addresses.
WANIP() netip.Addr
// SoleLanIP reports whether this network has a sole LAN client
// and if so, its IP address.
SoleLANIP() (_ netip.Addr, ok bool)
// TODO: port availability stuff for interacting with portmapping
}
// newTableFunc is a constructor for a NAT table.
// The provided IPPool is typically (outside of tests) a *network.
type newTableFunc func(IPPool) (NATTable, error)
// natTypes are the known NAT types.
var natTypes = map[string]newTableFunc{}
// registerNATType registers a NAT type.
func registerNATType(name string, f newTableFunc) {
if _, ok := natTypes[name]; ok {
panic("duplicate NAT type: " + name)
}
natTypes[name] = f
}
// NATTable is what a NAT implementation is expected to do.
//
// This project tests Tailscale as it faces various combinations various NAT
// implementations (e.g. Linux easy style NAT vs FreeBSD hard/endpoint dependent
// NAT vs Cloud 1:1 NAT, etc)
//
// Implementations of NATTable need not handle concurrency; the natlab serializes
// all calls into a NATTable.
//
// The provided `at` value will typically be time.Now, except for tests.
// Implementations should not use real time and should only compare
// previously provided time values.
type NATTable interface {
// PickOutgoingSrc returns the source address to use for an outgoing packet.
//
// The result should either be invalid (to drop the packet) or a WAN (not
// private) IP address.
//
// Typically, the src is a LAN source IP address, but it might also be a WAN
// IP address if the packet is being forwarded for a source machine that has
// a public IP address.
PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort)
// PickIncomingDst returns the destination address to use for an incoming
// packet. The incoming src address is always a public WAN IP.
//
// The result should either be invalid (to drop the packet) or the IP
// address of a machine on the local network address, usually a private
// LAN IP.
PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort)
}
// oneToOneNAT is a 1:1 NAT, like a typical EC2 VM.
type oneToOneNAT struct {
lanIP netip.Addr
wanIP netip.Addr
}
func init() {
registerNATType("one2one", func(p IPPool) (NATTable, error) {
lanIP, ok := p.SoleLANIP()
if !ok {
return nil, errors.New("can't use one2one NAT type on networks other than single-node networks")
}
return &oneToOneNAT{lanIP: lanIP, wanIP: p.WANIP()}, nil
})
}
func (n *oneToOneNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
return netip.AddrPortFrom(n.wanIP, src.Port())
}
func (n *oneToOneNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
return netip.AddrPortFrom(n.lanIP, dst.Port())
}
type hardKeyOut struct {
lanIP netip.Addr
dst netip.AddrPort
}
type hardKeyIn struct {
wanPort uint16
src netip.AddrPort
}
type portMappingAndTime struct {
port uint16
at time.Time
}
type lanAddrAndTime struct {
lanAddr netip.AddrPort
at time.Time
}
// hardNAT is an "Endpoint Dependent" NAT, like FreeBSD/pfSense/OPNsense.
// This is shown as "MappingVariesByDestIP: true" by netcheck, and what
// Tailscale calls "Hard NAT".
type hardNAT struct {
wanIP netip.Addr
out map[hardKeyOut]portMappingAndTime
in map[hardKeyIn]lanAddrAndTime
}
func init() {
registerNATType("hard", func(p IPPool) (NATTable, error) {
return &hardNAT{wanIP: p.WANIP()}, nil
})
}
func (n *hardNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
ko := hardKeyOut{src.Addr(), dst}
if pm, ok := n.out[ko]; ok {
// Existing flow.
// TODO: bump timestamp
return netip.AddrPortFrom(n.wanIP, pm.port)
}
// No existing mapping exists. Create one.
// TODO: clean up old expired mappings
// Instead of proper data structures that would be efficient, we instead
// just loop a bunch and look for a free port. This project is only used
// by tests and doesn't care about performance, this is good enough.
for {
port := rand.N(uint16(32<<10)) + 32<<10 // pick some "ephemeral" port
ki := hardKeyIn{wanPort: port, src: dst}
if _, ok := n.in[ki]; ok {
// Port already in use.
continue
}
mak.Set(&n.in, ki, lanAddrAndTime{lanAddr: src, at: at})
mak.Set(&n.out, ko, portMappingAndTime{port: port, at: at})
return netip.AddrPortFrom(n.wanIP, port)
}
}
func (n *hardNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
if dst.Addr() != n.wanIP {
return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
}
ki := hardKeyIn{wanPort: dst.Port(), src: src}
if pm, ok := n.in[ki]; ok {
// Existing flow.
return pm.lanAddr
}
return netip.AddrPort{} // drop; no mapping
}
type easyKeyOut struct {
src netip.AddrPort
}
// easyNAT is an "Endpoint Independent" NAT, like Linux and most home routers
// (many of which are Linux).
//
// This is shown as "MappingVariesByDestIP: false" by netcheck, and what
// Tailscale calls "Easy NAT".
//
// Unlike Linux, this implementation is capped at 32k entries and doesn't resort
// to other allocation strategies when all 32k WAN ports are taken.
type easyNAT struct {
wanIP netip.Addr
out map[netip.AddrPort]portMappingAndTime
in map[uint16]lanAddrAndTime
}
func init() {
registerNATType("easy", func(p IPPool) (NATTable, error) {
return &easyNAT{wanIP: p.WANIP()}, nil
})
}
func (n *easyNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
if pm, ok := n.out[src]; ok {
// Existing flow.
// TODO: bump timestamp
return netip.AddrPortFrom(n.wanIP, pm.port)
}
// Loop through all 32k high (ephemeral) ports, starting at a random
// position and looping back around to the start.
start := rand.N(uint16(32 << 10))
for off := range uint16(32 << 10) {
port := 32<<10 + (start+off)%(32<<10)
if _, ok := n.in[port]; !ok {
wanAddr := netip.AddrPortFrom(n.wanIP, port)
// Found a free port.
mak.Set(&n.out, src, portMappingAndTime{port: port, at: at})
mak.Set(&n.in, port, lanAddrAndTime{lanAddr: src, at: at})
return wanAddr
}
}
return netip.AddrPort{} // failed to allocate a mapping; TODO: fire an alert?
}
func (n *easyNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
if dst.Addr() != n.wanIP {
return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
}
return n.in[dst.Port()].lanAddr
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
echo "Type 'C-a c' to enter monitor; q to quit."
set -eux
qemu-system-x86_64 -M microvm,isa-serial=off \
-m 1G \
-nodefaults -no-user-config -nographic \
-kernel $HOME/src/github.com/tailscale/gokrazy-kernel/vmlinuz \
-append "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-dd02023b0001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet" \
-drive id=blk0,file=$HOME/src/tailscale.com/gokrazy/tsapp.img,format=raw \
-device virtio-blk-device,drive=blk0 \
-netdev stream,id=net0,addr.type=unix,addr.path=/tmp/qemu.sock \
-device virtio-serial-device \
-device virtio-net-device,netdev=net0,mac=5a:94:ef:e4:0c:ee \
-chardev stdio,id=virtiocon0,mux=on \
-device virtconsole,chardev=virtiocon0 \
-mon chardev=virtiocon0,mode=readline \
-audio none

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
Copyright © 2023 Apple Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,20 +0,0 @@
import Foundation
// We need to make this all configurable via config file of some kind and
// read it in.
struct Config {
static let mac = "5a:94:ef:e4:0c:ee"
static let serverSocket = "/tmp/qemu.sock"
static let clientSocket = "/tmp/qemu_client.sock"
static let memorySize = (4 * 1024 * 1024 * 1024) as UInt64
}
let vmBundlePath = NSHomeDirectory() + "/VM.bundle/"
let vmBundleURL = URL(fileURLWithPath: vmBundlePath)
let auxiliaryStorageURL = vmBundleURL.appendingPathComponent("AuxiliaryStorage")
let diskImageURL = vmBundleURL.appendingPathComponent("Disk.img")
let hardwareModelURL = vmBundleURL.appendingPathComponent("HardwareModel")
let machineIdentifierURL = vmBundleURL.appendingPathComponent("MachineIdentifier")
let restoreImageURL = vmBundleURL.appendingPathComponent("RestoreImage.ipsw")
let saveFileURL = vmBundleURL.appendingPathComponent("SaveFile.vzvmsave")

View File

@@ -1,21 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
A class that conforms to `VZVirtualMachineDelegate` and tracks the virtual machine's state.
*/
import Foundation
import Virtualization
class VnetDelegate: NSObject, VZVirtualMachineDelegate {
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
NSLog("Virtual machine did stop with error: \(error.localizedDescription)")
exit(-1)
}
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
NSLog("Guest did stop virtual machine.")
exit(0)
}
}

View File

@@ -1,126 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
The helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`.
*/
import Foundation
import Virtualization
#if arch(arm64)
struct VnetHostConfigHelper {
static func computeCPUCount() -> Int {
let totalAvailableCPUs = ProcessInfo.processInfo.processorCount
var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - 1
virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount)
return virtualCPUCount
}
static func computeMemorySize() -> UInt64 {
// Set the amount of system memory to 4 GB; this is a baseline value
// that you can change depending on your use case.
var memorySize = Config.memorySize
memorySize = max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
memorySize = min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize)
return memorySize
}
static func createBootLoader() -> VZMacOSBootLoader {
return VZMacOSBootLoader()
}
static func createGraphicsDeviceConfiguration() -> VZMacGraphicsDeviceConfiguration {
let graphicsConfiguration = VZMacGraphicsDeviceConfiguration()
graphicsConfiguration.displays = [
// The system arbitrarily chooses the resolution of the display to be 1920 x 1200.
VZMacGraphicsDisplayConfiguration(widthInPixels: 1920, heightInPixels: 1200, pixelsPerInch: 80)
]
return graphicsConfiguration
}
static func createBlockDeviceConfiguration() -> VZVirtioBlockDeviceConfiguration {
guard let diskImageAttachment = try? VZDiskImageStorageDeviceAttachment(url: diskImageURL, readOnly: false) else {
fatalError("Failed to create Disk image.")
}
let disk = VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
return disk
}
static func createNetworkDeviceConfiguration() -> VZVirtioNetworkDeviceConfiguration {
let networkDevice = VZVirtioNetworkDeviceConfiguration()
networkDevice.macAddress = VZMACAddress(string: Config.mac)!
let socket = Darwin.socket(AF_UNIX, SOCK_DGRAM, 0)
let serverSocket = Config.serverSocket
let clientSocket = Config.clientSocket
unlink(clientSocket)
var clientAddr = sockaddr_un()
clientAddr.sun_family = sa_family_t(AF_UNIX)
clientSocket.withCString { ptr in
withUnsafeMutablePointer(to: &clientAddr.sun_path.0) { dest in
_ = strcpy(dest, ptr)
}
}
let bindRes = Darwin.bind(socket,
withUnsafePointer(to: &clientAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
socklen_t(MemoryLayout<sockaddr_un>.size))
if bindRes == -1 {
print("Error binding virtual network client socket - \(String(cString: strerror(errno)))")
return networkDevice
}
var serverAddr = sockaddr_un()
serverAddr.sun_family = sa_family_t(AF_UNIX)
serverSocket.withCString { ptr in
withUnsafeMutablePointer(to: &serverAddr.sun_path.0) { dest in
_ = strcpy(dest, ptr)
}
}
let connectRes = Darwin.connect(socket,
withUnsafePointer(to: &serverAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
socklen_t(MemoryLayout<sockaddr_un>.size))
if connectRes == -1 {
print("Error binding virtual network server socket - \(String(cString: strerror(errno)))")
return networkDevice
}
print("Virtual if mac address is \(Config.mac)")
print("Client bound to \(clientSocket)")
print("Connected to server at \(serverSocket)")
print("Socket fd is \(socket)")
let handle = FileHandle(fileDescriptor: socket)
let device = VZFileHandleNetworkDeviceAttachment(fileHandle: handle)
networkDevice.attachment = device
return networkDevice
}
static func createPointingDeviceConfiguration() -> VZPointingDeviceConfiguration {
return VZMacTrackpadConfiguration()
}
static func createKeyboardConfiguration() -> VZKeyboardConfiguration {
if #available(macOS 14.0, *) {
return VZMacKeyboardConfiguration()
} else {
return VZUSBKeyboardConfiguration()
}
}
}
#endif

View File

@@ -1,53 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
Download the latest macOS restore image from the network.
*/
import Foundation
import Virtualization
#if arch(arm64)
class MacOSRestoreImage: NSObject {
private var downloadObserver: NSKeyValueObservation?
// MARK: Observe the download progress.
public func download(completionHandler: @escaping () -> Void) {
NSLog("Attempting to download latest available restore image.")
VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result<VZMacOSRestoreImage, Error>) in
switch result {
case let .failure(error):
fatalError(error.localizedDescription)
case let .success(restoreImage):
downloadRestoreImage(restoreImage: restoreImage, completionHandler: completionHandler)
}
}
}
// MARK: Download the restore image from the network.
private func downloadRestoreImage(restoreImage: VZMacOSRestoreImage, completionHandler: @escaping () -> Void) {
let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) { localURL, response, error in
if let error = error {
fatalError("Download failed. \(error.localizedDescription).")
}
guard (try? FileManager.default.moveItem(at: localURL!, to: restoreImageURL)) != nil else {
fatalError("Failed to move downloaded restore image to \(restoreImageURL).")
}
completionHandler()
}
downloadObserver = downloadTask.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in
NSLog("Restore image download progress: \(change.newValue! * 100).")
}
downloadTask.resume()
}
}
#endif

View File

@@ -1,169 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
A helper class to install a macOS virtual machine.
*/
import Virtualization
#if arch(arm64)
class MacOSVirtualMachineInstaller: NSObject {
private var installationObserver: NSKeyValueObservation?
private var virtualMachine: VZVirtualMachine!
private var virtualMachineResponder: VnetDelegate?
// Create a bundle on the user's Home directory to store any artifacts
// that the installation produces.
public func setUpVirtualMachineArtifacts() {
createVMBundle()
}
// MARK: Install macOS onto the virtual machine from IPSW.
public func installMacOS(ipswURL: URL) {
NSLog("Attempting to install from IPSW at \(ipswURL).")
VZMacOSRestoreImage.load(from: ipswURL, completionHandler: { [self](result: Result<VZMacOSRestoreImage, Error>) in
switch result {
case let .failure(error):
fatalError(error.localizedDescription)
case let .success(restoreImage):
installMacOS(restoreImage: restoreImage)
}
})
}
// MARK: - Internal helper functions.
private func installMacOS(restoreImage: VZMacOSRestoreImage) {
guard let macOSConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else {
fatalError("No supported configuration available.")
}
if !macOSConfiguration.hardwareModel.isSupported {
fatalError("macOSConfiguration configuration isn't supported on the current host.")
}
DispatchQueue.main.async { [self] in
setupVirtualMachine(macOSConfiguration: macOSConfiguration)
startInstallation(restoreImageURL: restoreImage.url)
}
}
// MARK: Create the Mac platform configuration.
private func createMacPlatformConfiguration(macOSConfiguration: VZMacOSConfigurationRequirements) -> VZMacPlatformConfiguration {
let macPlatformConfiguration = VZMacPlatformConfiguration()
guard let auxiliaryStorage = try? VZMacAuxiliaryStorage(creatingStorageAt: auxiliaryStorageURL,
hardwareModel: macOSConfiguration.hardwareModel,
options: []) else {
fatalError("Failed to create auxiliary storage.")
}
macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage
macPlatformConfiguration.hardwareModel = macOSConfiguration.hardwareModel
macPlatformConfiguration.machineIdentifier = VZMacMachineIdentifier()
// Store the hardware model and machine identifier to disk so that you
// can retrieve them for subsequent boots.
try! macPlatformConfiguration.hardwareModel.dataRepresentation.write(to: hardwareModelURL)
try! macPlatformConfiguration.machineIdentifier.dataRepresentation.write(to: machineIdentifierURL)
return macPlatformConfiguration
}
// MARK: Create the virtual machine configuration and instantiate the virtual machine.
private func setupVirtualMachine(macOSConfiguration: VZMacOSConfigurationRequirements) {
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
virtualMachineConfiguration.platform = createMacPlatformConfiguration(macOSConfiguration: macOSConfiguration)
virtualMachineConfiguration.cpuCount = VnetHostConfigHelper.computeCPUCount()
if virtualMachineConfiguration.cpuCount < macOSConfiguration.minimumSupportedCPUCount {
fatalError("CPUCount isn't supported by the macOS configuration.")
}
virtualMachineConfiguration.memorySize = VnetHostConfigHelper.computeMemorySize()
if virtualMachineConfiguration.memorySize < macOSConfiguration.minimumSupportedMemorySize {
fatalError("memorySize isn't supported by the macOS configuration.")
}
// Create a 128 GB disk image.
createDiskImage()
virtualMachineConfiguration.bootLoader = VnetHostConfigHelper.createBootLoader()
virtualMachineConfiguration.graphicsDevices = [VnetHostConfigHelper.createGraphicsDeviceConfiguration()]
virtualMachineConfiguration.storageDevices = [VnetHostConfigHelper.createBlockDeviceConfiguration()]
virtualMachineConfiguration.networkDevices = [VnetHostConfigHelper.createNetworkDeviceConfiguration()]
virtualMachineConfiguration.pointingDevices = [VnetHostConfigHelper.createPointingDeviceConfiguration()]
virtualMachineConfiguration.keyboards = [VnetHostConfigHelper.createKeyboardConfiguration()]
try! virtualMachineConfiguration.validate()
if #available(macOS 14.0, *) {
try! virtualMachineConfiguration.validateSaveRestoreSupport()
}
virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
virtualMachineResponder = VnetDelegate()
virtualMachine.delegate = virtualMachineResponder
}
// MARK: Begin macOS installation.
private func startInstallation(restoreImageURL: URL) {
let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: restoreImageURL)
NSLog("Starting installation.")
installer.install(completionHandler: { (result: Result<Void, Error>) in
if case let .failure(error) = result {
fatalError(error.localizedDescription)
} else {
NSLog("Installation succeeded.")
}
})
// Observe installation progress.
installationObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in
NSLog("Installation progress: \(change.newValue! * 100).")
}
}
private func createVMBundle() {
let bundleFd = mkdir(vmBundlePath, S_IRWXU | S_IRWXG | S_IRWXO)
if bundleFd == -1 {
if errno == EEXIST {
fatalError("Failed to create VM.bundle: the base directory already exists.")
}
fatalError("Failed to create VM.bundle.")
}
let result = close(bundleFd)
if result != 0 {
fatalError("Failed to close VM.bundle.")
}
}
// Create an empty disk image for the virtual machine.
private func createDiskImage() {
let diskFd = open(diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)
if diskFd == -1 {
fatalError("Cannot create disk image.")
}
// 128 GB disk space.
var result = ftruncate(diskFd, 128 * 1024 * 1024 * 1024)
if result != 0 {
fatalError("ftruncate() failed.")
}
result = close(diskFd)
if result != 0 {
fatalError("Failed to close the disk image.")
}
}
}
#endif

View File

@@ -1,44 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
The entry for `InstallationTool`.
*/
import Foundation
#if arch(arm64)
let installer = MacOSVirtualMachineInstaller()
if CommandLine.arguments.count == 2 {
let ipswPath = String(CommandLine.arguments[1])
let ipswURL = URL(fileURLWithPath: ipswPath)
guard ipswURL.isFileURL else {
fatalError("The provided IPSW path is not a valid file URL.")
}
installer.setUpVirtualMachineArtifacts()
installer.installMacOS(ipswURL: ipswURL)
dispatchMain()
} else if CommandLine.arguments.count == 1 {
installer.setUpVirtualMachineArtifacts()
let restoreImage = MacOSRestoreImage()
restoreImage.download {
// Install from the restore image that you downloaded.
installer.installMacOS(ipswURL: restoreImageURL)
}
dispatchMain()
} else {
NSLog("Invalid argument. Please either provide the path to an IPSW file, or run this tool without any argument.")
exit(-1)
}
#else
NSLog("This tool can only be run on Apple Silicon Macs.")
#endif

View File

@@ -1,196 +0,0 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
The app delegate that sets up and starts the virtual machine.
*/
import Cocoa
import Foundation
import Virtualization
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet var window: NSWindow!
@IBOutlet weak var virtualMachineView: VZVirtualMachineView!
private var virtualMachineResponder: VnetDelegate?
private var virtualMachine: VZVirtualMachine!
// MARK: Create the Mac platform configuration.
#if arch(arm64)
private func createMacPlaform() -> VZMacPlatformConfiguration {
let macPlatform = VZMacPlatformConfiguration()
let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: auxiliaryStorageURL)
macPlatform.auxiliaryStorage = auxiliaryStorage
if !FileManager.default.fileExists(atPath: vmBundlePath) {
fatalError("Missing Virtual Machine Bundle at \(vmBundlePath). Run InstallationTool first to create it.")
}
// Retrieve the hardware model and save this value to disk
// during installation.
guard let hardwareModelData = try? Data(contentsOf: hardwareModelURL) else {
fatalError("Failed to retrieve hardware model data.")
}
guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else {
fatalError("Failed to create hardware model.")
}
if !hardwareModel.isSupported {
fatalError("The hardware model isn't supported on the current host")
}
macPlatform.hardwareModel = hardwareModel
// Retrieve the machine identifier and save this value to disk
// during installation.
guard let machineIdentifierData = try? Data(contentsOf: machineIdentifierURL) else {
fatalError("Failed to retrieve machine identifier data.")
}
guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else {
fatalError("Failed to create machine identifier.")
}
macPlatform.machineIdentifier = machineIdentifier
return macPlatform
}
// MARK: Create the virtual machine configuration and instantiate the virtual machine.
private func createVirtualMachine() {
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
virtualMachineConfiguration.platform = createMacPlaform()
virtualMachineConfiguration.bootLoader = VnetHostConfigHelper.createBootLoader()
virtualMachineConfiguration.cpuCount = VnetHostConfigHelper.computeCPUCount()
virtualMachineConfiguration.memorySize = VnetHostConfigHelper.computeMemorySize()
virtualMachineConfiguration.graphicsDevices = [VnetHostConfigHelper.createGraphicsDeviceConfiguration()]
virtualMachineConfiguration.storageDevices = [VnetHostConfigHelper.createBlockDeviceConfiguration()]
virtualMachineConfiguration.networkDevices = [VnetHostConfigHelper.createNetworkDeviceConfiguration()]
virtualMachineConfiguration.pointingDevices = [VnetHostConfigHelper.createPointingDeviceConfiguration()]
virtualMachineConfiguration.keyboards = [VnetHostConfigHelper.createKeyboardConfiguration()]
try! virtualMachineConfiguration.validate()
if #available(macOS 14.0, *) {
try! virtualMachineConfiguration.validateSaveRestoreSupport()
}
virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
}
// MARK: Start or restore the virtual machine.
func startVirtualMachine() {
virtualMachine.start(completionHandler: { (result) in
if case let .failure(error) = result {
fatalError("Virtual machine failed to start with \(error)")
}
})
}
func resumeVirtualMachine() {
virtualMachine.resume(completionHandler: { (result) in
if case let .failure(error) = result {
fatalError("Virtual machine failed to resume with \(error)")
}
})
}
@available(macOS 14.0, *)
func restoreVirtualMachine() {
virtualMachine.restoreMachineStateFrom(url: saveFileURL, completionHandler: { [self] (error) in
// Remove the saved file. Whether success or failure, the state no longer matches the VM's disk.
let fileManager = FileManager.default
try! fileManager.removeItem(at: saveFileURL)
if error == nil {
self.resumeVirtualMachine()
} else {
self.startVirtualMachine()
}
})
}
#endif
func applicationDidFinishLaunching(_ aNotification: Notification) {
#if arch(arm64)
DispatchQueue.main.async { [self] in
createVirtualMachine()
virtualMachineResponder = VnetDelegate()
virtualMachine.delegate = virtualMachineResponder
virtualMachineView.virtualMachine = virtualMachine
virtualMachineView.capturesSystemKeys = true
if #available(macOS 14.0, *) {
// Configure the app to automatically respond to changes in the display size.
virtualMachineView.automaticallyReconfiguresDisplay = true
}
if #available(macOS 14.0, *) {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: saveFileURL.path) {
restoreVirtualMachine()
} else {
startVirtualMachine()
}
} else {
startVirtualMachine()
}
}
#endif
}
// MARK: Save the virtual machine when the app exits.
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
#if arch(arm64)
@available(macOS 14.0, *)
func saveVirtualMachine(completionHandler: @escaping () -> Void) {
virtualMachine.saveMachineStateTo(url: saveFileURL, completionHandler: { (error) in
guard error == nil else {
fatalError("Virtual machine failed to save with \(error!)")
}
completionHandler()
})
}
@available(macOS 14.0, *)
func pauseAndSaveVirtualMachine(completionHandler: @escaping () -> Void) {
virtualMachine.pause(completionHandler: { (result) in
if case let .failure(error) = result {
fatalError("Virtual machine failed to pause with \(error)")
}
self.saveVirtualMachine(completionHandler: completionHandler)
})
}
#endif
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
#if arch(arm64)
if #available(macOS 14.0, *) {
if virtualMachine.state == .running {
pauseAndSaveVirtualMachine(completionHandler: {
sender.reply(toApplicationShouldTerminate: true)
})
return .terminateLater
}
}
#endif
return .terminateNow
}
}

View File

@@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,58 +0,0 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,696 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="macOSVirtualMachineSampleApp_Swift" customModuleProvider="target">
<connections>
<outlet property="virtualMachineView" destination="EiT-Mj-1SZ" id="KBI-Ak-yeW"/>
<outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="vnetMacHost" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="vnetMacHost" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About vnetMacHost" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide vnetMacHost" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Save and quit vnetMacHost" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="bib-Uj-vzu">
<items>
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="newDocument:" target="-1" id="4Si-XN-c54"/>
</connections>
</menuItem>
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
<connections>
<action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
</connections>
</menuItem>
<menuItem title="Open Recent" id="tXI-mr-wws">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
<items>
<menuItem title="Clear Menu" id="vNY-rz-j42">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
<connections>
<action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
<connections>
<action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/>
</connections>
</menuItem>
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
<connections>
<action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/>
</connections>
</menuItem>
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
<connections>
<action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/>
</connections>
</menuItem>
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
<connections>
<action selector="print:" target="-1" id="qaZ-4w-aoO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Format" id="jxT-CU-nIS">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
<items>
<menuItem title="Font" id="Gi5-1S-RQB">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
<items>
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
<connections>
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
</connections>
</menuItem>
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
</connections>
</menuItem>
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
</connections>
</menuItem>
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
<connections>
<action selector="underline:" target="-1" id="FYS-2b-JAY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
</connections>
</menuItem>
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
<menuItem title="Kern" id="jBQ-r6-VK2">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
<items>
<menuItem title="Use Default" id="GUa-eO-cwY">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardKerning:" target="-1" id="6dk-9l-Ckg"/>
</connections>
</menuItem>
<menuItem title="Use None" id="cDB-IK-hbR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffKerning:" target="-1" id="U8a-gz-Maa"/>
</connections>
</menuItem>
<menuItem title="Tighten" id="46P-cB-AYj">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="tightenKerning:" target="-1" id="hr7-Nz-8ro"/>
</connections>
</menuItem>
<menuItem title="Loosen" id="ogc-rX-tC1">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="loosenKerning:" target="-1" id="8i4-f9-FKE"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ligatures" id="o6e-r0-MWq">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
<items>
<menuItem title="Use Default" id="agt-UL-0e3">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardLigatures:" target="-1" id="7uR-wd-Dx6"/>
</connections>
</menuItem>
<menuItem title="Use None" id="J7y-lM-qPV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffLigatures:" target="-1" id="iX2-gA-Ilz"/>
</connections>
</menuItem>
<menuItem title="Use All" id="xQD-1f-W4t">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useAllLigatures:" target="-1" id="KcB-kA-TuK"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Baseline" id="OaQ-X3-Vso">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
<items>
<menuItem title="Use Default" id="3Om-Ey-2VK">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unscript:" target="-1" id="0vZ-95-Ywn"/>
</connections>
</menuItem>
<menuItem title="Superscript" id="Rqc-34-cIF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="superscript:" target="-1" id="3qV-fo-wpU"/>
</connections>
</menuItem>
<menuItem title="Subscript" id="I0S-gh-46l">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="subscript:" target="-1" id="Q6W-4W-IGz"/>
</connections>
</menuItem>
<menuItem title="Raise" id="2h7-ER-AoG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="raiseBaseline:" target="-1" id="4sk-31-7Q9"/>
</connections>
</menuItem>
<menuItem title="Lower" id="1tx-W0-xDw">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowerBaseline:" target="-1" id="OF1-bc-KW4"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
<connections>
<action selector="orderFrontColorPanel:" target="-1" id="mSX-Xz-DV3"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="copyFont:" target="-1" id="GJO-xA-L4q"/>
</connections>
</menuItem>
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteFont:" target="-1" id="JfD-CL-leO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Text" id="Fal-I4-PZk">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Text" id="d9c-me-L2H">
<items>
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
<connections>
<action selector="alignLeft:" target="-1" id="zUv-R1-uAa"/>
</connections>
</menuItem>
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
<connections>
<action selector="alignCenter:" target="-1" id="spX-mk-kcS"/>
</connections>
</menuItem>
<menuItem title="Justify" id="J5U-5w-g23">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="alignJustified:" target="-1" id="ljL-7U-jND"/>
</connections>
</menuItem>
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
<connections>
<action selector="alignRight:" target="-1" id="r48-bG-YeY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
<menuItem title="Writing Direction" id="H1b-Si-o9J">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
<items>
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="YGs-j5-SAR">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="qtV-5e-UBP"/>
</connections>
</menuItem>
<menuItem id="Lbh-J2-qVU">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="S0X-9S-QSf"/>
</connections>
</menuItem>
<menuItem id="jFq-tB-4Kx">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="5fk-qB-AqJ"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="Nop-cj-93Q">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionNatural:" target="-1" id="lPI-Se-ZHp"/>
</connections>
</menuItem>
<menuItem id="BgM-ve-c93">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="caW-Bv-w94"/>
</connections>
</menuItem>
<menuItem id="RB4-Sm-HuC">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="EXD-6r-ZUu"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
<menuItem title="Show Ruler" id="vLm-3I-IUL">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleRuler:" target="-1" id="FOx-HJ-KwY"/>
</connections>
</menuItem>
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="copyRuler:" target="-1" id="71i-fW-3W2"/>
</connections>
</menuItem>
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="pasteRuler:" target="-1" id="cSh-wd-qM2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
</connections>
</menuItem>
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runToolbarCustomizationPalette:" target="-1" id="pQI-g3-MTW"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleSidebar:" target="-1" id="iwa-gc-5KM"/>
</connections>
</menuItem>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="vnetMacHost Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="200" y="121"/>
</menu>
<window title="vnetMacHost" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="335" y="390" width="960" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="EiT-Mj-1SZ" customClass="VZVirtualMachineView">
<rect key="frame" x="0.0" y="0.0" width="960" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<point key="canvasLocation" x="200" y="400"/>
</window>
</objects>
</document>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array/>
</plist>

View File

@@ -1,544 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
8F87D52126C34111000EADA4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D52026C34111000EADA4 /* AppDelegate.swift */; };
8F87D52326C34111000EADA4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F87D52226C34111000EADA4 /* Assets.xcassets */; };
8F87D52626C34111000EADA4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8F87D52426C34111000EADA4 /* MainMenu.xib */; };
8F87D53426C341AC000EADA4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53326C341AC000EADA4 /* main.swift */; };
8F87D53A26C3423F000EADA4 /* MacOSVirtualMachineInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53926C3423F000EADA4 /* MacOSVirtualMachineInstaller.swift */; };
8F87D54026C34259000EADA4 /* VnetHostConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53D26C34259000EADA4 /* VnetHostConfigHelper.swift */; };
8F87D54126C34259000EADA4 /* VnetDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53E26C34259000EADA4 /* VnetDelegate.swift */; };
8F87D54326C34265000EADA4 /* VnetDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53E26C34259000EADA4 /* VnetDelegate.swift */; };
8F87D54426C34269000EADA4 /* VnetHostConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53D26C34259000EADA4 /* VnetHostConfigHelper.swift */; };
8F87D54726C3427C000EADA4 /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F87D54626C3427C000EADA4 /* Virtualization.framework */; };
8F87D54826C34286000EADA4 /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F87D54626C3427C000EADA4 /* Virtualization.framework */; };
8FB90BEE26D5AC8100988F51 /* MacOSRestoreImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB90BED26D5AC8100988F51 /* MacOSRestoreImage.swift */; };
C266EA7F2C5D2AD800DC57E3 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C266EA7E2C5D2AD800DC57E3 /* Config.swift */; };
C266EA802C5D2AE700DC57E3 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C266EA7E2C5D2AD800DC57E3 /* Config.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
8F87D52F26C341AC000EADA4 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
8F87D51D26C34111000EADA4 /* vnetMacHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = vnetMacHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
8F87D52026C34111000EADA4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
8F87D52226C34111000EADA4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8F87D52526C34111000EADA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
8F87D53126C341AC000EADA4 /* InstallationTool-Swift */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "InstallationTool-Swift"; sourceTree = BUILT_PRODUCTS_DIR; };
8F87D53326C341AC000EADA4 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
8F87D53826C3423F000EADA4 /* InstallationTool.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = InstallationTool.entitlements; sourceTree = "<group>"; };
8F87D53926C3423F000EADA4 /* MacOSVirtualMachineInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacOSVirtualMachineInstaller.swift; sourceTree = "<group>"; };
8F87D53B26C34250000EADA4 /* vnetMacHost.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = vnetMacHost.entitlements; sourceTree = "<group>"; };
8F87D53D26C34259000EADA4 /* VnetHostConfigHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VnetHostConfigHelper.swift; sourceTree = "<group>"; };
8F87D53E26C34259000EADA4 /* VnetDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VnetDelegate.swift; sourceTree = "<group>"; };
8F87D54626C3427C000EADA4 /* Virtualization.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Virtualization.framework; path = System/Library/Frameworks/Virtualization.framework; sourceTree = SDKROOT; };
8FB90BE826D422FD00988F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8FB90BED26D5AC8100988F51 /* MacOSRestoreImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacOSRestoreImage.swift; sourceTree = "<group>"; };
B0E246092DFBF28FAEA2709F /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
C266EA7E2C5D2AD800DC57E3 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = SampleCode.xcconfig; path = Configuration/SampleCode.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8F87D51A26C34111000EADA4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8F87D54826C34286000EADA4 /* Virtualization.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
8F87D52E26C341AC000EADA4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8F87D54726C3427C000EADA4 /* Virtualization.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
09E329497FB7E44895839D88 /* LICENSE */ = {
isa = PBXGroup;
children = (
B0E246092DFBF28FAEA2709F /* LICENSE.txt */,
);
path = LICENSE;
sourceTree = "<group>";
};
3026A0D16D3A077128FE4194 /* Configuration */ = {
isa = PBXGroup;
children = (
F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */,
);
name = Configuration;
sourceTree = "<group>";
};
8F87D51426C34111000EADA4 = {
isa = PBXGroup;
children = (
8F87D53B26C34250000EADA4 /* vnetMacHost.entitlements */,
8F87D53826C3423F000EADA4 /* InstallationTool.entitlements */,
8FDABC17270D0F9100D7FC60 /* Swift */,
8F87D51E26C34111000EADA4 /* Products */,
8F87D54526C3427C000EADA4 /* Frameworks */,
3026A0D16D3A077128FE4194 /* Configuration */,
09E329497FB7E44895839D88 /* LICENSE */,
);
sourceTree = "<group>";
};
8F87D51E26C34111000EADA4 /* Products */ = {
isa = PBXGroup;
children = (
8F87D51D26C34111000EADA4 /* vnetMacHost.app */,
8F87D53126C341AC000EADA4 /* InstallationTool-Swift */,
);
name = Products;
sourceTree = "<group>";
};
8F87D51F26C34111000EADA4 /* vnetMacHost */ = {
isa = PBXGroup;
children = (
8F87D52026C34111000EADA4 /* AppDelegate.swift */,
8F87D52226C34111000EADA4 /* Assets.xcassets */,
8F87D52426C34111000EADA4 /* MainMenu.xib */,
8FB90BE826D422FD00988F51 /* Info.plist */,
);
path = vnetMacHost;
sourceTree = "<group>";
};
8F87D52C26C3418F000EADA4 /* Common */ = {
isa = PBXGroup;
children = (
C266EA7E2C5D2AD800DC57E3 /* Config.swift */,
8F87D53D26C34259000EADA4 /* VnetHostConfigHelper.swift */,
8F87D53E26C34259000EADA4 /* VnetDelegate.swift */,
);
path = Common;
sourceTree = "<group>";
};
8F87D53226C341AC000EADA4 /* InstallationTool */ = {
isa = PBXGroup;
children = (
8FB90BED26D5AC8100988F51 /* MacOSRestoreImage.swift */,
8F87D53926C3423F000EADA4 /* MacOSVirtualMachineInstaller.swift */,
8F87D53326C341AC000EADA4 /* main.swift */,
);
path = InstallationTool;
sourceTree = "<group>";
};
8F87D54526C3427C000EADA4 /* Frameworks */ = {
isa = PBXGroup;
children = (
8F87D54626C3427C000EADA4 /* Virtualization.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
8FDABC17270D0F9100D7FC60 /* Swift */ = {
isa = PBXGroup;
children = (
8F87D52C26C3418F000EADA4 /* Common */,
8F87D51F26C34111000EADA4 /* vnetMacHost */,
8F87D53226C341AC000EADA4 /* InstallationTool */,
);
path = Swift;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
8F87D51C26C34111000EADA4 /* vnetMacHost-Swift */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8F87D52926C34111000EADA4 /* Build configuration list for PBXNativeTarget "vnetMacHost-Swift" */;
buildPhases = (
8F87D51926C34111000EADA4 /* Sources */,
8F87D51A26C34111000EADA4 /* Frameworks */,
8F87D51B26C34111000EADA4 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "vnetMacHost-Swift";
productName = macOSVirtualMachineSampleApp;
productReference = 8F87D51D26C34111000EADA4 /* vnetMacHost.app */;
productType = "com.apple.product-type.application";
};
8F87D53026C341AC000EADA4 /* InstallationTool-Swift */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8F87D53526C341AC000EADA4 /* Build configuration list for PBXNativeTarget "InstallationTool-Swift" */;
buildPhases = (
8F87D52D26C341AC000EADA4 /* Sources */,
8F87D52E26C341AC000EADA4 /* Frameworks */,
8F87D52F26C341AC000EADA4 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = "InstallationTool-Swift";
productName = InstallationTool;
productReference = 8F87D53126C341AC000EADA4 /* InstallationTool-Swift */;
productType = "com.apple.product-type.tool";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8F87D51526C34111000EADA4 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
DefaultBuildSystemTypeForWorkspace = Latest;
LastSwiftUpdateCheck = 1300;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = Apple;
TargetAttributes = {
8F87D51C26C34111000EADA4 = {
CreatedOnToolsVersion = 13.0;
};
8F87D53026C341AC000EADA4 = {
CreatedOnToolsVersion = 13.0;
};
};
};
buildConfigurationList = 8F87D51826C34111000EADA4 /* Build configuration list for PBXProject "vnetMacHost" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 8F87D51426C34111000EADA4;
productRefGroup = 8F87D51E26C34111000EADA4 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
8F87D51C26C34111000EADA4 /* vnetMacHost-Swift */,
8F87D53026C341AC000EADA4 /* InstallationTool-Swift */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8F87D51B26C34111000EADA4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8F87D52326C34111000EADA4 /* Assets.xcassets in Resources */,
8F87D52626C34111000EADA4 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8F87D51926C34111000EADA4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8F87D52126C34111000EADA4 /* AppDelegate.swift in Sources */,
8F87D54126C34259000EADA4 /* VnetDelegate.swift in Sources */,
C266EA7F2C5D2AD800DC57E3 /* Config.swift in Sources */,
8F87D54026C34259000EADA4 /* VnetHostConfigHelper.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
8F87D52D26C341AC000EADA4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8F87D54426C34269000EADA4 /* VnetHostConfigHelper.swift in Sources */,
C266EA802C5D2AE700DC57E3 /* Config.swift in Sources */,
8F87D53A26C3423F000EADA4 /* MacOSVirtualMachineInstaller.swift in Sources */,
8FB90BEE26D5AC8100988F51 /* MacOSRestoreImage.swift in Sources */,
8F87D54326C34265000EADA4 /* VnetDelegate.swift in Sources */,
8F87D53426C341AC000EADA4 /* main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
8F87D52426C34111000EADA4 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
8F87D52526C34111000EADA4 /* Base */,
);
name = MainMenu.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
8F87D52726C34111000EADA4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
8F87D52826C34111000EADA4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
8F87D52A26C34111000EADA4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = vnetMacHost.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Swift/vnetMacHost/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Allow for using audio input devices.";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHost;
PRODUCT_NAME = vnetMacHost;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
8F87D52B26C34111000EADA4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = vnetMacHost.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Swift/vnetMacHost/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Allow for using audio input devices.";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHost;
PRODUCT_NAME = vnetMacHost;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
8F87D53626C341AC000EADA4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = InstallationTool.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_USER_SELECTED_FILES = readwrite;
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHostSetupTool;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
8F87D53726C341AC000EADA4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4E72614B21833A4E0FE3E98 /* SampleCode.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = InstallationTool.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_USER_SELECTED_FILES = readwrite;
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHostSetupTool;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
8F87D51826C34111000EADA4 /* Build configuration list for PBXProject "vnetMacHost" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8F87D52726C34111000EADA4 /* Debug */,
8F87D52826C34111000EADA4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8F87D52926C34111000EADA4 /* Build configuration list for PBXNativeTarget "vnetMacHost-Swift" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8F87D52A26C34111000EADA4 /* Debug */,
8F87D52B26C34111000EADA4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8F87D53526C341AC000EADA4 /* Build configuration list for PBXNativeTarget "InstallationTool-Swift" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8F87D53626C341AC000EADA4 /* Debug */,
8F87D53726C341AC000EADA4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 8F87D51526C34111000EADA4 /* Project object */;
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Latest</string>
</dict>
</plist>

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
BuildableName = "InstallationTool-Swift"
BlueprintName = "InstallationTool-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
BuildableName = "InstallationTool-Swift"
BlueprintName = "InstallationTool-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
BuildableName = "InstallationTool-Swift"
BlueprintName = "InstallationTool-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
BuildableName = "vnetMacHost.app"
BlueprintName = "vnetMacHost-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
BuildableName = "vnetMacHost.app"
BlueprintName = "vnetMacHost-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
BuildableName = "vnetMacHost.app"
BlueprintName = "vnetMacHost-Swift"
ReferencedContainer = "container:vnetMacHost.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>8FDABC39270D1DC600D7FC60</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>8FDABC58270D1FFE00D7FC60</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View File

@@ -97,7 +97,7 @@ func (d *Detector) detectCaptivePortalWithGOOS(ctx context.Context, netMon *netm
d.logf("[v2] attempting to do captive portal detection on interface %s", ifName)
res := d.detectOnInterface(ctx, i.Index, endpoints)
if res {
d.logf("DetectCaptivePortal(found=true,ifName=%s)", found, ifName)
d.logf("DetectCaptivePortal(found=true,ifName=%s)", ifName)
return true
}
}
@@ -106,13 +106,19 @@ func (d *Detector) detectCaptivePortalWithGOOS(ctx context.Context, netMon *netm
return false
}
// interfaceNameDoesNotNeedCaptiveDetection returns true if an interface does not require captive portal detection
// based on its name. This is useful to avoid making unnecessary HTTP requests on interfaces that are known to not
// require it. We also avoid making requests on the interface prefixes "pdp" and "rmnet", which are cellular data
// interfaces on iOS and Android, respectively, and would be needlessly battery-draining.
func interfaceNameDoesNotNeedCaptiveDetection(ifName string, goos string) bool {
ifName = strings.ToLower(ifName)
excludedPrefixes := []string{"tailscale", "tun", "tap", "docker", "kube", "wg"}
if goos == "windows" {
excludedPrefixes = append(excludedPrefixes, "loopback", "tunnel", "ppp", "isatap", "teredo", "6to4")
} else if goos == "darwin" || goos == "ios" {
excludedPrefixes = append(excludedPrefixes, "awdl", "bridge", "ap", "utun", "tap", "llw", "anpi", "lo", "stf", "gif", "xhc")
excludedPrefixes = append(excludedPrefixes, "pdp", "awdl", "bridge", "ap", "utun", "tap", "llw", "anpi", "lo", "stf", "gif", "xhc", "pktap")
} else if goos == "android" {
excludedPrefixes = append(excludedPrefixes, "rmnet", "p2p", "dummy", "sit")
}
for _, prefix := range excludedPrefixes {
if strings.HasPrefix(ifName, prefix) {

View File

@@ -9,6 +9,7 @@ import (
"sync"
"testing"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/net/netmon"
)
@@ -36,6 +37,7 @@ func TestDetectCaptivePortalReturnsFalse(t *testing.T) {
}
func TestAllEndpointsAreUpAndReturnExpectedResponse(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/13019")
d := NewDetector(t.Logf)
endpoints := availableEndpoints(nil, 0, t.Logf, runtime.GOOS)

View File

@@ -538,7 +538,9 @@ func (m *Manager) FlushCaches() error {
// CleanUp restores the system DNS configuration to its original state
// in case the Tailscale daemon terminated without closing the router.
// No other state needs to be instantiated before this runs.
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
//
// health must not be nil
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, health *health.Tracker, interfaceName string) {
oscfg, err := NewOSConfigurator(logf, nil, nil, interfaceName)
if err != nil {
logf("creating dns cleanup: %v", err)
@@ -546,7 +548,7 @@ func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
}
d := &tsdial.Dialer{Logf: logf}
d.SetNetMon(netMon)
dns := NewManager(logf, oscfg, nil, d, nil, nil, runtime.GOOS)
dns := NewManager(logf, oscfg, health, d, nil, nil, runtime.GOOS)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -281,6 +281,7 @@ func lookup(ctx context.Context, host string, logf logger.Logf, ht *health.Track
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) (dnsMap, error) {
dialer := netns.NewDialer(logf, netMon)
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.DisableKeepAlives = true // This transport is meant to be used once.
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))

View File

@@ -92,7 +92,9 @@ func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string)
// If the address doesn't parse, use the default index.
addr, err := parseAddress(address)
if err != nil {
logf("[unexpected] netns: error parsing address %q: %v", address, err)
if err != errUnspecifiedHost {
logf("[unexpected] netns: error parsing address %q: %v", address, err)
}
return defaultIdx()
}

View File

@@ -6,16 +6,22 @@
package netns
import (
"errors"
"net"
"net/netip"
)
var errUnspecifiedHost = errors.New("unspecified host")
func parseAddress(address string) (addr netip.Addr, err error) {
host, _, err := net.SplitHostPort(address)
if err != nil {
// error means the string didn't contain a port number, so use the string directly
host = address
}
if host == "" {
return addr, errUnspecifiedHost
}
return netip.ParseAddr(host)
}

View File

@@ -86,23 +86,26 @@ func controlC(logf logger.Logf, network, address string, c syscall.RawConn) (err
var ifaceIdxV4, ifaceIdxV6 uint32
if useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv(); useRoute {
addr, err := parseAddress(address)
if err != nil {
return fmt.Errorf("parseAddress: %w", err)
}
if canV4 && (addr.Is4() || addr.Is4In6()) {
addrV4 := addr.Unmap()
ifaceIdxV4, err = getInterfaceIndex(logf, addrV4, defIfaceIdxV4)
if err != nil {
return fmt.Errorf("getInterfaceIndex(%v): %w", addrV4, err)
if err == nil {
if canV4 && (addr.Is4() || addr.Is4In6()) {
addrV4 := addr.Unmap()
ifaceIdxV4, err = getInterfaceIndex(logf, addrV4, defIfaceIdxV4)
if err != nil {
return fmt.Errorf("getInterfaceIndex(%v): %w", addrV4, err)
}
}
}
if canV6 && addr.Is6() {
ifaceIdxV6, err = getInterfaceIndex(logf, addr, defIfaceIdxV6)
if err != nil {
return fmt.Errorf("getInterfaceIndex(%v): %w", addr, err)
if canV6 && addr.Is6() {
ifaceIdxV6, err = getInterfaceIndex(logf, addr, defIfaceIdxV6)
if err != nil {
return fmt.Errorf("getInterfaceIndex(%v): %w", addr, err)
}
}
} else {
if err != errUnspecifiedHost {
logf("[unexpected] netns: error parsing address %q: %v", address, err)
}
ifaceIdxV4, ifaceIdxV6 = defIfaceIdxV4, defIfaceIdxV6
}
} else {
ifaceIdxV4, ifaceIdxV6 = defIfaceIdxV4, defIfaceIdxV6

View File

@@ -61,7 +61,7 @@ func UpdateDstAddr(q *packet.Parsed, dst netip.Addr) {
b := q.Buffer()
if dst.Is6() {
v6 := dst.As16()
copy(b[24:36], v6[:])
copy(b[24:40], v6[:])
updateV6PacketChecksums(q, old, dst)
} else {
v4 := dst.As4()

View File

@@ -5,6 +5,7 @@ package checksum
import (
"encoding/binary"
"math/rand/v2"
"net/netip"
"testing"
@@ -94,7 +95,7 @@ func TestHeaderChecksumsV4(t *testing.T) {
}
func TestNatChecksumsV6UDP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
a1, a2 := randV6Addr(), randV6Addr()
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
@@ -124,25 +125,43 @@ func TestNatChecksumsV6UDP(t *testing.T) {
}
// Parse the packet.
var p packet.Parsed
var p, p2 packet.Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
UpdateSrcAddr(&p, a2)
p2.Decode(p.Buffer())
if p2.Src.Addr() != a2 {
t.Fatalf("got %v, want %v", p2.Src, a2)
}
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
UpdateDstAddr(&p, a1)
p2.Decode(p.Buffer())
if p2.Dst.Addr() != a1 {
t.Fatalf("got %v, want %v", p2.Dst, a1)
}
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating destination address")
}
}
func randV6Addr() netip.Addr {
a1, a2 := rand.Int64(), rand.Int64()
return netip.AddrFrom16([16]byte{
byte(a1 >> 56), byte(a1 >> 48), byte(a1 >> 40), byte(a1 >> 32),
byte(a1 >> 24), byte(a1 >> 16), byte(a1 >> 8), byte(a1),
byte(a2 >> 56), byte(a2 >> 48), byte(a2 >> 40), byte(a2 >> 32),
byte(a2 >> 24), byte(a2 >> 16), byte(a2 >> 8), byte(a2),
})
}
func TestNatChecksumsV6TCP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
a1, a2 := randV6Addr(), randV6Addr()
// Make a fake TCP packet with no payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
@@ -178,18 +197,26 @@ func TestNatChecksumsV6TCP(t *testing.T) {
}
// Parse the packet.
var p packet.Parsed
var p, p2 packet.Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
UpdateSrcAddr(&p, a2)
p2.Decode(p.Buffer())
if p2.Src.Addr() != a2 {
t.Fatalf("got %v, want %v", p2.Src, a2)
}
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
UpdateDstAddr(&p, a1)
p2.Decode(p.Buffer())
if p2.Dst.Addr() != a1 {
t.Fatalf("got %v, want %v", p2.Dst, a1)
}
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating destination address")
}

View File

@@ -13,8 +13,10 @@
package socks5
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
@@ -121,7 +123,7 @@ func (s *Server) Serve(l net.Listener) error {
}
go func() {
defer c.Close()
conn := &Conn{clientConn: c, srv: s}
conn := &Conn{logf: s.Logf, clientConn: c, srv: s}
err := conn.Run()
if err != nil {
s.logf("client connection failed: %v", err)
@@ -136,9 +138,12 @@ type Conn struct {
// The struct is filled by each of the internal
// methods in turn as the transaction progresses.
logf logger.Logf
srv *Server
clientConn net.Conn
request *request
udpClientAddr net.Addr
}
// Run starts the new connection.
@@ -172,58 +177,59 @@ func (c *Conn) Run() error {
func (c *Conn) handleRequest() error {
req, err := parseClientRequest(c.clientConn)
if err != nil {
res := &response{reply: generalFailure}
res := errorResponse(generalFailure)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
if req.command != connect {
res := &response{reply: commandNotSupported}
c.request = req
switch req.command {
case connect:
return c.handleTCP()
case udpAssociate:
return c.handleUDP()
default:
res := errorResponse(commandNotSupported)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return fmt.Errorf("unsupported command %v", req.command)
}
c.request = req
}
func (c *Conn) handleTCP() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv, err := c.srv.dial(
ctx,
"tcp",
net.JoinHostPort(c.request.destination, strconv.Itoa(int(c.request.port))),
c.request.destination.hostPort(),
)
if err != nil {
res := &response{reply: generalFailure}
res := errorResponse(generalFailure)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
defer srv.Close()
serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String())
localAddr := srv.LocalAddr().String()
serverAddr, serverPort, err := splitHostPort(localAddr)
if err != nil {
return err
}
serverPort, _ := strconv.Atoi(serverPortStr)
var bindAddrType addrType
if ip := net.ParseIP(serverAddr); ip != nil {
if ip.To4() != nil {
bindAddrType = ipv4
} else {
bindAddrType = ipv6
}
} else {
bindAddrType = domainName
}
res := &response{
reply: success,
bindAddrType: bindAddrType,
bindAddr: serverAddr,
bindPort: uint16(serverPort),
reply: success,
bindAddr: socksAddr{
addrType: getAddrType(serverAddr),
addr: serverAddr,
port: serverPort,
},
}
buf, err := res.marshal()
if err != nil {
res = &response{reply: generalFailure}
res = errorResponse(generalFailure)
buf, _ = res.marshal()
}
c.clientConn.Write(buf)
@@ -246,6 +252,208 @@ func (c *Conn) handleRequest() error {
return <-errc
}
func (c *Conn) handleUDP() error {
// The DST.ADDR and DST.PORT fields contain the address and port that
// the client expects to use to send UDP datagrams on for the
// association. The server MAY use this information to limit access
// to the association.
// @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928.
//
// We do NOT limit the access from the client currently in this implementation.
_ = c.request.destination
addr := c.clientConn.LocalAddr()
host, _, err := net.SplitHostPort(addr.String())
if err != nil {
return err
}
clientUDPConn, err := net.ListenPacket("udp", net.JoinHostPort(host, "0"))
if err != nil {
res := errorResponse(generalFailure)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
defer clientUDPConn.Close()
serverUDPConn, err := net.ListenPacket("udp", "[::]:0")
if err != nil {
res := errorResponse(generalFailure)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
defer serverUDPConn.Close()
bindAddr, bindPort, err := splitHostPort(clientUDPConn.LocalAddr().String())
if err != nil {
return err
}
res := &response{
reply: success,
bindAddr: socksAddr{
addrType: getAddrType(bindAddr),
addr: bindAddr,
port: bindPort,
},
}
buf, err := res.marshal()
if err != nil {
res = errorResponse(generalFailure)
buf, _ = res.marshal()
}
c.clientConn.Write(buf)
return c.transferUDP(c.clientConn, clientUDPConn, serverUDPConn)
}
func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, targetConn net.PacketConn) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const bufferSize = 8 * 1024
const readTimeout = 5 * time.Second
// client -> target
go func() {
defer cancel()
buf := make([]byte, bufferSize)
for {
select {
case <-ctx.Done():
return
default:
err := c.handleUDPRequest(clientConn, targetConn, buf, readTimeout)
if err != nil {
if isTimeout(err) {
continue
}
if errors.Is(err, net.ErrClosed) {
return
}
c.logf("udp transfer: handle udp request fail: %v", err)
}
}
}
}()
// target -> client
go func() {
defer cancel()
buf := make([]byte, bufferSize)
for {
select {
case <-ctx.Done():
return
default:
err := c.handleUDPResponse(targetConn, clientConn, buf, readTimeout)
if err != nil {
if isTimeout(err) {
continue
}
if errors.Is(err, net.ErrClosed) {
return
}
c.logf("udp transfer: handle udp response fail: %v", err)
}
}
}
}()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
_, err := io.Copy(io.Discard, associatedTCP)
if err != nil {
err = fmt.Errorf("udp associated tcp conn: %w", err)
}
return err
}
func (c *Conn) handleUDPRequest(
clientConn net.PacketConn,
targetConn net.PacketConn,
buf []byte,
readTimeout time.Duration,
) error {
// add a deadline for the read to avoid blocking forever
_ = clientConn.SetReadDeadline(time.Now().Add(readTimeout))
n, addr, err := clientConn.ReadFrom(buf)
if err != nil {
return fmt.Errorf("read from client: %w", err)
}
c.udpClientAddr = addr
req, data, err := parseUDPRequest(buf[:n])
if err != nil {
return fmt.Errorf("parse udp request: %w", err)
}
targetAddr, err := net.ResolveUDPAddr("udp", req.addr.hostPort())
if err != nil {
c.logf("resolve target addr fail: %v", err)
}
nn, err := targetConn.WriteTo(data, targetAddr)
if err != nil {
return fmt.Errorf("write to target %s fail: %w", targetAddr, err)
}
if nn != len(data) {
return fmt.Errorf("write to target %s fail: %w", targetAddr, io.ErrShortWrite)
}
return nil
}
func (c *Conn) handleUDPResponse(
targetConn net.PacketConn,
clientConn net.PacketConn,
buf []byte,
readTimeout time.Duration,
) error {
// add a deadline for the read to avoid blocking forever
_ = targetConn.SetReadDeadline(time.Now().Add(readTimeout))
n, addr, err := targetConn.ReadFrom(buf)
if err != nil {
return fmt.Errorf("read from target: %w", err)
}
host, port, err := splitHostPort(addr.String())
if err != nil {
return fmt.Errorf("split host port: %w", err)
}
hdr := udpRequest{addr: socksAddr{addrType: getAddrType(host), addr: host, port: port}}
pkt, err := hdr.marshal()
if err != nil {
return fmt.Errorf("marshal udp request: %w", err)
}
data := append(pkt, buf[:n]...)
// use addr from client to send back
nn, err := clientConn.WriteTo(data, c.udpClientAddr)
if err != nil {
return fmt.Errorf("write to client: %w", err)
}
if nn != len(data) {
return fmt.Errorf("write to client: %w", io.ErrShortWrite)
}
return nil
}
func isTimeout(err error) bool {
terr, ok := errors.Unwrap(err).(interface{ Timeout() bool })
return ok && terr.Timeout()
}
func splitHostPort(hostport string) (host string, port uint16, err error) {
host, portStr, err := net.SplitHostPort(hostport)
if err != nil {
return "", 0, err
}
portInt, err := strconv.Atoi(portStr)
if err != nil {
return "", 0, err
}
if portInt < 0 || portInt > 65535 {
return "", 0, fmt.Errorf("invalid port number %d", portInt)
}
return host, uint16(portInt), nil
}
// parseClientGreeting parses a request initiation packet.
func parseClientGreeting(r io.Reader, authMethod byte) error {
var hdr [2]byte
@@ -295,114 +503,118 @@ func parseClientAuth(r io.Reader) (usr, pwd string, err error) {
return string(usrBytes), string(pwdBytes), nil
}
func getAddrType(addr string) addrType {
if ip := net.ParseIP(addr); ip != nil {
if ip.To4() != nil {
return ipv4
}
return ipv6
}
return domainName
}
// request represents data contained within a SOCKS5
// connection request packet.
type request struct {
command commandType
destination string
port uint16
destAddrType addrType
command commandType
destination socksAddr
}
// parseClientRequest converts raw packet bytes into a
// SOCKS5Request struct.
func parseClientRequest(r io.Reader) (*request, error) {
var hdr [4]byte
var hdr [3]byte
_, err := io.ReadFull(r, hdr[:])
if err != nil {
return nil, fmt.Errorf("could not read packet header")
}
cmd := hdr[1]
destAddrType := addrType(hdr[3])
destination, err := parseSocksAddr(r)
return &request{
command: commandType(cmd),
destination: destination,
}, err
}
type socksAddr struct {
addrType addrType
addr string
port uint16
}
var zeroSocksAddr = socksAddr{addrType: ipv4, addr: "0.0.0.0", port: 0}
func parseSocksAddr(r io.Reader) (addr socksAddr, err error) {
var addrTypeData [1]byte
_, err = io.ReadFull(r, addrTypeData[:])
if err != nil {
return socksAddr{}, fmt.Errorf("could not read address type")
}
dstAddrType := addrType(addrTypeData[0])
var destination string
var port uint16
if destAddrType == ipv4 {
switch dstAddrType {
case ipv4:
var ip [4]byte
_, err = io.ReadFull(r, ip[:])
if err != nil {
return nil, fmt.Errorf("could not read IPv4 address")
return socksAddr{}, fmt.Errorf("could not read IPv4 address")
}
destination = net.IP(ip[:]).String()
} else if destAddrType == domainName {
case domainName:
var dstSizeByte [1]byte
_, err = io.ReadFull(r, dstSizeByte[:])
if err != nil {
return nil, fmt.Errorf("could not read domain name size")
return socksAddr{}, fmt.Errorf("could not read domain name size")
}
dstSize := int(dstSizeByte[0])
domainName := make([]byte, dstSize)
_, err = io.ReadFull(r, domainName)
if err != nil {
return nil, fmt.Errorf("could not read domain name")
return socksAddr{}, fmt.Errorf("could not read domain name")
}
destination = string(domainName)
} else if destAddrType == ipv6 {
case ipv6:
var ip [16]byte
_, err = io.ReadFull(r, ip[:])
if err != nil {
return nil, fmt.Errorf("could not read IPv6 address")
return socksAddr{}, fmt.Errorf("could not read IPv6 address")
}
destination = net.IP(ip[:]).String()
} else {
return nil, fmt.Errorf("unsupported address type")
default:
return socksAddr{}, fmt.Errorf("unsupported address type")
}
var portBytes [2]byte
_, err = io.ReadFull(r, portBytes[:])
if err != nil {
return nil, fmt.Errorf("could not read port")
return socksAddr{}, fmt.Errorf("could not read port")
}
port = binary.BigEndian.Uint16(portBytes[:])
return &request{
command: commandType(cmd),
destination: destination,
port: port,
destAddrType: destAddrType,
port := binary.BigEndian.Uint16(portBytes[:])
return socksAddr{
addrType: dstAddrType,
addr: destination,
port: port,
}, nil
}
// response contains the contents of
// a response packet sent from the proxy
// to the client.
type response struct {
reply replyCode
bindAddrType addrType
bindAddr string
bindPort uint16
}
// marshal converts a SOCKS5Response struct into
// a packet. If res.reply == Success, it may throw an error on
// receiving an invalid bind address. Otherwise, it will not throw.
func (res *response) marshal() ([]byte, error) {
pkt := make([]byte, 4)
pkt[0] = socks5Version
pkt[1] = byte(res.reply)
pkt[2] = 0 // null reserved byte
pkt[3] = byte(res.bindAddrType)
if res.reply != success {
return pkt, nil
}
func (s socksAddr) marshal() ([]byte, error) {
var addr []byte
switch res.bindAddrType {
switch s.addrType {
case ipv4:
addr = net.ParseIP(res.bindAddr).To4()
addr = net.ParseIP(s.addr).To4()
if addr == nil {
return nil, fmt.Errorf("invalid IPv4 address for binding")
}
case domainName:
if len(res.bindAddr) > 255 {
if len(s.addr) > 255 {
return nil, fmt.Errorf("invalid domain name for binding")
}
addr = make([]byte, 0, len(res.bindAddr)+1)
addr = append(addr, byte(len(res.bindAddr)))
addr = append(addr, []byte(res.bindAddr)...)
addr = make([]byte, 0, len(s.addr)+1)
addr = append(addr, byte(len(s.addr)))
addr = append(addr, []byte(s.addr)...)
case ipv6:
addr = net.ParseIP(res.bindAddr).To16()
addr = net.ParseIP(s.addr).To16()
if addr == nil {
return nil, fmt.Errorf("invalid IPv6 address for binding")
}
@@ -410,8 +622,86 @@ func (res *response) marshal() ([]byte, error) {
return nil, fmt.Errorf("unsupported address type")
}
pkt := []byte{byte(s.addrType)}
pkt = append(pkt, addr...)
pkt = binary.BigEndian.AppendUint16(pkt, uint16(res.bindPort))
pkt = binary.BigEndian.AppendUint16(pkt, s.port)
return pkt, nil
}
func (s socksAddr) hostPort() string {
return net.JoinHostPort(s.addr, strconv.Itoa(int(s.port)))
}
// response contains the contents of
// a response packet sent from the proxy
// to the client.
type response struct {
reply replyCode
bindAddr socksAddr
}
func errorResponse(code replyCode) *response {
return &response{reply: code, bindAddr: zeroSocksAddr}
}
// marshal converts a SOCKS5Response struct into
// a packet. If res.reply == Success, it may throw an error on
// receiving an invalid bind address. Otherwise, it will not throw.
func (res *response) marshal() ([]byte, error) {
pkt := make([]byte, 3)
pkt[0] = socks5Version
pkt[1] = byte(res.reply)
pkt[2] = 0 // null reserved byte
addrPkt, err := res.bindAddr.marshal()
if err != nil {
return nil, err
}
return append(pkt, addrPkt...), nil
}
type udpRequest struct {
frag byte
addr socksAddr
}
// +----+------+------+----------+----------+----------+
// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
// +----+------+------+----------+----------+----------+
// | 2 | 1 | 1 | Variable | 2 | Variable |
// +----+------+------+----------+----------+----------+
func parseUDPRequest(data []byte) (*udpRequest, []byte, error) {
if len(data) < 4 {
return nil, nil, fmt.Errorf("invalid packet length")
}
// reserved bytes
if !(data[0] == 0 && data[1] == 0) {
return nil, nil, fmt.Errorf("invalid udp request header")
}
frag := data[2]
reader := bytes.NewReader(data[3:])
addr, err := parseSocksAddr(reader)
bodyLen := reader.Len() // (*bytes.Reader).Len() return unread data length
body := data[len(data)-bodyLen:]
return &udpRequest{
frag: frag,
addr: addr,
}, body, err
}
func (u *udpRequest) marshal() ([]byte, error) {
pkt := make([]byte, 3)
pkt[0] = 0
pkt[1] = 0
pkt[2] = u.frag
addrPkt, err := u.addr.marshal()
if err != nil {
return nil, err
}
return append(pkt, addrPkt...), nil
}

View File

@@ -4,6 +4,7 @@
package socks5
import (
"bytes"
"errors"
"fmt"
"io"
@@ -32,6 +33,19 @@ func backendServer(listener net.Listener) {
listener.Close()
}
func udpEchoServer(conn net.PacketConn) {
var buf [1024]byte
n, addr, err := conn.ReadFrom(buf[:])
if err != nil {
panic(err)
}
_, err = conn.WriteTo(buf[:n], addr)
if err != nil {
panic(err)
}
conn.Close()
}
func TestRead(t *testing.T) {
// backend server which we'll use SOCKS5 to connect to
listener, err := net.Listen("tcp", ":0")
@@ -152,3 +166,102 @@ func TestReadPassword(t *testing.T) {
t.Fatal(err)
}
}
func TestUDP(t *testing.T) {
// backend UDP server which we'll use SOCKS5 to connect to
listener, err := net.ListenPacket("udp", ":0")
if err != nil {
t.Fatal(err)
}
backendServerPort := listener.LocalAddr().(*net.UDPAddr).Port
go udpEchoServer(listener)
// SOCKS5 server
socks5, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
socks5Port := socks5.Addr().(*net.TCPAddr).Port
go socks5Server(socks5)
// net/proxy don't support UDP, so we need to manually send the SOCKS5 UDP request
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", socks5Port))
if err != nil {
t.Fatal(err)
}
_, err = conn.Write([]byte{0x05, 0x01, 0x00}) // client hello with no auth
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf) // server hello
if err != nil {
t.Fatal(err)
}
if n != 2 || buf[0] != 0x05 || buf[1] != 0x00 {
t.Fatalf("got: %q want: 0x05 0x00", buf[:n])
}
targetAddr := socksAddr{
addrType: domainName,
addr: "localhost",
port: uint16(backendServerPort),
}
targetAddrPkt, err := targetAddr.marshal()
if err != nil {
t.Fatal(err)
}
_, err = conn.Write(append([]byte{0x05, 0x03, 0x00}, targetAddrPkt...)) // client reqeust
if err != nil {
t.Fatal(err)
}
n, err = conn.Read(buf) // server response
if err != nil {
t.Fatal(err)
}
if n < 3 || !bytes.Equal(buf[:3], []byte{0x05, 0x00, 0x00}) {
t.Fatalf("got: %q want: 0x05 0x00 0x00", buf[:n])
}
udpProxySocksAddr, err := parseSocksAddr(bytes.NewReader(buf[3:n]))
if err != nil {
t.Fatal(err)
}
udpProxyAddr, err := net.ResolveUDPAddr("udp", udpProxySocksAddr.hostPort())
if err != nil {
t.Fatal(err)
}
udpConn, err := net.DialUDP("udp", nil, udpProxyAddr)
if err != nil {
t.Fatal(err)
}
udpPayload, err := (&udpRequest{addr: targetAddr}).marshal()
if err != nil {
t.Fatal(err)
}
udpPayload = append(udpPayload, []byte("Test")...)
_, err = udpConn.Write(udpPayload) // send udp package
if err != nil {
t.Fatal(err)
}
n, _, err = udpConn.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
_, responseBody, err := parseUDPRequest(buf[:n]) // read udp response
if err != nil {
t.Fatal(err)
}
if string(responseBody) != "Test" {
t.Fatalf("got: %q want: Test", responseBody)
}
err = udpConn.Close()
if err != nil {
t.Fatal(err)
}
err = conn.Close()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -76,17 +76,30 @@ func Config(host string, ht *health.Tracker, base *tls.Config) *tls.Config {
// own cert verification, as do the same work that it'd do
// (with the baked-in fallback root) in the VerifyConnection hook.
conf.InsecureSkipVerify = true
conf.VerifyConnection = func(cs tls.ConnectionState) error {
conf.VerifyConnection = func(cs tls.ConnectionState) (retErr error) {
// Perform some health checks on this certificate before we do
// any verification.
var selfSignedIssuer string
if certs := cs.PeerCertificates; len(certs) > 0 && certIsSelfSigned(certs[0]) {
selfSignedIssuer = certs[0].Issuer.String()
}
if ht != nil {
if certIsSelfSigned(cs.PeerCertificates[0]) {
// Self-signed certs are never valid.
ht.SetTLSConnectionError(cs.ServerName, fmt.Errorf("certificate is self-signed"))
} else {
// Ensure we clear any error state for this ServerName.
ht.SetTLSConnectionError(cs.ServerName, nil)
}
defer func() {
if retErr != nil && selfSignedIssuer != "" {
// Self-signed certs are never valid.
//
// TODO(bradfitz): plumb down the selfSignedIssuer as a
// structured health warning argument.
ht.SetTLSConnectionError(cs.ServerName, fmt.Errorf("likely intercepted connection; certificate is self-signed by %v", selfSignedIssuer))
} else {
// Ensure we clear any error state for this ServerName.
ht.SetTLSConnectionError(cs.ServerName, nil)
if selfSignedIssuer != "" {
// Log the self-signed issuer, but don't treat it as an error.
log.Printf("tlsdial: warning: server cert for %q passed x509 validation but is self-signed by %q", host, selfSignedIssuer)
}
}
}()
}
// First try doing x509 verification with the system's

View File

@@ -166,6 +166,7 @@ func (d *Dialer) Close() error {
c.Close()
}
d.activeSysConns = nil
d.PeerAPITransport().CloseIdleConnections()
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"net/netip"
"os"
"reflect"
"runtime"
"slices"
"strings"
"sync"
@@ -17,6 +18,7 @@ import (
"time"
"github.com/gaissmai/bart"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"go4.org/mem"
@@ -160,6 +162,10 @@ type Wrapper struct {
PreFilterPacketInboundFromWireGuard FilterFunc
// PostFilterPacketInboundFromWireGuard is the inbound filter function that runs after the main filter.
PostFilterPacketInboundFromWireGuard FilterFunc
// EndPacketVectorInboundFromWireGuardFlush is a function that runs after all packets in a given vector
// have been handled by all filters. Filters may queue packets for the purposes of GRO, requiring an
// explicit flush.
EndPacketVectorInboundFromWireGuardFlush func()
// PreFilterPacketOutboundToWireGuardNetstackIntercept is a filter function that runs before the main filter
// for packets from the local system. This filter is populated by netstack to hook
// packets that should be handled by netstack. If set, this filter runs before
@@ -894,13 +900,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
return 0, res.err
}
if res.data == nil {
n, err := t.injectedRead(res.injected, buffs[0], offset)
sizes[0] = n
if err != nil && n == 0 {
return 0, err
}
return 1, err
return t.injectedRead(res.injected, buffs, sizes, offset)
}
metricPacketOut.Add(int64(len(res.data)))
@@ -955,27 +955,85 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
return buffsPos, res.err
}
const (
minTCPHeaderSize = 20
)
func stackGSOToTunGSO(pkt []byte, gso stack.GSO) (tun.GSOOptions, error) {
options := tun.GSOOptions{
CsumStart: gso.L3HdrLen,
CsumOffset: gso.CsumOffset,
GSOSize: gso.MSS,
NeedsCsum: gso.NeedsCsum,
}
switch gso.Type {
case stack.GSONone:
options.GSOType = tun.GSONone
return options, nil
case stack.GSOTCPv4:
options.GSOType = tun.GSOTCPv4
case stack.GSOTCPv6:
options.GSOType = tun.GSOTCPv6
default:
return tun.GSOOptions{}, fmt.Errorf("unsupported gVisor GSOType: %v", gso.Type)
}
// options.HdrLen is both layer 3 and 4 together, whereas gVisor only
// gives us layer 3 length. We have to gather TCP header length
// ourselves.
if len(pkt) < int(gso.L3HdrLen)+minTCPHeaderSize {
return tun.GSOOptions{}, errors.New("gVisor GSOTCP packet length too short")
}
tcphLen := uint16(pkt[int(gso.L3HdrLen)+12] >> 4 * 4)
options.HdrLen = gso.L3HdrLen + tcphLen
return options, nil
}
func invertGSOChecksum(pkt []byte, gso stack.GSO) {
if gso.NeedsCsum != true {
return
}
at := int(gso.L3HdrLen + gso.CsumOffset)
if at+1 > len(pkt)-1 {
return
}
pkt[at] = ^pkt[at]
pkt[at+1] = ^pkt[at+1]
}
// injectedRead handles injected reads, which bypass filters.
func (t *Wrapper) injectedRead(res tunInjectedRead, buf []byte, offset int) (int, error) {
metricPacketOut.Add(1)
func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []int, offset int) (n int, err error) {
var gso stack.GSO
var n int
if !res.packet.IsNil() {
n = copy(buf[offset:], res.packet.NetworkHeader().Slice())
n += copy(buf[offset+n:], res.packet.TransportHeader().Slice())
n += copy(buf[offset+n:], res.packet.Data().AsRange().ToSlice())
res.packet.DecRef()
pkt := outBuffs[0][offset:]
if res.packet != nil {
bufN := copy(pkt, res.packet.NetworkHeader().Slice())
bufN += copy(pkt[bufN:], res.packet.TransportHeader().Slice())
bufN += copy(pkt[bufN:], res.packet.Data().AsRange().ToSlice())
gso = res.packet.GSOOptions
pkt = pkt[:bufN]
defer res.packet.DecRef() // defer DecRef so we may continue to reference it
} else {
n = copy(buf[offset:], res.data)
sizes[0] = copy(pkt, res.data)
pkt = pkt[:sizes[0]]
n = 1
}
pc := t.peerConfig.Load()
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(buf[offset : offset+n])
p.Decode(pkt)
// We invert the transport layer checksum before and after snat() if gVisor
// handed us a segment with a partial checksum. A partial checksum is not a
// ones' complement of the sum, and incremental checksum updating that could
// occur as a result of snat() is not aware of this. Alternatively we could
// plumb partial transport layer checksum awareness down through snat(),
// but the surface area of such a change is much larger, and not yet
// justified by this singular case.
invertGSOChecksum(pkt, gso)
pc.snat(p)
invertGSOChecksum(pkt, gso)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
@@ -983,11 +1041,24 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, buf []byte, offset int) (int
}
}
if stats := t.stats.Load(); stats != nil {
stats.UpdateTxVirtual(buf[offset:][:n])
if res.packet != nil {
var gsoOptions tun.GSOOptions
gsoOptions, err = stackGSOToTunGSO(pkt, gso)
if err != nil {
return 0, err
}
n, err = tun.GSOSplit(pkt, gsoOptions, outBuffs, sizes, offset)
}
if stats := t.stats.Load(); stats != nil {
for i := 0; i < n; i++ {
stats.UpdateTxVirtual(outBuffs[i][offset : offset+sizes[i]])
}
}
t.noteActivity()
return n, nil
metricPacketOut.Add(int64(n))
return n, err
}
func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook capture.Callback, pc *peerConfigTable) filter.Response {
@@ -1112,6 +1183,9 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
}
}
}
if t.EndPacketVectorInboundFromWireGuardFlush != nil {
t.EndPacketVectorInboundFromWireGuardFlush()
}
if t.disableFilter {
i = len(buffs)
}
@@ -1288,6 +1362,14 @@ func (t *Wrapper) InjectOutboundPacketBuffer(pkt *stack.PacketBuffer) error {
}
func (t *Wrapper) BatchSize() int {
if runtime.GOOS == "linux" {
// Always setup Linux to handle vectors, even in the very rare case that
// the underlying t.tdev returns 1. gVisor GSO is always enabled for
// Linux, and we cannot make a determination on gVisor usage at
// wireguard-go.Device startup, which is when this value matters for
// packet memory init.
return conn.IdealBatchSize
}
return t.tdev.BatchSize()
}

View File

@@ -7,19 +7,26 @@
package prober
import (
"container/ring"
"context"
"errors"
"encoding/json"
"fmt"
"hash/fnv"
"log"
"maps"
"math/rand"
"net/http"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"tailscale.com/tsweb"
)
// recentHistSize is the number of recent probe results and latencies to keep
// in memory.
const recentHistSize = 10
// ProbeClass defines a probe of a specific type: a probing function that will
// be regularly ran, and metric labels that will be added automatically to all
// probes using this class.
@@ -106,6 +113,14 @@ func (p *Prober) Run(name string, interval time.Duration, labels Labels, pc Prob
l[k] = v
}
probe := newProbe(p, name, interval, l, pc)
p.probes[name] = probe
go probe.loop()
return probe
}
// newProbe creates a new Probe with the given parameters, but does not start it.
func newProbe(p *Prober, name string, interval time.Duration, l prometheus.Labels, pc ProbeClass) *Probe {
ctx, cancel := context.WithCancel(context.Background())
probe := &Probe{
prober: p,
@@ -117,6 +132,9 @@ func (p *Prober) Run(name string, interval time.Duration, labels Labels, pc Prob
probeClass: pc,
interval: interval,
initialDelay: initialDelay(name, interval),
successHist: ring.New(recentHistSize),
latencyHist: ring.New(recentHistSize),
metrics: prometheus.NewRegistry(),
metricLabels: l,
mInterval: prometheus.NewDesc("interval_secs", "Probe interval in seconds", nil, l),
@@ -131,15 +149,14 @@ func (p *Prober) Run(name string, interval time.Duration, labels Labels, pc Prob
Name: "seconds_total", Help: "Total amount of time spent executing the probe", ConstLabels: l,
}, []string{"status"}),
}
prometheus.WrapRegistererWithPrefix(p.namespace+"_", p.metrics).MustRegister(probe.metrics)
if p.metrics != nil {
prometheus.WrapRegistererWithPrefix(p.namespace+"_", p.metrics).MustRegister(probe.metrics)
}
probe.metrics.MustRegister(probe)
p.probes[name] = probe
go probe.loop()
return probe
}
// unregister removes a probe from the prober's internal state.
func (p *Prober) unregister(probe *Probe) {
p.mu.Lock()
defer p.mu.Unlock()
@@ -206,6 +223,7 @@ type Probe struct {
ctx context.Context
cancel context.CancelFunc // run to initiate shutdown
stopped chan struct{} // closed when shutdown is complete
runMu sync.Mutex // ensures only one probe runs at a time
name string
probeClass ProbeClass
@@ -232,6 +250,10 @@ type Probe struct {
latency time.Duration // last successful probe latency
succeeded bool // whether the last doProbe call succeeded
lastErr error
// History of recent probe results and latencies.
successHist *ring.Ring
latencyHist *ring.Ring
}
// Close shuts down the Probe and unregisters it from its Prober.
@@ -278,13 +300,17 @@ func (p *Probe) loop() {
}
}
// run invokes fun and records the results.
// run invokes the probe function and records the result. It returns the probe
// result and an error if the probe failed.
//
// fun is invoked with a timeout slightly less than interval, so that
// the probe either succeeds or fails before the next cycle is
// scheduled to start.
func (p *Probe) run() {
start := p.recordStart()
// The probe function is invoked with a timeout slightly less than interval, so
// that the probe either succeeds or fails before the next cycle is scheduled to
// start.
func (p *Probe) run() (pi ProbeInfo, err error) {
p.runMu.Lock()
defer p.runMu.Unlock()
p.recordStart()
defer func() {
// Prevent a panic within one probe function from killing the
// entire prober, so that a single buggy probe doesn't destroy
@@ -293,29 +319,30 @@ func (p *Probe) run() {
// alert for debugging.
if r := recover(); r != nil {
log.Printf("probe %s panicked: %v", p.name, r)
p.recordEnd(start, errors.New("panic"))
err = fmt.Errorf("panic: %v", r)
p.recordEnd(err)
}
}()
timeout := time.Duration(float64(p.interval) * 0.8)
ctx, cancel := context.WithTimeout(p.ctx, timeout)
defer cancel()
err := p.probeClass.Probe(ctx)
p.recordEnd(start, err)
err = p.probeClass.Probe(ctx)
p.recordEnd(err)
if err != nil {
log.Printf("probe %s: %v", p.name, err)
}
pi = p.probeInfoLocked()
return
}
func (p *Probe) recordStart() time.Time {
st := p.prober.now()
func (p *Probe) recordStart() {
p.mu.Lock()
defer p.mu.Unlock()
p.start = st
return st
p.start = p.prober.now()
p.mu.Unlock()
}
func (p *Probe) recordEnd(start time.Time, err error) {
func (p *Probe) recordEnd(err error) {
end := p.prober.now()
p.mu.Lock()
defer p.mu.Unlock()
@@ -327,22 +354,55 @@ func (p *Probe) recordEnd(start time.Time, err error) {
p.latency = latency
p.mAttempts.WithLabelValues("ok").Inc()
p.mSeconds.WithLabelValues("ok").Add(latency.Seconds())
p.latencyHist.Value = latency
p.latencyHist = p.latencyHist.Next()
} else {
p.latency = 0
p.mAttempts.WithLabelValues("fail").Inc()
p.mSeconds.WithLabelValues("fail").Add(latency.Seconds())
}
p.successHist.Value = p.succeeded
p.successHist = p.successHist.Next()
}
// ProbeInfo is the state of a Probe.
// ProbeInfo is a snapshot of the configuration and state of a Probe.
type ProbeInfo struct {
Start time.Time
End time.Time
Latency string
Result bool
Error string
Name string
Class string
Interval time.Duration
Labels map[string]string
Start time.Time
End time.Time
Latency time.Duration
Result bool
Error string
RecentResults []bool
RecentLatencies []time.Duration
}
// RecentSuccessRatio returns the success ratio of the probe in the recent history.
func (pb ProbeInfo) RecentSuccessRatio() float64 {
if len(pb.RecentResults) == 0 {
return 0
}
var sum int
for _, r := range pb.RecentResults {
if r {
sum++
}
}
return float64(sum) / float64(len(pb.RecentResults))
}
// RecentMedianLatency returns the median latency of the probe in the recent history.
func (pb ProbeInfo) RecentMedianLatency() time.Duration {
if len(pb.RecentLatencies) == 0 {
return 0
}
return pb.RecentLatencies[len(pb.RecentLatencies)/2]
}
// ProbeInfo returns the state of all probes.
func (p *Prober) ProbeInfo() map[string]ProbeInfo {
out := map[string]ProbeInfo{}
@@ -352,26 +412,100 @@ func (p *Prober) ProbeInfo() map[string]ProbeInfo {
probes = append(probes, probe)
}
p.mu.Unlock()
for _, probe := range probes {
probe.mu.Lock()
inf := ProbeInfo{
Start: probe.start,
End: probe.end,
Result: probe.succeeded,
}
if probe.lastErr != nil {
inf.Error = probe.lastErr.Error()
}
if probe.latency > 0 {
inf.Latency = probe.latency.String()
}
out[probe.name] = inf
out[probe.name] = probe.probeInfoLocked()
probe.mu.Unlock()
}
return out
}
// probeInfoLocked returns the state of the probe.
func (probe *Probe) probeInfoLocked() ProbeInfo {
inf := ProbeInfo{
Name: probe.name,
Class: probe.probeClass.Class,
Interval: probe.interval,
Labels: probe.metricLabels,
Start: probe.start,
End: probe.end,
Result: probe.succeeded,
}
if probe.lastErr != nil {
inf.Error = probe.lastErr.Error()
}
if probe.latency > 0 {
inf.Latency = probe.latency
}
probe.latencyHist.Do(func(v any) {
if l, ok := v.(time.Duration); ok {
inf.RecentLatencies = append(inf.RecentLatencies, l)
}
})
probe.successHist.Do(func(v any) {
if r, ok := v.(bool); ok {
inf.RecentResults = append(inf.RecentResults, r)
}
})
return inf
}
// RunHandlerResponse is the JSON response format for the RunHandler.
type RunHandlerResponse struct {
ProbeInfo ProbeInfo
PreviousSuccessRatio float64
PreviousMedianLatency time.Duration
}
// RunHandler runs a probe by name and returns the result as an HTTP response.
func (p *Prober) RunHandler(w http.ResponseWriter, r *http.Request) error {
// Look up prober by name.
name := r.FormValue("name")
if name == "" {
return tsweb.Error(http.StatusBadRequest, "missing name parameter", nil)
}
p.mu.Lock()
probe, ok := p.probes[name]
p.mu.Unlock()
if !ok {
return tsweb.Error(http.StatusNotFound, fmt.Sprintf("unknown probe %q", name), nil)
}
probe.mu.Lock()
prevInfo := probe.probeInfoLocked()
probe.mu.Unlock()
info, err := probe.run()
respStatus := http.StatusOK
if err != nil {
respStatus = http.StatusFailedDependency
}
// Return serialized JSON response if the client requested JSON
if r.Header.Get("Accept") == "application/json" {
resp := &RunHandlerResponse{
ProbeInfo: info,
PreviousSuccessRatio: prevInfo.RecentSuccessRatio(),
PreviousMedianLatency: prevInfo.RecentMedianLatency(),
}
w.WriteHeader(respStatus)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
return tsweb.Error(http.StatusInternalServerError, "error encoding JSON response", err)
}
return nil
}
stats := fmt.Sprintf("Previous runs: success rate %d%%, median latency %v",
int(prevInfo.RecentSuccessRatio()*100), prevInfo.RecentMedianLatency())
if err != nil {
return tsweb.Error(respStatus, fmt.Sprintf("Probe failed: %s\n%s", err.Error(), stats), err)
}
w.WriteHeader(respStatus)
w.Write([]byte(fmt.Sprintf("Probe succeeded in %v\n%s", info.Latency, stats)))
return nil
}
// Describe implements prometheus.Collector.
func (p *Probe) Describe(ch chan<- *prometheus.Desc) {
ch <- p.mInterval

View File

@@ -5,16 +5,22 @@ package prober
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/prometheus/client_golang/prometheus/testutil"
"tailscale.com/tstest"
"tailscale.com/tsweb"
)
const (
@@ -292,6 +298,254 @@ func TestOnceMode(t *testing.T) {
}
}
func TestProberProbeInfo(t *testing.T) {
clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker).WithOnce(true)
p.Run("probe1", probeInterval, nil, FuncProbe(func(context.Context) error {
clk.Advance(500 * time.Millisecond)
return nil
}))
p.Run("probe2", probeInterval, nil, FuncProbe(func(context.Context) error { return fmt.Errorf("error2") }))
p.Wait()
info := p.ProbeInfo()
wantInfo := map[string]ProbeInfo{
"probe1": {
Name: "probe1",
Interval: probeInterval,
Labels: map[string]string{"class": "", "name": "probe1"},
Latency: 500 * time.Millisecond,
Result: true,
RecentResults: []bool{true},
RecentLatencies: []time.Duration{500 * time.Millisecond},
},
"probe2": {
Name: "probe2",
Interval: probeInterval,
Labels: map[string]string{"class": "", "name": "probe2"},
Error: "error2",
RecentResults: []bool{false},
RecentLatencies: nil, // no latency for failed probes
},
}
if diff := cmp.Diff(wantInfo, info, cmpopts.IgnoreFields(ProbeInfo{}, "Start", "End")); diff != "" {
t.Fatalf("unexpected ProbeInfo (-want +got):\n%s", diff)
}
}
func TestProbeInfoRecent(t *testing.T) {
type probeResult struct {
latency time.Duration
err error
}
tests := []struct {
name string
results []probeResult
wantProbeInfo ProbeInfo
wantRecentSuccessRatio float64
wantRecentMedianLatency time.Duration
}{
{
name: "no_runs",
wantProbeInfo: ProbeInfo{},
wantRecentSuccessRatio: 0,
wantRecentMedianLatency: 0,
},
{
name: "single_success",
results: []probeResult{{latency: 100 * time.Millisecond, err: nil}},
wantProbeInfo: ProbeInfo{
Latency: 100 * time.Millisecond,
Result: true,
RecentResults: []bool{true},
RecentLatencies: []time.Duration{100 * time.Millisecond},
},
wantRecentSuccessRatio: 1,
wantRecentMedianLatency: 100 * time.Millisecond,
},
{
name: "single_failure",
results: []probeResult{{latency: 100 * time.Millisecond, err: errors.New("error123")}},
wantProbeInfo: ProbeInfo{
Result: false,
RecentResults: []bool{false},
RecentLatencies: nil,
Error: "error123",
},
wantRecentSuccessRatio: 0,
wantRecentMedianLatency: 0,
},
{
name: "recent_mix",
results: []probeResult{
{latency: 10 * time.Millisecond, err: errors.New("error1")},
{latency: 20 * time.Millisecond, err: nil},
{latency: 30 * time.Millisecond, err: nil},
{latency: 40 * time.Millisecond, err: errors.New("error4")},
{latency: 50 * time.Millisecond, err: nil},
{latency: 60 * time.Millisecond, err: nil},
{latency: 70 * time.Millisecond, err: errors.New("error7")},
{latency: 80 * time.Millisecond, err: nil},
},
wantProbeInfo: ProbeInfo{
Result: true,
Latency: 80 * time.Millisecond,
RecentResults: []bool{false, true, true, false, true, true, false, true},
RecentLatencies: []time.Duration{
20 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
60 * time.Millisecond,
80 * time.Millisecond,
},
},
wantRecentSuccessRatio: 0.625,
wantRecentMedianLatency: 50 * time.Millisecond,
},
{
name: "only_last_10",
results: []probeResult{
{latency: 10 * time.Millisecond, err: errors.New("old_error")},
{latency: 20 * time.Millisecond, err: nil},
{latency: 30 * time.Millisecond, err: nil},
{latency: 40 * time.Millisecond, err: nil},
{latency: 50 * time.Millisecond, err: nil},
{latency: 60 * time.Millisecond, err: nil},
{latency: 70 * time.Millisecond, err: nil},
{latency: 80 * time.Millisecond, err: nil},
{latency: 90 * time.Millisecond, err: nil},
{latency: 100 * time.Millisecond, err: nil},
{latency: 110 * time.Millisecond, err: nil},
},
wantProbeInfo: ProbeInfo{
Result: true,
Latency: 110 * time.Millisecond,
RecentResults: []bool{true, true, true, true, true, true, true, true, true, true},
RecentLatencies: []time.Duration{
20 * time.Millisecond,
30 * time.Millisecond,
40 * time.Millisecond,
50 * time.Millisecond,
60 * time.Millisecond,
70 * time.Millisecond,
80 * time.Millisecond,
90 * time.Millisecond,
100 * time.Millisecond,
110 * time.Millisecond,
},
},
wantRecentSuccessRatio: 1,
wantRecentMedianLatency: 70 * time.Millisecond,
},
}
clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker).WithOnce(true)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
probe := newProbe(p, "", probeInterval, nil, FuncProbe(func(context.Context) error { return nil }))
for _, r := range tt.results {
probe.recordStart()
clk.Advance(r.latency)
probe.recordEnd(r.err)
}
info := probe.probeInfoLocked()
if diff := cmp.Diff(tt.wantProbeInfo, info, cmpopts.IgnoreFields(ProbeInfo{}, "Start", "End", "Interval")); diff != "" {
t.Fatalf("unexpected ProbeInfo (-want +got):\n%s", diff)
}
if got := info.RecentSuccessRatio(); got != tt.wantRecentSuccessRatio {
t.Errorf("recentSuccessRatio() = %v, want %v", got, tt.wantRecentSuccessRatio)
}
if got := info.RecentMedianLatency(); got != tt.wantRecentMedianLatency {
t.Errorf("recentMedianLatency() = %v, want %v", got, tt.wantRecentMedianLatency)
}
})
}
}
func TestProberRunHandler(t *testing.T) {
clk := newFakeTime()
tests := []struct {
name string
probeFunc func(context.Context) error
wantResponseCode int
wantJSONResponse RunHandlerResponse
wantPlaintextResponse string
}{
{
name: "success",
probeFunc: func(context.Context) error { return nil },
wantResponseCode: 200,
wantJSONResponse: RunHandlerResponse{
ProbeInfo: ProbeInfo{
Name: "success",
Interval: probeInterval,
Result: true,
RecentResults: []bool{true, true},
},
PreviousSuccessRatio: 1,
},
wantPlaintextResponse: "Probe succeeded",
},
{
name: "failure",
probeFunc: func(context.Context) error { return fmt.Errorf("error123") },
wantResponseCode: 424,
wantJSONResponse: RunHandlerResponse{
ProbeInfo: ProbeInfo{
Name: "failure",
Interval: probeInterval,
Result: false,
Error: "error123",
RecentResults: []bool{false, false},
},
},
wantPlaintextResponse: "Probe failed",
},
}
for _, tt := range tests {
for _, reqJSON := range []bool{true, false} {
t.Run(fmt.Sprintf("%s_json-%v", tt.name, reqJSON), func(t *testing.T) {
p := newForTest(clk.Now, clk.NewTicker).WithOnce(true)
probe := p.Run(tt.name, probeInterval, nil, FuncProbe(tt.probeFunc))
defer probe.Close()
<-probe.stopped // wait for the first run.
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/prober/run/?name="+tt.name, nil)
if reqJSON {
req.Header.Set("Accept", "application/json")
}
tsweb.StdHandler(tsweb.ReturnHandlerFunc(p.RunHandler), tsweb.HandlerOptions{}).ServeHTTP(w, req)
if w.Result().StatusCode != tt.wantResponseCode {
t.Errorf("unexpected response code: got %d, want %d", w.Code, tt.wantResponseCode)
}
if reqJSON {
var gotJSON RunHandlerResponse
if err := json.Unmarshal(w.Body.Bytes(), &gotJSON); err != nil {
t.Fatalf("failed to unmarshal JSON response: %v; body: %s", err, w.Body.String())
}
if diff := cmp.Diff(tt.wantJSONResponse, gotJSON, cmpopts.IgnoreFields(ProbeInfo{}, "Start", "End", "Labels", "RecentLatencies")); diff != "" {
t.Errorf("unexpected JSON response (-want +got):\n%s", diff)
}
} else {
body, _ := io.ReadAll(w.Result().Body)
if !strings.Contains(string(body), tt.wantPlaintextResponse) {
t.Errorf("unexpected response body: got %q, want to contain %q", body, tt.wantPlaintextResponse)
}
}
})
}
}
}
type fakeTicker struct {
ch chan time.Time
interval time.Duration

124
prober/status.go Normal file
View File

@@ -0,0 +1,124 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prober
import (
"embed"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"tailscale.com/tsweb"
"tailscale.com/util/mak"
)
//go:embed status.html
var statusFiles embed.FS
var statusTpl = template.Must(template.ParseFS(statusFiles, "status.html"))
type statusHandlerOpt func(*statusHandlerParams)
type statusHandlerParams struct {
title string
pageLinks map[string]string
probeLinks map[string]string
}
// WithTitle sets the title of the status page.
func WithTitle(title string) statusHandlerOpt {
return func(opts *statusHandlerParams) {
opts.title = title
}
}
// WithPageLink adds a top-level link to the status page.
func WithPageLink(text, url string) statusHandlerOpt {
return func(opts *statusHandlerParams) {
mak.Set(&opts.pageLinks, text, url)
}
}
// WithProbeLink adds a link to each probe on the status page.
// The textTpl and urlTpl are Go templates that will be rendered
// with the respective ProbeInfo struct as the data.
func WithProbeLink(textTpl, urlTpl string) statusHandlerOpt {
return func(opts *statusHandlerParams) {
mak.Set(&opts.probeLinks, textTpl, urlTpl)
}
}
// StatusHandler is a handler for the probe overview HTTP endpoint.
// It shows a list of probes and their current status.
func (p *Prober) StatusHandler(opts ...statusHandlerOpt) tsweb.ReturnHandlerFunc {
params := &statusHandlerParams{
title: "Prober Status",
}
for _, opt := range opts {
opt(params)
}
return func(w http.ResponseWriter, r *http.Request) error {
type probeStatus struct {
ProbeInfo
TimeSinceLast time.Duration
Links map[string]template.URL
}
vars := struct {
Title string
Links map[string]template.URL
TotalProbes int64
UnhealthyProbes int64
Probes map[string]probeStatus
}{
Title: params.title,
}
for text, url := range params.pageLinks {
mak.Set(&vars.Links, text, template.URL(url))
}
for name, info := range p.ProbeInfo() {
vars.TotalProbes++
if !info.Result {
vars.UnhealthyProbes++
}
s := probeStatus{ProbeInfo: info}
if !info.End.IsZero() {
s.TimeSinceLast = time.Since(info.End)
}
for textTpl, urlTpl := range params.probeLinks {
text, err := renderTemplate(textTpl, info)
if err != nil {
return tsweb.Error(500, err.Error(), err)
}
url, err := renderTemplate(urlTpl, info)
if err != nil {
return tsweb.Error(500, err.Error(), err)
}
mak.Set(&s.Links, text, template.URL(url))
}
mak.Set(&vars.Probes, name, s)
}
if err := statusTpl.ExecuteTemplate(w, "status", vars); err != nil {
return tsweb.HTTPError{Code: 500, Err: err, Msg: "error rendering status page"}
}
return nil
}
}
// renderTemplate renders the given Go template with the provided data
// and returns the result as a string.
func renderTemplate(tpl string, data any) (string, error) {
t, err := template.New("").Parse(tpl)
if err != nil {
return "", fmt.Errorf("error parsing template %q: %w", tpl, err)
}
var buf strings.Builder
if err := t.ExecuteTemplate(&buf, "", data); err != nil {
return "", fmt.Errorf("error rendering template %q with data %v: %w", tpl, data, err)
}
return buf.String(), nil
}

132
prober/status.html Normal file
View File

@@ -0,0 +1,132 @@
{{define "status"}}
<html>
<head><title>{{.Title}}</title></head>
<style>
body {
/* max-width: 60rem; */
margin-left: auto;
margin-right: auto;
padding: 3rem 1rem 8rem;
line-height: 1.4;
font-size: 1rem;
font-weight: 400;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
text-rendering: optimizeLegibility;
}
.small {
font-size: 0.7rem;
}
h1 {
font-weight: 500;
letter-spacing: -.025em;
}
a { color: rgb(74 125 221); }
a:hover { color: rgb(73 100 149); }
ul {
list-style: none;
margin: 0;
padding: 0;
}
ul>li::before {
position: absolute;
top: .625rem;
left: .125rem;
height: .375rem;
width: .375rem;
border-radius: 9999px;
background-color: currentColor;
opacity: .4;
content: "";
}
ul>li {
position: relative;
padding-left: 1.25rem;
}
th, td {
padding: 5px;
text-align: left;
background: #eeeeee;
}
.error {
color: red;
}
</style>
<body>
<h1>{{.Title}}</h1>
<ul>
<li>Prober Status:
{{if .UnhealthyProbes }}
<span class="error">{{.UnhealthyProbes}}</span>
out of {{.TotalProbes}} probes failed or never ran.
{{else}}
All {{.TotalProbes}} probes are healthy
{{end}}
</li>
{{ range $text, $url := .Links }}
<li><a href="{{$url}}">{{$text}}</a></li>
{{end}}
</ul>
<h1>Probes:</h1>
<table class="sortable">
<thead><tr>
<th>Name</th>
<th>Class & Labels</th>
<th>Interval</th>
<th>Result</th>
<th>Success</th>
<th>Latency</th>
<th>Error</th>
</tr></thead>
<tbody>
{{range $name, $probeInfo := .Probes}}
<tr>
<td>
{{$name}}
{{range $text, $url := $probeInfo.Links}}
<br/>
<button onclick="location.href='{{$url}}';" type="button">
{{$text}}
</button>
{{end}}
</td>
<td>{{$probeInfo.Class}}<br/>
<div class="small">
{{range $label, $value := $probeInfo.Labels}}
{{$label}}={{$value}}<br/>
{{end}}
</div>
</td>
<td>{{$probeInfo.Interval}}</td>
<td data-sort="{{$probeInfo.TimeSinceLast.Milliseconds}}">
{{if $probeInfo.TimeSinceLast}}
{{$probeInfo.TimeSinceLast.String}}<br/>
<span class="small">{{$probeInfo.End}}</span>
{{else}}
Never
{{end}}
</td>
<td>
{{if $probeInfo.Result}}
{{$probeInfo.Result}}
{{else}}
<span class="error">{{$probeInfo.Result}}</span>
{{end}}<br/>
<div class="small">Recent: {{$probeInfo.RecentResults}}</div>
<div class="small">Mean: {{$probeInfo.RecentSuccessRatio}}</div>
</td>
<td data-sort="{{$probeInfo.Latency.Milliseconds}}">
{{$probeInfo.Latency.String}}
<div class="small">Recent: {{$probeInfo.RecentLatencies}}</div>
<div class="small">Median: {{$probeInfo.RecentMedianLatency}}</div>
</td>
<td class="small">{{$probeInfo.Error}}</td>
</tr>
{{end}}
</tbody>
</table>
<link href="https://cdn.jsdelivr.net/gh/tofsjonas/sortable@latest/sortable-base.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/gh/tofsjonas/sortable@latest/sortable.min.js"></script>
</body>
</html>
{{end}}

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-2x9Ns5o6oenCcsHkOFjoCz/R5YjPwJEImK0a1valYBE=
# nix-direnv cache busting line: sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=

View File

@@ -146,7 +146,8 @@ type CapabilityVersion int
// - 101: 2024-07-01: Client supports SSH agent forwarding when handling connections with /bin/su
// - 102: 2024-07-12: NodeAttrDisableMagicSockCryptoRouting support
// - 103: 2024-07-24: Client supports NodeAttrDisableCaptivePortalDetection
const CurrentCapabilityVersion CapabilityVersion = 103
// - 104: 2024-08-03: SelfNodeV6MasqAddrForThisPeer now works
const CurrentCapabilityVersion CapabilityVersion = 104
type StableID string

View File

@@ -842,6 +842,7 @@ func TestClientSideJailing(t *testing.T) {
// TestNATPing creates two nodes, n1 and n2, sets up masquerades for both and
// tries to do bi-directional pings between them.
func TestNATPing(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/12169")
tstest.Shard(t)
tstest.Parallel(t)
for _, v6 := range []bool{false, true} {

View File

@@ -22,6 +22,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/metrics"
"tailscale.com/tstest"
"tailscale.com/util/httpm"
@@ -864,6 +865,7 @@ func TestStdHandler_CanceledAfterHeader(t *testing.T) {
}
func TestStdHandler_ConnectionClosedDuringBody(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/13017")
now := time.Now()
// Start a HTTP server that returns 1MB of data.

View File

@@ -592,9 +592,23 @@ func New(logf logger.Logf, prefHint string) (NetfilterRunner, error) {
mode := detectFirewallMode(logf, prefHint)
switch mode {
case FirewallModeIPTables:
return newIPTablesRunner(logf)
// Note that we don't simply return an newIPTablesRunner here because it
// would return a `nil` iptablesRunner which is different from returning
// a nil NetfilterRunner.
ipr, err := newIPTablesRunner(logf)
if err != nil {
return nil, err
}
return ipr, nil
case FirewallModeNfTables:
return newNfTablesRunner(logf)
// Note that we don't simply return an newNfTablesRunner here because it
// would return a `nil` nftablesRunner which is different from returning
// a nil NetfilterRunner.
nfr, err := newNfTablesRunner(logf)
if err != nil {
return nil, err
}
return nfr, nil
default:
return nil, fmt.Errorf("unknown firewall mode %v", mode)
}

View File

@@ -54,7 +54,7 @@ func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, st
// Skip getent entirely on Non-Unix platforms that won't ever have it.
// (Using HasPrefix for "wasip1", anticipating that WASI support will
// move beyond "preview 1" some day.)
if runtime.GOOS == "windows" || runtime.GOOS == "js" || strings.HasPrefix(runtime.GOOS, "wasi") {
if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" {
u, err := std(usernameOrUID)
return u, "", err
}

View File

@@ -189,6 +189,7 @@ func (l *PolicyLock) lockSlow() (err error) {
select {
case resultCh <- policyLockResult{handle, err}:
// lockSlow has received the result.
break send_result
default:
select {
case <-closing:

View File

@@ -5,9 +5,9 @@ end
tsdebug_ll = Proto("tsdebug", "Tailscale debug")
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
SNAT_IP_4 = ProtoField.ipv4("tsdebug.SNAT_IP_4", "Pre-NAT Source IPv4 address")
SNAT_IP_6 = ProtoField.ipv4("tsdebug.SNAT_IP_6", "Pre-NAT Source IPv6 address")
SNAT_IP_6 = ProtoField.ipv6("tsdebug.SNAT_IP_6", "Pre-NAT Source IPv6 address")
DNAT_IP_4 = ProtoField.ipv4("tsdebug.DNAT_IP_4", "Pre-NAT Dest IPv4 address")
DNAT_IP_6 = ProtoField.ipv4("tsdebug.DNAT_IP_6", "Pre-NAT Dest IPv6 address")
DNAT_IP_6 = ProtoField.ipv6("tsdebug.DNAT_IP_6", "Pre-NAT Dest IPv6 address")
tsdebug_ll.fields = {PATH, SNAT_IP_4, SNAT_IP_6, DNAT_IP_4, DNAT_IP_6}
function tsdebug_ll.dissector(buffer, pinfo, tree)
@@ -63,7 +63,7 @@ local ts_dissectors = DissectorTable.new("ts.proto", "Tailscale-specific dissect
tsdisco_meta = Proto("tsdisco", "Tailscale DISCO metadata")
DISCO_IS_DERP = ProtoField.bool("tsdisco.IS_DERP","From DERP")
DISCO_SRC_IP_4 = ProtoField.ipv4("tsdisco.SRC_IP_4", "Source IPv4 address")
DISCO_SRC_IP_6 = ProtoField.ipv4("tsdisco.SRC_IP_6", "Source IPv6 address")
DISCO_SRC_IP_6 = ProtoField.ipv6("tsdisco.SRC_IP_6", "Source IPv6 address")
DISCO_SRC_PORT = ProtoField.uint16("tsdisco.SRC_PORT","Source port", base.DEC)
DISCO_DERP_PUB = ProtoField.bytes("tsdisco.DERP_PUB", "DERP public key", base.SPACE)
tsdisco_meta.fields = {DISCO_IS_DERP, DISCO_SRC_PORT, DISCO_DERP_PUB, DISCO_SRC_IP_4, DISCO_SRC_IP_6}

View File

@@ -4,200 +4,22 @@
package magicsock
import (
"errors"
"net"
"net/netip"
"sync"
"sync/atomic"
"syscall"
"time"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/net/neterror"
"tailscale.com/types/nettype"
)
// xnetBatchReaderWriter defines the batching i/o methods of
// golang.org/x/net/ipv4.PacketConn (and ipv6.PacketConn).
// TODO(jwhited): This should eventually be replaced with the standard library
// implementation of https://github.com/golang/go/issues/45886
type xnetBatchReaderWriter interface {
xnetBatchReader
xnetBatchWriter
}
type xnetBatchReader interface {
ReadBatch([]ipv6.Message, int) (int, error)
}
type xnetBatchWriter interface {
WriteBatch([]ipv6.Message, int) (int, error)
}
// batchingUDPConn is a UDP socket that provides batched i/o.
type batchingUDPConn struct {
pc nettype.PacketConn
xpc xnetBatchReaderWriter
rxOffload bool // supports UDP GRO or similar
txOffload atomic.Bool // supports UDP GSO or similar
setGSOSizeInControl func(control *[]byte, gsoSize uint16) // typically setGSOSizeInControl(); swappable for testing
getGSOSizeFromControl func(control []byte) (int, error) // typically getGSOSizeFromControl(); swappable for testing
sendBatchPool sync.Pool
}
func (c *batchingUDPConn) ReadFromUDPAddrPort(p []byte) (n int, addr netip.AddrPort, err error) {
if c.rxOffload {
// UDP_GRO is opt-in on Linux via setsockopt(). Once enabled you may
// receive a "monster datagram" from any read call. The ReadFrom() API
// does not support passing the GSO size and is unsafe to use in such a
// case. Other platforms may vary in behavior, but we go with the most
// conservative approach to prevent this from becoming a footgun in the
// future.
return 0, netip.AddrPort{}, errors.New("rx UDP offload is enabled on this socket, single packet reads are unavailable")
}
return c.pc.ReadFromUDPAddrPort(p)
}
func (c *batchingUDPConn) SetDeadline(t time.Time) error {
return c.pc.SetDeadline(t)
}
func (c *batchingUDPConn) SetReadDeadline(t time.Time) error {
return c.pc.SetReadDeadline(t)
}
func (c *batchingUDPConn) SetWriteDeadline(t time.Time) error {
return c.pc.SetWriteDeadline(t)
}
const (
// This was initially established for Linux, but may split out to
// GOOS-specific values later. It originates as UDP_MAX_SEGMENTS in the
// kernel's TX path, and UDP_GRO_CNT_MAX for RX.
udpSegmentMaxDatagrams = 64
var (
// This acts as a compile-time check for our usage of ipv6.Message in
// batchingConn for both IPv6 and IPv4 operations.
_ ipv6.Message = ipv4.Message{}
)
const (
// Exceeding these values results in EMSGSIZE.
maxIPv4PayloadLen = 1<<16 - 1 - 20 - 8
maxIPv6PayloadLen = 1<<16 - 1 - 8
)
// coalesceMessages iterates msgs, coalescing them where possible while
// maintaining datagram order. All msgs have their Addr field set to addr.
func (c *batchingUDPConn) coalesceMessages(addr *net.UDPAddr, buffs [][]byte, msgs []ipv6.Message) int {
var (
base = -1 // index of msg we are currently coalescing into
gsoSize int // segmentation size of msgs[base]
dgramCnt int // number of dgrams coalesced into msgs[base]
endBatch bool // tracking flag to start a new batch on next iteration of buffs
)
maxPayloadLen := maxIPv4PayloadLen
if addr.IP.To4() == nil {
maxPayloadLen = maxIPv6PayloadLen
}
for i, buff := range buffs {
if i > 0 {
msgLen := len(buff)
baseLenBefore := len(msgs[base].Buffers[0])
freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore
if msgLen+baseLenBefore <= maxPayloadLen &&
msgLen <= gsoSize &&
msgLen <= freeBaseCap &&
dgramCnt < udpSegmentMaxDatagrams &&
!endBatch {
msgs[base].Buffers[0] = append(msgs[base].Buffers[0], make([]byte, msgLen)...)
copy(msgs[base].Buffers[0][baseLenBefore:], buff)
if i == len(buffs)-1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
dgramCnt++
if msgLen < gsoSize {
// A smaller than gsoSize packet on the tail is legal, but
// it must end the batch.
endBatch = true
}
continue
}
}
if dgramCnt > 1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
// Reset prior to incrementing base since we are preparing to start a
// new potential batch.
endBatch = false
base++
gsoSize = len(buff)
msgs[base].OOB = msgs[base].OOB[:0]
msgs[base].Buffers[0] = buff
msgs[base].Addr = addr
dgramCnt = 1
}
return base + 1
}
type sendBatch struct {
msgs []ipv6.Message
ua *net.UDPAddr
}
func (c *batchingUDPConn) getSendBatch() *sendBatch {
batch := c.sendBatchPool.Get().(*sendBatch)
return batch
}
func (c *batchingUDPConn) putSendBatch(batch *sendBatch) {
for i := range batch.msgs {
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers, OOB: batch.msgs[i].OOB}
}
c.sendBatchPool.Put(batch)
}
func (c *batchingUDPConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error {
batch := c.getSendBatch()
defer c.putSendBatch(batch)
if addr.Addr().Is6() {
as16 := addr.Addr().As16()
copy(batch.ua.IP, as16[:])
batch.ua.IP = batch.ua.IP[:16]
} else {
as4 := addr.Addr().As4()
copy(batch.ua.IP, as4[:])
batch.ua.IP = batch.ua.IP[:4]
}
batch.ua.Port = int(addr.Port())
var (
n int
retried bool
)
retry:
if c.txOffload.Load() {
n = c.coalesceMessages(batch.ua, buffs, batch.msgs)
} else {
for i := range buffs {
batch.msgs[i].Buffers[0] = buffs[i]
batch.msgs[i].Addr = batch.ua
batch.msgs[i].OOB = batch.msgs[i].OOB[:0]
}
n = len(buffs)
}
err := c.writeBatch(batch.msgs[:n])
if err != nil && c.txOffload.Load() && neterror.ShouldDisableUDPGSO(err) {
c.txOffload.Store(false)
retried = true
goto retry
}
if retried {
return neterror.ErrUDPGSODisabled{OnLaddr: c.pc.LocalAddr().String(), RetryErr: err}
}
return err
}
func (c *batchingUDPConn) SyscallConn() (syscall.RawConn, error) {
sc, ok := c.pc.(syscall.Conn)
if !ok {
return nil, errUnsupportedConnType
}
return sc.SyscallConn()
// batchingConn is a nettype.PacketConn that provides batched i/o.
type batchingConn interface {
nettype.PacketConn
ReadBatch(msgs []ipv6.Message, flags int) (n int, err error)
WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
package magicsock
import (
"tailscale.com/types/nettype"
)
func tryUpgradeToBatchingConn(pconn nettype.PacketConn, _ string, _ int) nettype.PacketConn {
return pconn
}

View File

@@ -0,0 +1,419 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.org/x/sys/unix"
"tailscale.com/hostinfo"
"tailscale.com/net/neterror"
"tailscale.com/types/nettype"
)
// xnetBatchReaderWriter defines the batching i/o methods of
// golang.org/x/net/ipv4.PacketConn (and ipv6.PacketConn).
// TODO(jwhited): This should eventually be replaced with the standard library
// implementation of https://github.com/golang/go/issues/45886
type xnetBatchReaderWriter interface {
xnetBatchReader
xnetBatchWriter
}
type xnetBatchReader interface {
ReadBatch([]ipv6.Message, int) (int, error)
}
type xnetBatchWriter interface {
WriteBatch([]ipv6.Message, int) (int, error)
}
// linuxBatchingConn is a UDP socket that provides batched i/o. It implements
// batchingConn.
type linuxBatchingConn struct {
pc nettype.PacketConn
xpc xnetBatchReaderWriter
rxOffload bool // supports UDP GRO or similar
txOffload atomic.Bool // supports UDP GSO or similar
setGSOSizeInControl func(control *[]byte, gsoSize uint16) // typically setGSOSizeInControl(); swappable for testing
getGSOSizeFromControl func(control []byte) (int, error) // typically getGSOSizeFromControl(); swappable for testing
sendBatchPool sync.Pool
}
func (c *linuxBatchingConn) ReadFromUDPAddrPort(p []byte) (n int, addr netip.AddrPort, err error) {
if c.rxOffload {
// UDP_GRO is opt-in on Linux via setsockopt(). Once enabled you may
// receive a "monster datagram" from any read call. The ReadFrom() API
// does not support passing the GSO size and is unsafe to use in such a
// case. Other platforms may vary in behavior, but we go with the most
// conservative approach to prevent this from becoming a footgun in the
// future.
return 0, netip.AddrPort{}, errors.New("rx UDP offload is enabled on this socket, single packet reads are unavailable")
}
return c.pc.ReadFromUDPAddrPort(p)
}
func (c *linuxBatchingConn) SetDeadline(t time.Time) error {
return c.pc.SetDeadline(t)
}
func (c *linuxBatchingConn) SetReadDeadline(t time.Time) error {
return c.pc.SetReadDeadline(t)
}
func (c *linuxBatchingConn) SetWriteDeadline(t time.Time) error {
return c.pc.SetWriteDeadline(t)
}
const (
// This was initially established for Linux, but may split out to
// GOOS-specific values later. It originates as UDP_MAX_SEGMENTS in the
// kernel's TX path, and UDP_GRO_CNT_MAX for RX.
udpSegmentMaxDatagrams = 64
)
const (
// Exceeding these values results in EMSGSIZE.
maxIPv4PayloadLen = 1<<16 - 1 - 20 - 8
maxIPv6PayloadLen = 1<<16 - 1 - 8
)
// coalesceMessages iterates msgs, coalescing them where possible while
// maintaining datagram order. All msgs have their Addr field set to addr.
func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, buffs [][]byte, msgs []ipv6.Message) int {
var (
base = -1 // index of msg we are currently coalescing into
gsoSize int // segmentation size of msgs[base]
dgramCnt int // number of dgrams coalesced into msgs[base]
endBatch bool // tracking flag to start a new batch on next iteration of buffs
)
maxPayloadLen := maxIPv4PayloadLen
if addr.IP.To4() == nil {
maxPayloadLen = maxIPv6PayloadLen
}
for i, buff := range buffs {
if i > 0 {
msgLen := len(buff)
baseLenBefore := len(msgs[base].Buffers[0])
freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore
if msgLen+baseLenBefore <= maxPayloadLen &&
msgLen <= gsoSize &&
msgLen <= freeBaseCap &&
dgramCnt < udpSegmentMaxDatagrams &&
!endBatch {
msgs[base].Buffers[0] = append(msgs[base].Buffers[0], make([]byte, msgLen)...)
copy(msgs[base].Buffers[0][baseLenBefore:], buff)
if i == len(buffs)-1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
dgramCnt++
if msgLen < gsoSize {
// A smaller than gsoSize packet on the tail is legal, but
// it must end the batch.
endBatch = true
}
continue
}
}
if dgramCnt > 1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
// Reset prior to incrementing base since we are preparing to start a
// new potential batch.
endBatch = false
base++
gsoSize = len(buff)
msgs[base].OOB = msgs[base].OOB[:0]
msgs[base].Buffers[0] = buff
msgs[base].Addr = addr
dgramCnt = 1
}
return base + 1
}
type sendBatch struct {
msgs []ipv6.Message
ua *net.UDPAddr
}
func (c *linuxBatchingConn) getSendBatch() *sendBatch {
batch := c.sendBatchPool.Get().(*sendBatch)
return batch
}
func (c *linuxBatchingConn) putSendBatch(batch *sendBatch) {
for i := range batch.msgs {
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers, OOB: batch.msgs[i].OOB}
}
c.sendBatchPool.Put(batch)
}
func (c *linuxBatchingConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error {
batch := c.getSendBatch()
defer c.putSendBatch(batch)
if addr.Addr().Is6() {
as16 := addr.Addr().As16()
copy(batch.ua.IP, as16[:])
batch.ua.IP = batch.ua.IP[:16]
} else {
as4 := addr.Addr().As4()
copy(batch.ua.IP, as4[:])
batch.ua.IP = batch.ua.IP[:4]
}
batch.ua.Port = int(addr.Port())
var (
n int
retried bool
)
retry:
if c.txOffload.Load() {
n = c.coalesceMessages(batch.ua, buffs, batch.msgs)
} else {
for i := range buffs {
batch.msgs[i].Buffers[0] = buffs[i]
batch.msgs[i].Addr = batch.ua
batch.msgs[i].OOB = batch.msgs[i].OOB[:0]
}
n = len(buffs)
}
err := c.writeBatch(batch.msgs[:n])
if err != nil && c.txOffload.Load() && neterror.ShouldDisableUDPGSO(err) {
c.txOffload.Store(false)
retried = true
goto retry
}
if retried {
return neterror.ErrUDPGSODisabled{OnLaddr: c.pc.LocalAddr().String(), RetryErr: err}
}
return err
}
func (c *linuxBatchingConn) SyscallConn() (syscall.RawConn, error) {
sc, ok := c.pc.(syscall.Conn)
if !ok {
return nil, errUnsupportedConnType
}
return sc.SyscallConn()
}
func (c *linuxBatchingConn) writeBatch(msgs []ipv6.Message) error {
var head int
for {
n, err := c.xpc.WriteBatch(msgs[head:], 0)
if err != nil || n == len(msgs[head:]) {
// Returning the number of packets written would require
// unraveling individual msg len and gso size during a coalesced
// write. The top of the call stack disregards partial success,
// so keep this simple for now.
return err
}
head += n
}
}
// splitCoalescedMessages splits coalesced messages from the tail of dst
// beginning at index 'firstMsgAt' into the head of the same slice. It reports
// the number of elements to evaluate in msgs for nonzero len (msgs[i].N). An
// error is returned if a socket control message cannot be parsed or a split
// operation would overflow msgs.
func (c *linuxBatchingConn) splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int) (n int, err error) {
for i := firstMsgAt; i < len(msgs); i++ {
msg := &msgs[i]
if msg.N == 0 {
return n, err
}
var (
gsoSize int
start int
end = msg.N
numToSplit = 1
)
gsoSize, err = c.getGSOSizeFromControl(msg.OOB[:msg.NN])
if err != nil {
return n, err
}
if gsoSize > 0 {
numToSplit = (msg.N + gsoSize - 1) / gsoSize
end = gsoSize
}
for j := 0; j < numToSplit; j++ {
if n > i {
return n, errors.New("splitting coalesced packet resulted in overflow")
}
copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end])
msgs[n].N = copied
msgs[n].Addr = msg.Addr
start = end
end += gsoSize
if end > msg.N {
end = msg.N
}
n++
}
if i != n-1 {
// It is legal for bytes to move within msg.Buffers[0] as a result
// of splitting, so we only zero the source msg len when it is not
// the destination of the last split operation above.
msg.N = 0
}
}
return n, nil
}
func (c *linuxBatchingConn) ReadBatch(msgs []ipv6.Message, flags int) (n int, err error) {
if !c.rxOffload || len(msgs) < 2 {
return c.xpc.ReadBatch(msgs, flags)
}
// Read into the tail of msgs, split into the head.
readAt := len(msgs) - 2
numRead, err := c.xpc.ReadBatch(msgs[readAt:], 0)
if err != nil || numRead == 0 {
return 0, err
}
return c.splitCoalescedMessages(msgs, readAt)
}
func (c *linuxBatchingConn) LocalAddr() net.Addr {
return c.pc.LocalAddr().(*net.UDPAddr)
}
func (c *linuxBatchingConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
return c.pc.WriteToUDPAddrPort(b, addr)
}
func (c *linuxBatchingConn) Close() error {
return c.pc.Close()
}
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
// and returns two booleans indicating TX and RX UDP offload support.
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
if c, ok := pconn.(*net.UDPConn); ok {
rc, err := c.SyscallConn()
if err != nil {
return
}
err = rc.Control(func(fd uintptr) {
_, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
hasTX = errSyscall == nil
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
hasRX = errSyscall == nil
})
if err != nil {
return false, false
}
}
return hasTX, hasRX
}
// getGSOSizeFromControl returns the GSO size found in control. If no GSO size
// is found or the len(control) < unix.SizeofCmsghdr, this function returns 0.
// A non-nil error will be returned if len(control) > unix.SizeofCmsghdr but
// its contents cannot be parsed as a socket control message.
func getGSOSizeFromControl(control []byte) (int, error) {
var (
hdr unix.Cmsghdr
data []byte
rem = control
err error
)
for len(rem) > unix.SizeofCmsghdr {
hdr, data, rem, err = unix.ParseOneSocketControlMessage(control)
if err != nil {
return 0, fmt.Errorf("error parsing socket control message: %w", err)
}
if hdr.Level == unix.SOL_UDP && hdr.Type == unix.UDP_GRO && len(data) >= 2 {
return int(binary.NativeEndian.Uint16(data[:2])), nil
}
}
return 0, nil
}
// setGSOSizeInControl sets a socket control message in control containing
// gsoSize. If len(control) < controlMessageSize control's len will be set to 0.
func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
*control = (*control)[:0]
if cap(*control) < int(unsafe.Sizeof(unix.Cmsghdr{})) {
return
}
if cap(*control) < controlMessageSize {
return
}
*control = (*control)[:cap(*control)]
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&(*control)[0]))
hdr.Level = unix.SOL_UDP
hdr.Type = unix.UDP_SEGMENT
hdr.SetLen(unix.CmsgLen(2))
binary.NativeEndian.PutUint16((*control)[unix.SizeofCmsghdr:], gsoSize)
*control = (*control)[:unix.CmsgSpace(2)]
}
// tryUpgradeToBatchingConn probes the capabilities of the OS and pconn, and
// upgrades pconn to a *linuxBatchingConn if appropriate.
func tryUpgradeToBatchingConn(pconn nettype.PacketConn, network string, batchSize int) nettype.PacketConn {
if network != "udp4" && network != "udp6" {
return pconn
}
if strings.HasPrefix(hostinfo.GetOSVersion(), "2.") {
// recvmmsg/sendmmsg were added in 2.6.33, but we support down to
// 2.6.32 for old NAS devices. See https://github.com/tailscale/tailscale/issues/6807.
// As a cheap heuristic: if the Linux kernel starts with "2", just
// consider it too old for mmsg. Nobody who cares about performance runs
// such ancient kernels. UDP offload was added much later, so no
// upgrades are available.
return pconn
}
uc, ok := pconn.(*net.UDPConn)
if !ok {
return pconn
}
b := &linuxBatchingConn{
pc: pconn,
getGSOSizeFromControl: getGSOSizeFromControl,
setGSOSizeInControl: setGSOSizeInControl,
sendBatchPool: sync.Pool{
New: func() any {
ua := &net.UDPAddr{
IP: make([]byte, 16),
}
msgs := make([]ipv6.Message, batchSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{
ua: ua,
msgs: msgs,
}
},
},
}
switch network {
case "udp4":
b.xpc = ipv4.NewPacketConn(uc)
case "udp6":
b.xpc = ipv6.NewPacketConn(uc)
default:
panic("bogus network")
}
var txOffload bool
txOffload, b.rxOffload = tryEnableUDPOffload(uc)
b.txOffload.Store(txOffload)
return b
}

View File

@@ -0,0 +1,244 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"encoding/binary"
"net"
"testing"
"golang.org/x/net/ipv6"
)
func setGSOSize(control *[]byte, gsoSize uint16) {
*control = (*control)[:cap(*control)]
binary.LittleEndian.PutUint16(*control, gsoSize)
}
func getGSOSize(control []byte) (int, error) {
if len(control) < 2 {
return 0, nil
}
return int(binary.LittleEndian.Uint16(control)), nil
}
func Test_linuxBatchingConn_splitCoalescedMessages(t *testing.T) {
c := &linuxBatchingConn{
setGSOSizeInControl: setGSOSize,
getGSOSizeFromControl: getGSOSize,
}
newMsg := func(n, gso int) ipv6.Message {
msg := ipv6.Message{
Buffers: [][]byte{make([]byte, 1024)},
N: n,
OOB: make([]byte, 2),
}
binary.LittleEndian.PutUint16(msg.OOB, uint16(gso))
if gso > 0 {
msg.NN = 2
}
return msg
}
cases := []struct {
name string
msgs []ipv6.Message
firstMsgAt int
wantNumEval int
wantMsgLens []int
wantErr bool
}{
{
name: "second last split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(3, 1),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 3,
wantMsgLens: []int{1, 1, 1, 0},
wantErr: false,
},
{
name: "second last no split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 1,
wantMsgLens: []int{1, 0, 0, 0},
wantErr: false,
},
{
name: "second last no split last no split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(1, 0),
},
firstMsgAt: 2,
wantNumEval: 2,
wantMsgLens: []int{1, 1, 0, 0},
wantErr: false,
},
{
name: "second last no split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(3, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(2, 1),
newMsg(2, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last no split last split overflow",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(4, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := c.splitCoalescedMessages(tt.msgs, 2)
if err != nil && !tt.wantErr {
t.Fatalf("err: %v", err)
}
if got != tt.wantNumEval {
t.Fatalf("got to eval: %d want: %d", got, tt.wantNumEval)
}
for i, msg := range tt.msgs {
if msg.N != tt.wantMsgLens[i] {
t.Fatalf("msg[%d].N: %d want: %d", i, msg.N, tt.wantMsgLens[i])
}
}
})
}
}
func Test_linuxBatchingConn_coalesceMessages(t *testing.T) {
c := &linuxBatchingConn{
setGSOSizeInControl: setGSOSize,
getGSOSizeFromControl: getGSOSize,
}
cases := []struct {
name string
buffs [][]byte
wantLens []int
wantGSO []int
}{
{
name: "one message no coalesce",
buffs: [][]byte{
make([]byte, 1, 1),
},
wantLens: []int{1},
wantGSO: []int{0},
},
{
name: "two messages equal len coalesce",
buffs: [][]byte{
make([]byte, 1, 2),
make([]byte, 1, 1),
},
wantLens: []int{2},
wantGSO: []int{1},
},
{
name: "two messages unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
},
wantLens: []int{3},
wantGSO: []int{2},
},
{
name: "three messages second unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
make([]byte, 2, 2),
},
wantLens: []int{3, 2},
wantGSO: []int{2, 0},
},
{
name: "three messages limited cap coalesce",
buffs: [][]byte{
make([]byte, 2, 4),
make([]byte, 2, 2),
make([]byte, 2, 2),
},
wantLens: []int{4, 2},
wantGSO: []int{2, 0},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
addr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 1,
}
msgs := make([]ipv6.Message, len(tt.buffs))
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0, 2)
}
got := c.coalesceMessages(addr, tt.buffs, msgs)
if got != len(tt.wantLens) {
t.Fatalf("got len %d want: %d", got, len(tt.wantLens))
}
for i := range got {
if msgs[i].Addr != addr {
t.Errorf("msgs[%d].Addr != passed addr", i)
}
gotLen := len(msgs[i].Buffers[0])
if gotLen != tt.wantLens[i] {
t.Errorf("len(msgs[%d].Buffers[0]) %d != %d", i, gotLen, tt.wantLens[i])
}
gotGSO, err := getGSOSize(msgs[i].OOB)
if err != nil {
t.Fatalf("msgs[%d] getGSOSize err: %v", i, err)
}
if gotGSO != tt.wantGSO[i] {
t.Errorf("msgs[%d] gsoSize %d != %d", i, gotGSO, tt.wantGSO[i])
}
}
})
}
}

View File

@@ -0,0 +1,182 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(ios || android || js)
package magicsock
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"slices"
"strings"
"time"
"tailscale.com/types/logger"
"tailscale.com/util/cloudenv"
)
const maxCloudInfoWait = 2 * time.Second
type cloudInfo struct {
client http.Client
logf logger.Logf
// The following parameters are fixed for the lifetime of the cloudInfo
// object, but are used for testing.
cloud cloudenv.Cloud
endpoint string
}
func newCloudInfo(logf logger.Logf) *cloudInfo {
tr := &http.Transport{
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: maxCloudInfoWait,
}).Dial,
}
return &cloudInfo{
client: http.Client{Transport: tr},
logf: logf,
cloud: cloudenv.Get(),
endpoint: "http://" + cloudenv.CommonNonRoutableMetadataIP,
}
}
// GetPublicIPs returns any public IPs attached to the current cloud instance,
// if the tailscaled process is running in a known cloud and there are any such
// IPs present.
func (ci *cloudInfo) GetPublicIPs(ctx context.Context) ([]netip.Addr, error) {
switch ci.cloud {
case cloudenv.AWS:
ret, err := ci.getAWS(ctx)
ci.logf("[v1] cloudinfo.GetPublicIPs: AWS: %v, %v", ret, err)
return ret, err
}
return nil, nil
}
// getAWSMetadata makes a request to the AWS metadata service at the given
// path, authenticating with the provided IMDSv2 token. The returned metadata
// is split by newline and returned as a slice.
func (ci *cloudInfo) getAWSMetadata(ctx context.Context, token, path string) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", ci.endpoint+path, nil)
if err != nil {
return nil, fmt.Errorf("creating request to %q: %w", path, err)
}
req.Header.Set("X-aws-ec2-metadata-token", token)
resp, err := ci.client.Do(req)
if err != nil {
return nil, fmt.Errorf("making request to metadata service %q: %w", path, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Good
case http.StatusNotFound:
// Nothing found, but this isn't an error; just return
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body for %q: %w", path, err)
}
return strings.Split(strings.TrimSpace(string(body)), "\n"), nil
}
// getAWS returns all public IPv4 and IPv6 addresses present in the AWS instance metadata.
func (ci *cloudInfo) getAWS(ctx context.Context) ([]netip.Addr, error) {
ctx, cancel := context.WithTimeout(ctx, maxCloudInfoWait)
defer cancel()
// Get a token so we can query the metadata service.
req, err := http.NewRequestWithContext(ctx, "PUT", ci.endpoint+"/latest/api/token", nil)
if err != nil {
return nil, fmt.Errorf("creating token request: %w", err)
}
req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "10")
resp, err := ci.client.Do(req)
if err != nil {
return nil, fmt.Errorf("making token request to metadata service: %w", err)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("reading token response body: %w", err)
}
token := string(body)
server := resp.Header.Get("Server")
if server != "EC2ws" {
return nil, fmt.Errorf("unexpected server header: %q", server)
}
// Iterate over all interfaces and get their public IP addresses, both IPv4 and IPv6.
macAddrs, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/")
if err != nil {
return nil, fmt.Errorf("getting interface MAC addresses: %w", err)
}
var (
addrs []netip.Addr
errs []error
)
addAddr := func(addr string) {
ip, err := netip.ParseAddr(addr)
if err != nil {
errs = append(errs, fmt.Errorf("parsing IP address %q: %w", addr, err))
return
}
addrs = append(addrs, ip)
}
for _, mac := range macAddrs {
ips, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/public-ipv4s")
if err != nil {
errs = append(errs, fmt.Errorf("getting IPv4 addresses for %q: %w", mac, err))
continue
}
for _, ip := range ips {
addAddr(ip)
}
// Try querying for IPv6 addresses.
ips, err = ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/ipv6s")
if err != nil {
errs = append(errs, fmt.Errorf("getting IPv6 addresses for %q: %w", mac, err))
continue
}
for _, ip := range ips {
addAddr(ip)
}
}
// Sort the returned addresses for determinism.
slices.SortFunc(addrs, func(a, b netip.Addr) int {
return a.Compare(b)
})
// Preferentially return any addresses we found, even if there were errors.
if len(addrs) > 0 {
return addrs, nil
}
if len(errs) > 0 {
return nil, fmt.Errorf("getting IP addresses: %w", errors.Join(errs...))
}
return nil, nil
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || android || js
package magicsock
import (
"context"
"net/netip"
"tailscale.com/types/logger"
)
type cloudInfo struct{}
func newCloudInfo(_ logger.Logf) *cloudInfo {
return &cloudInfo{}
}
func (ci *cloudInfo) GetPublicIPs(_ context.Context) ([]netip.Addr, error) {
return nil, nil
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"context"
"net/http"
"net/http/httptest"
"net/netip"
"slices"
"testing"
"tailscale.com/util/cloudenv"
)
func TestCloudInfo_AWS(t *testing.T) {
const (
mac1 = "06:1d:00:00:00:00"
mac2 = "06:1d:00:00:00:01"
publicV4 = "1.2.3.4"
otherV4_1 = "5.6.7.8"
otherV4_2 = "11.12.13.14"
v6addr = "2001:db8::1"
macsPrefix = "/latest/meta-data/network/interfaces/macs/"
)
// Launch a fake AWS IMDS server
fake := &fakeIMDS{
tb: t,
paths: map[string]string{
macsPrefix: mac1 + "\n" + mac2,
// This is the "main" public IP address for the instance
macsPrefix + mac1 + "/public-ipv4s": publicV4,
// There's another interface with two public IPs
// attached to it and an IPv6 address, all of which we
// should discover.
macsPrefix + mac2 + "/public-ipv4s": otherV4_1 + "\n" + otherV4_2,
macsPrefix + mac2 + "/ipv6s": v6addr,
},
}
srv := httptest.NewServer(fake)
defer srv.Close()
ci := newCloudInfo(t.Logf)
ci.cloud = cloudenv.AWS
ci.endpoint = srv.URL
ips, err := ci.GetPublicIPs(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
wantIPs := []netip.Addr{
netip.MustParseAddr(publicV4),
netip.MustParseAddr(otherV4_1),
netip.MustParseAddr(otherV4_2),
netip.MustParseAddr(v6addr),
}
if !slices.Equal(ips, wantIPs) {
t.Fatalf("got %v, want %v", ips, wantIPs)
}
}
func TestCloudInfo_AWSNotPublic(t *testing.T) {
returns404 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "PUT" && r.URL.Path == "/latest/api/token" {
w.Header().Set("Server", "EC2ws")
w.Write([]byte("fake-imds-token"))
return
}
http.NotFound(w, r)
})
srv := httptest.NewServer(returns404)
defer srv.Close()
ci := newCloudInfo(t.Logf)
ci.cloud = cloudenv.AWS
ci.endpoint = srv.URL
// If the IMDS server doesn't return any public IPs, it's not an error
// and we should just get an empty list.
ips, err := ci.GetPublicIPs(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(ips) != 0 {
t.Fatalf("got %v, want none", ips)
}
}
type fakeIMDS struct {
tb testing.TB
paths map[string]string
}
func (f *fakeIMDS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.tb.Logf("%s %s", r.Method, r.URL.Path)
path := r.URL.Path
// Handle the /latest/api/token case
const token = "fake-imds-token"
if r.Method == "PUT" && path == "/latest/api/token" {
w.Header().Set("Server", "EC2ws")
w.Write([]byte(token))
return
}
// Otherwise, require the IMDSv2 token to be set
if r.Header.Get("X-aws-ec2-metadata-token") != token {
f.tb.Errorf("missing or invalid IMDSv2 token")
http.Error(w, "missing or invalid IMDSv2 token", http.StatusForbidden)
return
}
if v, ok := f.paths[path]; ok {
w.Write([]byte(v))
return
}
http.NotFound(w, r)
}

View File

@@ -25,7 +25,6 @@ import (
"github.com/tailscale/wireguard-go/conn"
"go4.org/mem"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/control/controlknobs"
@@ -133,6 +132,9 @@ type Conn struct {
// bind is the wireguard-go conn.Bind for Conn.
bind *connBind
// cloudInfo is used to query cloud metadata services.
cloudInfo *cloudInfo
// ============================================================
// Fields that must be accessed via atomic load/stores.
@@ -425,9 +427,10 @@ func (o *Options) derpActiveFunc() func() {
// newConn is the error-free, network-listening-side-effect-free based
// of NewConn. Mostly for tests.
func newConn() *Conn {
func newConn(logf logger.Logf) *Conn {
discoPrivate := key.NewDisco()
c := &Conn{
logf: logf,
derpRecvCh: make(chan derpReadResult, 1), // must be buffered, see issue 3736
derpStarted: make(chan struct{}),
peerLastDerp: make(map[key.NodePublic]int),
@@ -435,6 +438,7 @@ func newConn() *Conn {
discoInfo: make(map[key.DiscoPublic]*discoInfo),
discoPrivate: discoPrivate,
discoPublic: discoPrivate.Public(),
cloudInfo: newCloudInfo(logf),
}
c.discoShort = c.discoPublic.ShortString()
c.bind = &connBind{Conn: c, closed: true}
@@ -462,10 +466,9 @@ func NewConn(opts Options) (*Conn, error) {
return nil, errors.New("magicsock.Options.NetMon must be non-nil")
}
c := newConn()
c := newConn(opts.logf())
c.port.Store(uint32(opts.Port))
c.controlKnobs = opts.ControlKnobs
c.logf = opts.logf()
c.epFunc = opts.endpointsFunc()
c.derpActiveFunc = opts.derpActiveFunc()
c.idleFunc = opts.IdleFunc
@@ -952,6 +955,27 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
addAddr(ap, tailcfg.EndpointExplicitConf)
}
// If we're on a cloud instance, we might have a public IPv4 or IPv6
// address that we can be reached at. Find those, if they exist, and
// add them.
if addrs, err := c.cloudInfo.GetPublicIPs(ctx); err == nil {
var port4, port6 uint16
if addr := c.pconn4.LocalAddr(); addr != nil {
port4 = uint16(addr.Port)
}
if addr := c.pconn6.LocalAddr(); addr != nil {
port6 = uint16(addr.Port)
}
for _, addr := range addrs {
if addr.Is4() && port4 > 0 {
addAddr(netip.AddrPortFrom(addr, port4), tailcfg.EndpointLocal)
} else if addr.Is6() && port6 > 0 {
addAddr(netip.AddrPortFrom(addr, port6), tailcfg.EndpointLocal)
}
}
}
// Update our set of endpoints by adding any endpoints that we
// previously found but haven't expired yet. This also updates the
// cache with the set of endpoints discovered in this function.
@@ -1076,12 +1100,6 @@ var errNoUDP = errors.New("no UDP available on platform")
var errUnsupportedConnType = errors.New("unsupported connection type")
var (
// This acts as a compile-time check for our usage of ipv6.Message in
// batchingUDPConn for both IPv6 and IPv4 operations.
_ ipv6.Message = ipv4.Message{}
)
func (c *Conn) sendUDPBatch(addr netip.AddrPort, buffs [][]byte) (sent bool, err error) {
isIPv6 := false
switch {
@@ -2631,153 +2649,6 @@ func (c *Conn) ParseEndpoint(nodeKeyStr string) (conn.Endpoint, error) {
return ep, nil
}
func (c *batchingUDPConn) writeBatch(msgs []ipv6.Message) error {
var head int
for {
n, err := c.xpc.WriteBatch(msgs[head:], 0)
if err != nil || n == len(msgs[head:]) {
// Returning the number of packets written would require
// unraveling individual msg len and gso size during a coalesced
// write. The top of the call stack disregards partial success,
// so keep this simple for now.
return err
}
head += n
}
}
// splitCoalescedMessages splits coalesced messages from the tail of dst
// beginning at index 'firstMsgAt' into the head of the same slice. It reports
// the number of elements to evaluate in msgs for nonzero len (msgs[i].N). An
// error is returned if a socket control message cannot be parsed or a split
// operation would overflow msgs.
func (c *batchingUDPConn) splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int) (n int, err error) {
for i := firstMsgAt; i < len(msgs); i++ {
msg := &msgs[i]
if msg.N == 0 {
return n, err
}
var (
gsoSize int
start int
end = msg.N
numToSplit = 1
)
gsoSize, err = c.getGSOSizeFromControl(msg.OOB[:msg.NN])
if err != nil {
return n, err
}
if gsoSize > 0 {
numToSplit = (msg.N + gsoSize - 1) / gsoSize
end = gsoSize
}
for j := 0; j < numToSplit; j++ {
if n > i {
return n, errors.New("splitting coalesced packet resulted in overflow")
}
copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end])
msgs[n].N = copied
msgs[n].Addr = msg.Addr
start = end
end += gsoSize
if end > msg.N {
end = msg.N
}
n++
}
if i != n-1 {
// It is legal for bytes to move within msg.Buffers[0] as a result
// of splitting, so we only zero the source msg len when it is not
// the destination of the last split operation above.
msg.N = 0
}
}
return n, nil
}
func (c *batchingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (n int, err error) {
if !c.rxOffload || len(msgs) < 2 {
return c.xpc.ReadBatch(msgs, flags)
}
// Read into the tail of msgs, split into the head.
readAt := len(msgs) - 2
numRead, err := c.xpc.ReadBatch(msgs[readAt:], 0)
if err != nil || numRead == 0 {
return 0, err
}
return c.splitCoalescedMessages(msgs, readAt)
}
func (c *batchingUDPConn) LocalAddr() net.Addr {
return c.pc.LocalAddr().(*net.UDPAddr)
}
func (c *batchingUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
return c.pc.WriteToUDPAddrPort(b, addr)
}
func (c *batchingUDPConn) Close() error {
return c.pc.Close()
}
// tryUpgradeToBatchingUDPConn probes the capabilities of the OS and pconn, and
// upgrades pconn to a *batchingUDPConn if appropriate.
func tryUpgradeToBatchingUDPConn(pconn nettype.PacketConn, network string, batchSize int) nettype.PacketConn {
if network != "udp4" && network != "udp6" {
return pconn
}
if runtime.GOOS != "linux" {
return pconn
}
if strings.HasPrefix(hostinfo.GetOSVersion(), "2.") {
// recvmmsg/sendmmsg were added in 2.6.33, but we support down to
// 2.6.32 for old NAS devices. See https://github.com/tailscale/tailscale/issues/6807.
// As a cheap heuristic: if the Linux kernel starts with "2", just
// consider it too old for mmsg. Nobody who cares about performance runs
// such ancient kernels. UDP offload was added much later, so no
// upgrades are available.
return pconn
}
uc, ok := pconn.(*net.UDPConn)
if !ok {
return pconn
}
b := &batchingUDPConn{
pc: pconn,
getGSOSizeFromControl: getGSOSizeFromControl,
setGSOSizeInControl: setGSOSizeInControl,
sendBatchPool: sync.Pool{
New: func() any {
ua := &net.UDPAddr{
IP: make([]byte, 16),
}
msgs := make([]ipv6.Message, batchSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{
ua: ua,
msgs: msgs,
}
},
},
}
switch network {
case "udp4":
b.xpc = ipv4.NewPacketConn(uc)
case "udp6":
b.xpc = ipv6.NewPacketConn(uc)
default:
panic("bogus network")
}
var txOffload bool
txOffload, b.rxOffload = tryEnableUDPOffload(uc)
b.txOffload.Store(txOffload)
return b
}
func newBlockForeverConn() *blockForeverConn {
c := new(blockForeverConn)
c.cond = sync.NewCond(&c.mu)

View File

@@ -21,16 +21,6 @@ func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
portableTrySetSocketBuffer(pconn, logf)
}
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
return false, false
}
func getGSOSizeFromControl(control []byte) (int, error) {
return 0, nil
}
func setGSOSizeInControl(control *[]byte, gso uint16) {}
const (
controlMessageSize = 0
)

View File

@@ -318,70 +318,6 @@ func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
}
}
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
// and returns two booleans indicating TX and RX UDP offload support.
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
if c, ok := pconn.(*net.UDPConn); ok {
rc, err := c.SyscallConn()
if err != nil {
return
}
err = rc.Control(func(fd uintptr) {
_, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
hasTX = errSyscall == nil
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
hasRX = errSyscall == nil
})
if err != nil {
return false, false
}
}
return hasTX, hasRX
}
// getGSOSizeFromControl returns the GSO size found in control. If no GSO size
// is found or the len(control) < unix.SizeofCmsghdr, this function returns 0.
// A non-nil error will be returned if len(control) > unix.SizeofCmsghdr but
// its contents cannot be parsed as a socket control message.
func getGSOSizeFromControl(control []byte) (int, error) {
var (
hdr unix.Cmsghdr
data []byte
rem = control
err error
)
for len(rem) > unix.SizeofCmsghdr {
hdr, data, rem, err = unix.ParseOneSocketControlMessage(control)
if err != nil {
return 0, fmt.Errorf("error parsing socket control message: %w", err)
}
if hdr.Level == unix.SOL_UDP && hdr.Type == unix.UDP_GRO && len(data) >= 2 {
return int(binary.NativeEndian.Uint16(data[:2])), nil
}
}
return 0, nil
}
// setGSOSizeInControl sets a socket control message in control containing
// gsoSize. If len(control) < controlMessageSize control's len will be set to 0.
func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
*control = (*control)[:0]
if cap(*control) < int(unsafe.Sizeof(unix.Cmsghdr{})) {
return
}
if cap(*control) < controlMessageSize {
return
}
*control = (*control)[:cap(*control)]
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&(*control)[0]))
hdr.Level = unix.SOL_UDP
hdr.Type = unix.UDP_SEGMENT
hdr.SetLen(unix.CmsgLen(2))
binary.NativeEndian.PutUint16((*control)[unix.SizeofCmsghdr:], gsoSize)
*control = (*control)[:unix.CmsgSpace(2)]
}
var controlMessageSize = -1 // bomb if used for allocation before init
func init() {

View File

@@ -35,7 +35,6 @@ import (
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/control/controlknobs"
"tailscale.com/derp"
@@ -452,7 +451,7 @@ func TestPickDERPFallback(t *testing.T) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
c := newConn()
c := newConn(t.Logf)
dm := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {},
@@ -483,7 +482,7 @@ func TestPickDERPFallback(t *testing.T) {
// distribution over nodes works.
got := map[int]int{}
for range 50 {
c = newConn()
c = newConn(t.Logf)
c.derpMap = dm
got[c.pickDERPFallback()]++
}
@@ -1185,8 +1184,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
}
func TestDiscoMessage(t *testing.T) {
c := newConn()
c.logf = t.Logf
c := newConn(t.Logf)
c.privateKey = key.NewNode()
peer1Pub := c.DiscoPublicKey()
@@ -2039,238 +2037,6 @@ func TestBufferedDerpWritesBeforeDrop(t *testing.T) {
t.Logf("bufferedDerpWritesBeforeDrop = %d", vv)
}
func setGSOSize(control *[]byte, gsoSize uint16) {
*control = (*control)[:cap(*control)]
binary.LittleEndian.PutUint16(*control, gsoSize)
}
func getGSOSize(control []byte) (int, error) {
if len(control) < 2 {
return 0, nil
}
return int(binary.LittleEndian.Uint16(control)), nil
}
func Test_batchingUDPConn_splitCoalescedMessages(t *testing.T) {
c := &batchingUDPConn{
setGSOSizeInControl: setGSOSize,
getGSOSizeFromControl: getGSOSize,
}
newMsg := func(n, gso int) ipv6.Message {
msg := ipv6.Message{
Buffers: [][]byte{make([]byte, 1024)},
N: n,
OOB: make([]byte, 2),
}
binary.LittleEndian.PutUint16(msg.OOB, uint16(gso))
if gso > 0 {
msg.NN = 2
}
return msg
}
cases := []struct {
name string
msgs []ipv6.Message
firstMsgAt int
wantNumEval int
wantMsgLens []int
wantErr bool
}{
{
name: "second last split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(3, 1),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 3,
wantMsgLens: []int{1, 1, 1, 0},
wantErr: false,
},
{
name: "second last no split last empty",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 1,
wantMsgLens: []int{1, 0, 0, 0},
wantErr: false,
},
{
name: "second last no split last no split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(1, 0),
},
firstMsgAt: 2,
wantNumEval: 2,
wantMsgLens: []int{1, 1, 0, 0},
wantErr: false,
},
{
name: "second last no split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(3, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last split last split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(2, 1),
newMsg(2, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
},
{
name: "second last no split last split overflow",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(4, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := c.splitCoalescedMessages(tt.msgs, 2)
if err != nil && !tt.wantErr {
t.Fatalf("err: %v", err)
}
if got != tt.wantNumEval {
t.Fatalf("got to eval: %d want: %d", got, tt.wantNumEval)
}
for i, msg := range tt.msgs {
if msg.N != tt.wantMsgLens[i] {
t.Fatalf("msg[%d].N: %d want: %d", i, msg.N, tt.wantMsgLens[i])
}
}
})
}
}
func Test_batchingUDPConn_coalesceMessages(t *testing.T) {
c := &batchingUDPConn{
setGSOSizeInControl: setGSOSize,
getGSOSizeFromControl: getGSOSize,
}
cases := []struct {
name string
buffs [][]byte
wantLens []int
wantGSO []int
}{
{
name: "one message no coalesce",
buffs: [][]byte{
make([]byte, 1, 1),
},
wantLens: []int{1},
wantGSO: []int{0},
},
{
name: "two messages equal len coalesce",
buffs: [][]byte{
make([]byte, 1, 2),
make([]byte, 1, 1),
},
wantLens: []int{2},
wantGSO: []int{1},
},
{
name: "two messages unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
},
wantLens: []int{3},
wantGSO: []int{2},
},
{
name: "three messages second unequal len coalesce",
buffs: [][]byte{
make([]byte, 2, 3),
make([]byte, 1, 1),
make([]byte, 2, 2),
},
wantLens: []int{3, 2},
wantGSO: []int{2, 0},
},
{
name: "three messages limited cap coalesce",
buffs: [][]byte{
make([]byte, 2, 4),
make([]byte, 2, 2),
make([]byte, 2, 2),
},
wantLens: []int{4, 2},
wantGSO: []int{2, 0},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
addr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 1,
}
msgs := make([]ipv6.Message, len(tt.buffs))
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0, 2)
}
got := c.coalesceMessages(addr, tt.buffs, msgs)
if got != len(tt.wantLens) {
t.Fatalf("got len %d want: %d", got, len(tt.wantLens))
}
for i := range got {
if msgs[i].Addr != addr {
t.Errorf("msgs[%d].Addr != passed addr", i)
}
gotLen := len(msgs[i].Buffers[0])
if gotLen != tt.wantLens[i] {
t.Errorf("len(msgs[%d].Buffers[0]) %d != %d", i, gotLen, tt.wantLens[i])
}
gotGSO, err := getGSOSize(msgs[i].OOB)
if err != nil {
t.Fatalf("msgs[%d] getGSOSize err: %v", i, err)
}
if gotGSO != tt.wantGSO[i] {
t.Errorf("msgs[%d] gsoSize %d != %d", i, gotGSO, tt.wantGSO[i])
}
}
})
}
}
// newWireguard starts up a new wireguard-go device attached to a test tun, and
// returns the device, tun and endpoint port. To add peers call device.IpcSet with UAPI instructions.
func newWireguard(t *testing.T, uapi string, aips []netip.Prefix) (*device.Device, *tuntest.ChannelTUN, uint16) {
@@ -3161,8 +2927,7 @@ func TestMaybeSetNearestDERP(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ht := new(health.Tracker)
c := newConn()
c.logf = t.Logf
c := newConn(t.Logf)
c.myDerp = tt.old
c.derpMap = derpMap
c.health = ht

View File

@@ -35,12 +35,12 @@ type RebindingUDPConn struct {
// setConnLocked sets the provided nettype.PacketConn. It should be called only
// after acquiring RebindingUDPConn.mu. It upgrades the provided
// nettype.PacketConn to a *batchingUDPConn when appropriate. This upgrade
// is intentionally pushed closest to where read/write ops occur in order to
// avoid disrupting surrounding code that assumes nettype.PacketConn is a
// nettype.PacketConn to a batchingConn when appropriate. This upgrade is
// intentionally pushed closest to where read/write ops occur in order to avoid
// disrupting surrounding code that assumes nettype.PacketConn is a
// *net.UDPConn.
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, batchSize int) {
upc := tryUpgradeToBatchingUDPConn(p, network, batchSize)
upc := tryUpgradeToBatchingConn(p, network, batchSize)
c.pconn = upc
c.pconnAtomic.Store(&upc)
c.port = uint16(c.localAddrLocked().Port)
@@ -74,7 +74,7 @@ func (c *RebindingUDPConn) ReadFromUDPAddrPort(b []byte) (int, netip.AddrPort, e
func (c *RebindingUDPConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error {
for {
pconn := *c.pconnAtomic.Load()
b, ok := pconn.(*batchingUDPConn)
b, ok := pconn.(batchingConn)
if !ok {
for _, buf := range buffs {
_, err := c.writeToUDPAddrPortWithInitPconn(pconn, buf, addr)
@@ -101,7 +101,7 @@ func (c *RebindingUDPConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) err
func (c *RebindingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (int, error) {
for {
pconn := *c.pconnAtomic.Load()
b, ok := pconn.(*batchingUDPConn)
b, ok := pconn.(batchingConn)
if !ok {
n, ap, err := c.readFromWithInitPconn(pconn, msgs[0].Buffers[0])
if err == nil {

View File

@@ -0,0 +1,16 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package netstack
import (
nsgro "gvisor.dev/gvisor/pkg/tcpip/stack/gro"
)
// gro wraps a gVisor GRO implementation. It exists solely to prevent iOS from
// importing said package (see _ios.go).
type gro struct {
nsgro.GRO
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package netstack
import (
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// gro on iOS delivers packets to its Dispatcher, immediately. This type exists
// to prevent importation of the gVisor GRO implementation as said package
// increases binary size. This is a penalty we do not wish to pay since we
// currently do not leverage GRO on iOS.
type gro struct {
Dispatcher stack.NetworkDispatcher
}
func (g *gro) Init(v bool) {
if v {
panic("GRO is not supported on this platform")
}
}
func (g *gro) Flush() {}
func (g *gro) Enqueue(pkt *stack.PacketBuffer) {
g.Dispatcher.DeliverNetworkPacket(pkt.NetworkProtocolNumber, pkt)
}

View File

@@ -0,0 +1,414 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package netstack
import (
"bytes"
"context"
"sync"
"github.com/tailscale/wireguard-go/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/header/parse"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
)
type queue struct {
// TODO(jwhited): evaluate performance with mu as Mutex and/or alternative
// non-channel buffer.
c chan *stack.PacketBuffer
mu sync.RWMutex // mu guards closed
closed bool
}
func (q *queue) Close() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.closed {
close(q.c)
}
q.closed = true
}
func (q *queue) Read() *stack.PacketBuffer {
select {
case p := <-q.c:
return p
default:
return nil
}
}
func (q *queue) ReadContext(ctx context.Context) *stack.PacketBuffer {
select {
case pkt := <-q.c:
return pkt
case <-ctx.Done():
return nil
}
}
func (q *queue) Write(pkt *stack.PacketBuffer) tcpip.Error {
// q holds the PacketBuffer.
q.mu.RLock()
defer q.mu.RUnlock()
if q.closed {
return &tcpip.ErrClosedForSend{}
}
wrote := false
select {
case q.c <- pkt.IncRef():
wrote = true
default:
// TODO(jwhited): reconsider/count
pkt.DecRef()
}
if wrote {
return nil
}
return &tcpip.ErrNoBufferSpace{}
}
func (q *queue) Num() int {
return len(q.c)
}
var _ stack.LinkEndpoint = (*linkEndpoint)(nil)
var _ stack.GSOEndpoint = (*linkEndpoint)(nil)
// linkEndpoint implements stack.LinkEndpoint and stack.GSOEndpoint. Outbound
// packets written by gVisor towards Tailscale are stored in a channel.
// Inbound is fed to gVisor via injectInbound or enqueueGRO. This is loosely
// modeled after gvisor.dev/pkg/tcpip/link/channel.Endpoint.
type linkEndpoint struct {
SupportedGSOKind stack.SupportedGSO
initGRO initGRO
mu sync.RWMutex // mu guards the following fields
dispatcher stack.NetworkDispatcher
linkAddr tcpip.LinkAddress
mtu uint32
gro gro // mu only guards access to gro.Dispatcher
q *queue // outbound
}
// TODO(jwhited): move to linkEndpointOpts struct or similar.
type initGRO bool
const (
disableGRO initGRO = false
enableGRO initGRO = true
)
func newLinkEndpoint(size int, mtu uint32, linkAddr tcpip.LinkAddress, gro initGRO) *linkEndpoint {
le := &linkEndpoint{
q: &queue{
c: make(chan *stack.PacketBuffer, size),
},
mtu: mtu,
linkAddr: linkAddr,
}
le.initGRO = gro
le.gro.Init(bool(gro))
return le
}
// Close closes l. Further packet injections will return an error, and all
// pending packets are discarded. Close may be called concurrently with
// WritePackets.
func (l *linkEndpoint) Close() {
l.mu.Lock()
if l.gro.Dispatcher != nil {
l.gro.Flush()
}
l.dispatcher = nil
l.gro.Dispatcher = nil
l.mu.Unlock()
l.q.Close()
l.Drain()
}
// Read does non-blocking read one packet from the outbound packet queue.
func (l *linkEndpoint) Read() *stack.PacketBuffer {
return l.q.Read()
}
// ReadContext does blocking read for one packet from the outbound packet queue.
// It can be cancelled by ctx, and in this case, it returns nil.
func (l *linkEndpoint) ReadContext(ctx context.Context) *stack.PacketBuffer {
return l.q.ReadContext(ctx)
}
// Drain removes all outbound packets from the channel and counts them.
func (l *linkEndpoint) Drain() int {
c := 0
for pkt := l.Read(); pkt != nil; pkt = l.Read() {
pkt.DecRef()
c++
}
return c
}
// NumQueued returns the number of packets queued for outbound.
func (l *linkEndpoint) NumQueued() int {
return l.q.Num()
}
// rxChecksumOffload validates IPv4, TCP, and UDP header checksums in p,
// returning an equivalent *stack.PacketBuffer if they are valid, otherwise nil.
// The set of headers validated covers where gVisor would perform validation if
// !stack.PacketBuffer.RXChecksumValidated, i.e. it satisfies
// stack.CapabilityRXChecksumOffload. Other protocols with checksum fields,
// e.g. ICMP{v6}, are still validated by gVisor regardless of rx checksum
// offloading capabilities.
func rxChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
var (
pn tcpip.NetworkProtocolNumber
csumStart int
)
buf := p.Buffer()
switch p.IPVersion {
case 4:
if len(buf) < header.IPv4MinimumSize {
return nil
}
csumStart = int((buf[0] & 0x0F) * 4)
if csumStart < header.IPv4MinimumSize || csumStart > header.IPv4MaximumHeaderSize || len(buf) < csumStart {
return nil
}
if ^tun.Checksum(buf[:csumStart], 0) != 0 {
return nil
}
pn = header.IPv4ProtocolNumber
case 6:
if len(buf) < header.IPv6FixedHeaderSize {
return nil
}
csumStart = header.IPv6FixedHeaderSize
pn = header.IPv6ProtocolNumber
if p.IPProto != ipproto.ICMPv6 && p.IPProto != ipproto.TCP && p.IPProto != ipproto.UDP {
// buf could have extension headers before a UDP or TCP header, but
// packet.Parsed.IPProto will be set to the ext header type, so we
// have to look deeper. We are still responsible for validating the
// L4 checksum in this case. So, make use of gVisor's existing
// extension header parsing via parse.IPv6() in order to unpack the
// L4 csumStart index. This is not particularly efficient as we have
// to allocate a short-lived stack.PacketBuffer that cannot be
// re-used. parse.IPv6() "consumes" the IPv6 headers, so we can't
// inject this stack.PacketBuffer into the stack at a later point.
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(buf)),
})
defer packetBuf.DecRef()
// The rightmost bool returns false only if packetBuf is too short,
// which we've already accounted for above.
transportProto, _, _, _, _ := parse.IPv6(packetBuf)
if transportProto == header.TCPProtocolNumber || transportProto == header.UDPProtocolNumber {
csumLen := packetBuf.Data().Size()
if len(buf) < csumLen {
return nil
}
csumStart = len(buf) - csumLen
p.IPProto = ipproto.Proto(transportProto)
}
}
}
if p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP {
lenForPseudo := len(buf) - csumStart
csum := tun.PseudoHeaderChecksum(
uint8(p.IPProto),
p.Src.Addr().AsSlice(),
p.Dst.Addr().AsSlice(),
uint16(lenForPseudo))
csum = tun.Checksum(buf[csumStart:], csum)
if ^csum != 0 {
return nil
}
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(buf)),
})
packetBuf.NetworkProtocolNumber = pn
// Setting this is not technically required. gVisor overrides where
// stack.CapabilityRXChecksumOffload is advertised from Capabilities().
// https://github.com/google/gvisor/blob/64c016c92987cc04dfd4c7b091ddd21bdad875f8/pkg/tcpip/stack/nic.go#L763
// This is also why we offload for all packets since we cannot signal this
// per-packet.
packetBuf.RXChecksumValidated = true
return packetBuf
}
func (l *linkEndpoint) injectInbound(p *packet.Parsed) {
l.mu.RLock()
d := l.dispatcher
l.mu.RUnlock()
if d == nil {
return
}
pkt := rxChecksumOffload(p)
if pkt == nil {
return
}
d.DeliverNetworkPacket(pkt.NetworkProtocolNumber, pkt)
pkt.DecRef()
}
// enqueueGRO enqueues the provided packet for GRO. It may immediately deliver
// it to the underlying stack.NetworkDispatcher depending on its contents and if
// GRO was initialized via newLinkEndpoint. To explicitly flush previously
// enqueued packets see flushGRO. enqueueGRO is not thread-safe and must not
// be called concurrently with flushGRO.
func (l *linkEndpoint) enqueueGRO(p *packet.Parsed) {
l.mu.RLock()
defer l.mu.RUnlock()
if l.gro.Dispatcher == nil {
return
}
pkt := rxChecksumOffload(p)
if pkt == nil {
return
}
// TODO(jwhited): gro.Enqueue() duplicates a lot of p.Decode().
// We may want to push stack.PacketBuffer further up as a
// replacement for packet.Parsed, or inversely push packet.Parsed
// down into refactored GRO logic.
l.gro.Enqueue(pkt)
pkt.DecRef()
}
// flushGRO flushes previously enqueueGRO'd packets to the underlying
// stack.NetworkDispatcher. flushGRO is not thread-safe, and must not be
// called concurrently with enqueueGRO.
func (l *linkEndpoint) flushGRO() {
if !l.initGRO {
// If GRO was not initialized fast path return to avoid scanning GRO
// buckets (see l.gro.Flush()) that will always be empty.
return
}
l.mu.RLock()
defer l.mu.RUnlock()
if l.gro.Dispatcher != nil {
l.gro.Flush()
}
}
// Attach saves the stack network-layer dispatcher for use later when packets
// are injected.
func (l *linkEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
l.mu.Lock()
defer l.mu.Unlock()
l.dispatcher = dispatcher
l.gro.Dispatcher = dispatcher
}
// IsAttached implements stack.LinkEndpoint.IsAttached.
func (l *linkEndpoint) IsAttached() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return l.dispatcher != nil
}
// MTU implements stack.LinkEndpoint.MTU.
func (l *linkEndpoint) MTU() uint32 {
l.mu.RLock()
defer l.mu.RUnlock()
return l.mtu
}
// SetMTU implements stack.LinkEndpoint.SetMTU.
func (l *linkEndpoint) SetMTU(mtu uint32) {
l.mu.Lock()
defer l.mu.Unlock()
l.mtu = mtu
}
// Capabilities implements stack.LinkEndpoint.Capabilities.
func (l *linkEndpoint) Capabilities() stack.LinkEndpointCapabilities {
// We are required to offload RX checksum validation for the purposes of
// GRO.
return stack.CapabilityRXChecksumOffload
}
// GSOMaxSize implements stack.GSOEndpoint.
func (*linkEndpoint) GSOMaxSize() uint32 {
// This an increase from 32k returned by channel.Endpoint.GSOMaxSize() to
// 64k, which improves throughput.
return (1 << 16) - 1
}
// SupportedGSO implements stack.GSOEndpoint.
func (l *linkEndpoint) SupportedGSO() stack.SupportedGSO {
return l.SupportedGSOKind
}
// MaxHeaderLength returns the maximum size of the link layer header. Given it
// doesn't have a header, it just returns 0.
func (*linkEndpoint) MaxHeaderLength() uint16 {
return 0
}
// LinkAddress returns the link address of this endpoint.
func (l *linkEndpoint) LinkAddress() tcpip.LinkAddress {
l.mu.RLock()
defer l.mu.RUnlock()
return l.linkAddr
}
// SetLinkAddress implements stack.LinkEndpoint.SetLinkAddress.
func (l *linkEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
l.mu.Lock()
defer l.mu.Unlock()
l.linkAddr = addr
}
// WritePackets stores outbound packets into the channel.
// Multiple concurrent calls are permitted.
func (l *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
n := 0
// TODO(jwhited): evaluate writing a stack.PacketBufferList instead of a
// single packet. We can split 2 x 64K GSO across
// wireguard-go/conn.IdealBatchSize (128 slots) @ 1280 MTU, and non-GSO we
// could do more. Read API would need to change to take advantage. Verify
// gVisor limits around max number of segments packed together. Since we
// control MTU (and by effect TCP MSS in gVisor) we *shouldn't* expect to
// ever overflow 128 slots (see wireguard-go/tun.ErrTooManySegments usage).
for _, pkt := range pkts.AsSlice() {
if err := l.q.Write(pkt); err != nil {
if _, ok := err.(*tcpip.ErrNoBufferSpace); !ok && n == 0 {
return 0, err
}
break
}
n++
}
return n, nil
}
// Wait implements stack.LinkEndpoint.Wait.
func (*linkEndpoint) Wait() {}
// ARPHardwareType implements stack.LinkEndpoint.ARPHardwareType.
func (*linkEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
// AddHeader implements stack.LinkEndpoint.AddHeader.
func (*linkEndpoint) AddHeader(*stack.PacketBuffer) {}
// ParseHeader implements stack.LinkEndpoint.ParseHeader.
func (*linkEndpoint) ParseHeader(*stack.PacketBuffer) bool { return true }
// SetOnCloseAction implements stack.LinkEndpoint.
func (*linkEndpoint) SetOnCloseAction(func()) {}

View File

@@ -0,0 +1,112 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package netstack
import (
"bytes"
"net/netip"
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/net/packet"
)
func Test_rxChecksumOffload(t *testing.T) {
payloadLen := 100
tcpFields := &header.TCPFields{
SrcPort: 1,
DstPort: 1,
SeqNum: 1,
AckNum: 1,
DataOffset: 20,
Flags: header.TCPFlagAck | header.TCPFlagPsh,
WindowSize: 3000,
}
tcp4 := make([]byte, 20+20+payloadLen)
ipv4H := header.IPv4(tcp4)
ipv4H.Encode(&header.IPv4Fields{
SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.1").AsSlice()),
DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("192.0.2.2").AsSlice()),
Protocol: uint8(header.TCPProtocolNumber),
TTL: 64,
TotalLength: uint16(len(tcp4)),
})
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
tcpH := header.TCP(tcp4[20:])
tcpH.Encode(tcpFields)
pseudoCsum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(20+payloadLen))
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
tcp6ExtHeader := make([]byte, 40+8+20+payloadLen)
ipv6H := header.IPv6(tcp6ExtHeader)
ipv6H.Encode(&header.IPv6Fields{
SrcAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::1").AsSlice()),
DstAddr: tcpip.AddrFromSlice(netip.MustParseAddr("2001:db8::2").AsSlice()),
TransportProtocol: 60, // really next header; destination options ext header
HopLimit: 64,
PayloadLength: uint16(8 + 20 + payloadLen),
})
tcp6ExtHeader[40] = uint8(header.TCPProtocolNumber) // next header
tcp6ExtHeader[41] = 0 // length of ext header in 8-octet units, exclusive of first 8 octets.
// 42-47 options and padding
tcpH = header.TCP(tcp6ExtHeader[48:])
tcpH.Encode(tcpFields)
pseudoCsum = header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipv6H.SourceAddress(), ipv6H.DestinationAddress(), uint16(20+payloadLen))
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
tcp4InvalidCsum := make([]byte, len(tcp4))
copy(tcp4InvalidCsum, tcp4)
at := 20 + 16
tcp4InvalidCsum[at] = ^tcp4InvalidCsum[at]
tcp6ExtHeaderInvalidCsum := make([]byte, len(tcp6ExtHeader))
copy(tcp6ExtHeaderInvalidCsum, tcp6ExtHeader)
at = 40 + 8 + 16
tcp6ExtHeaderInvalidCsum[at] = ^tcp6ExtHeaderInvalidCsum[at]
tests := []struct {
name string
input []byte
wantPB bool
}{
{
"tcp4 packet valid csum",
tcp4,
true,
},
{
"tcp6 with ext header valid csum",
tcp6ExtHeader,
true,
},
{
"tcp4 packet invalid csum",
tcp4InvalidCsum,
false,
},
{
"tcp6 with ext header invalid csum",
tcp6ExtHeaderInvalidCsum,
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &packet.Parsed{}
p.Decode(tt.input)
got := rxChecksumOffload(p)
if tt.wantPB != (got != nil) {
t.Fatalf("wantPB = %v != (got != nil): %v", tt.wantPB, got != nil)
}
if tt.wantPB {
gotBuf := got.ToBuffer()
if !bytes.Equal(tt.input, gotBuf.Flatten()) {
t.Fatal("output packet unequal to input")
}
}
})
}
}

View File

@@ -5,7 +5,6 @@
package netstack
import (
"bytes"
"context"
"errors"
"expvar"
@@ -21,12 +20,10 @@ import (
"sync/atomic"
"time"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -176,7 +173,7 @@ type Impl struct {
ProcessSubnets bool
ipstack *stack.Stack
linkEP *channel.Endpoint
linkEP *linkEndpoint
tundev *tstun.Wrapper
e wgengine.Engine
pm *proxymap.Mapper
@@ -245,6 +242,44 @@ const nicID = 1
// have a UDP packet as big as the MTU.
const maxUDPPacketSize = tstun.MaxPacketSize
func setTCPBufSizes(ipstack *stack.Stack) error {
// tcpip.TCP{Receive,Send}BufferSizeRangeOption is gVisor's version of
// Linux's tcp_{r,w}mem. Application within gVisor differs as some Linux
// features are not (yet) implemented, and socket buffer memory is not
// controlled within gVisor, e.g. we allocate *stack.PacketBuffer's for the
// write path within Tailscale. Therefore, we loosen our understanding of
// the relationship between these Linux and gVisor tunables. The chosen
// values are biased towards higher throughput on high bandwidth-delay
// product paths, except on memory-constrained platforms.
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
// Min is unused by gVisor at the time of writing, but partially plumbed
// for application by the TCP_WINDOW_CLAMP socket option.
Min: tcpRXBufMinSize,
// Default is used by gVisor at socket creation.
Default: tcpRXBufDefSize,
// Max is used by gVisor to cap the advertised receive window post-read.
// (tcp_moderate_rcvbuf=true, the default).
Max: tcpRXBufMaxSize,
}
tcpipErr := ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt)
if tcpipErr != nil {
return fmt.Errorf("could not set TCP RX buf size: %v", tcpipErr)
}
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
// Min in unused by gVisor at the time of writing.
Min: tcpTXBufMinSize,
// Default is used by gVisor at socket creation.
Default: tcpTXBufDefSize,
// Max is used by gVisor to cap the send window.
Max: tcpTXBufMaxSize,
}
tcpipErr = ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt)
if tcpipErr != nil {
return fmt.Errorf("could not set TCP TX buf size: %v", tcpipErr)
}
return nil
}
// Create creates and populates a new Impl.
func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, dialer *tsdial.Dialer, dns *dns.Manager, pm *proxymap.Mapper, driveForLocal drive.FileSystemForLocal) (*Impl, error) {
if mc == nil {
@@ -285,7 +320,18 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
return nil, fmt.Errorf("could not disable TCP RACK: %v", tcpipErr)
}
}
linkEP := channel.New(512, uint32(tstun.DefaultTUNMTU()), "")
err := setTCPBufSizes(ipstack)
if err != nil {
return nil, err
}
var linkEP *linkEndpoint
if runtime.GOOS == "linux" {
// TODO(jwhited): add Windows support https://github.com/tailscale/corp/issues/21874
linkEP = newLinkEndpoint(512, uint32(tstun.DefaultTUNMTU()), "", enableGRO)
linkEP.SupportedGSOKind = stack.HostGSOSupported
} else {
linkEP = newLinkEndpoint(512, uint32(tstun.DefaultTUNMTU()), "", disableGRO)
}
if tcpipProblem := ipstack.CreateNIC(nicID, linkEP); tcpipProblem != nil {
return nil, fmt.Errorf("could not create netstack NIC: %v", tcpipProblem)
}
@@ -333,6 +379,7 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
ns.tundev.PostFilterPacketInboundFromWireGuard = ns.injectInbound
ns.tundev.EndPacketVectorInboundFromWireGuardFlush = linkEP.flushGRO
ns.tundev.PreFilterPacketOutboundToWireGuardNetstackIntercept = ns.handleLocalPackets
stacksForMetrics.Store(ns, struct{}{})
return ns, nil
@@ -509,9 +556,7 @@ func (ns *Impl) Start(lb *ipnlocal.LocalBackend) error {
panic("nil LocalBackend")
}
ns.lb = lb
// size = 0 means use default buffer size
const tcpReceiveBufferSize = 0
tcpFwd := tcp.NewForwarder(ns.ipstack, tcpReceiveBufferSize, maxInFlightConnectionAttempts(), ns.acceptTCP)
tcpFwd := tcp.NewForwarder(ns.ipstack, tcpRXBufDefSize, maxInFlightConnectionAttempts(), ns.acceptTCP)
udpFwd := udp.NewForwarder(ns.ipstack, ns.acceptUDP)
ns.ipstack.SetTransportProtocolHandler(tcp.ProtocolNumber, ns.wrapTCPProtocolHandler(tcpFwd.HandlePacket))
ns.ipstack.SetTransportProtocolHandler(udp.ProtocolNumber, ns.wrapUDPProtocolHandler(udpFwd.HandlePacket))
@@ -734,23 +779,11 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re
// care about the packet; resume processing.
return filter.Accept
}
var pn tcpip.NetworkProtocolNumber
switch p.IPVersion {
case 4:
pn = header.IPv4ProtocolNumber
case 6:
pn = header.IPv6ProtocolNumber
}
if debugPackets {
ns.logf("[v2] service packet in (from %v): % x", p.Src, p.Buffer())
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(p.Buffer())),
})
ns.linkEP.InjectInbound(pn, packetBuf)
packetBuf.DecRef()
ns.linkEP.injectInbound(p)
return filter.DropSilently
}
@@ -791,7 +824,7 @@ func (ns *Impl) DialContextUDP(ctx context.Context, ipp netip.AddrPort) (*gonet.
func (ns *Impl) inject() {
for {
pkt := ns.linkEP.ReadContext(ns.ctx)
if pkt.IsNil() {
if pkt == nil {
if ns.ctx.Err() != nil {
// Return without logging.
return
@@ -1035,21 +1068,10 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
return filter.DropSilently
}
var pn tcpip.NetworkProtocolNumber
switch p.IPVersion {
case 4:
pn = header.IPv4ProtocolNumber
case 6:
pn = header.IPv6ProtocolNumber
}
if debugPackets {
ns.logf("[v2] packet in (from %v): % x", p.Src, p.Buffer())
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(p.Buffer())),
})
ns.linkEP.InjectInbound(pn, packetBuf)
packetBuf.DecRef()
ns.linkEP.enqueueGRO(p)
// We've now delivered this to netstack, so we're done.
// Instead of returning a filter.Accept here (which would also

View File

@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package netstack
import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
)
const (
tcpRXBufMinSize = tcp.MinBufferSize
tcpRXBufDefSize = tcp.DefaultSendBufferSize
tcpRXBufMaxSize = 8 << 20 // 8MiB
tcpTXBufMinSize = tcp.MinBufferSize
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
tcpTXBufMaxSize = 6 << 20 // 6MiB
)

View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package netstack
import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
)
const (
// tcp{RX,TX}Buf{Min,Def,Max}Size mirror gVisor defaults. We leave these
// unchanged on iOS for now as to not increase pressure towards the
// NetworkExtension memory limit.
// TODO(jwhited): test memory/throughput impact of collapsing to values in _default.go
tcpRXBufMinSize = tcp.MinBufferSize
tcpRXBufDefSize = tcp.DefaultSendBufferSize
tcpRXBufMaxSize = tcp.MaxBufferSize
tcpTXBufMinSize = tcp.MinBufferSize
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
tcpTXBufMaxSize = tcp.MaxBufferSize
)

View File

@@ -374,7 +374,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
e.logf("onPortUpdate(port=%v, network=%s)", port, network)
if err := e.router.UpdateMagicsockPort(port, network); err != nil {
e.logf("UpdateMagicsockPort(port=%v, network=%s) failed: %w", port, network, err)
e.logf("UpdateMagicsockPort(port=%v, network=%s) failed: %v", port, network, err)
}
}
magicsockOpts := magicsock.Options{

View File

@@ -692,3 +692,5 @@ azules
tabby
ussuri
kitty
tanuki
neko