Compare commits

..

13 Commits

Author SHA1 Message Date
Brad Fitzpatrick
2c23f1aedc derp: add a unique.Make-vs-local map benchmark
goos: darwin
    goarch: arm64
    pkg: tailscale.com/derp
    cpu: Apple M1
    BenchmarkUnique-8       139699720               10.59 ns/op
    BenchmarkUnique-8       138409840                8.619 ns/op
    BenchmarkUnique-8       134697708                8.521 ns/op
    BenchmarkUnique-8       136568799                8.653 ns/op
    BenchmarkUnique-8       134478981                8.647 ns/op
    BenchmarkLocalMap-8     675015452                1.643 ns/op
    BenchmarkLocalMap-8     717245598                1.648 ns/op
    BenchmarkLocalMap-8     697626253                1.657 ns/op
    BenchmarkLocalMap-8     729024962                1.670 ns/op
    BenchmarkLocalMap-8     712870580                1.668 ns/op
    PASS
    ok      tailscale.com/derp      19.038s

Updates tailscale/corp#24485

Change-Id: Ie8008b07c8c4625cf2b83e38eff169e2248b2d05
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-09 14:51:21 -08:00
M. J. Fromberger
6ff85846bc safeweb: add a Shutdown method to the Server type (#14048)
Updates #14047

Change-Id: I2d20454c715b11ad9c6aad1d81445e05a170c3a2
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2024-11-08 10:02:16 -08:00
Anton Tolchanov
64d70fb718 ipn/ipnlocal: log a summary of posture identity response
Perhaps I was too opimistic in #13323 thinking we won't need logs for
this. Let's log a summary of the response without logging specific
identifiers.

Updates tailscale/corp#24437

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-11-08 16:20:07 +00:00
Brad Fitzpatrick
020cacbe70 derp/derphttp: don't link websockets other than on GOOS=js
Or unless the new "ts_debug_websockets" build tag is set.

Updates #1278

Change-Id: Ic4c4f81c1924250efd025b055585faec37a5491d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 22:29:41 -08:00
Brad Fitzpatrick
c3306bfd15 control/controlhttp/controlhttpserver: split out Accept to its own package
Otherwise all the clients only using control/controlhttp for the
ts2021 HTTP client were also pulling in WebSocket libraries, as the
server side always needs to speak websockets, but only GOOS=js clients
speak it.

This doesn't yet totally remove the websocket dependency on Linux because
Linux has a envknob opt-in to act like GOOS=js for manual testing and force
the use of WebSockets for DERP only (not control). We can put that behind
a build tag in a future change to eliminate the dep on all GOOSes.

Updates #1278

Change-Id: I4f60508f4cad52bf8c8943c8851ecee506b7ebc9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 22:29:41 -08:00
Brad Fitzpatrick
23880eb5b0 cmd/tailscaled: support "ts_omit_ssh" build tag to remove SSH
Some environments would like to remove Tailscale SSH support for the
binary for various reasons when not needed (either for peace of mind,
or the ~1MB of binary space savings).

Updates tailscale/corp#24454
Updates #1278
Updates #12614

Change-Id: Iadd6c5a393992c254b5dc9aa9a526916f96fd07a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 16:06:59 -08:00
Irbe Krumina
2c8859c2e7 client/tailscale,ipn/{ipnlocal,localapi}: add a pre-shutdown localAPI endpoint that terminates control connections. (#14028)
Adds a /disconnect-control local API endpoint that just shuts down control client.
This can be run before shutting down an HA subnet router/app connector replica - it will ensure
that all connection to control are dropped and control thus considers this node inactive and tells
peers to switch over to another replica. Meanwhile the existing connections keep working (assuming
that the replica is given some graceful shutdown period).

Updates tailscale/tailscale#14020

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-07 19:27:53 +00:00
Brad Fitzpatrick
3090461961 tsweb/varz: optimize some allocs, add helper func for others
Updates #cleanup
Updates tailscale/corp#23546 (noticed when doing this)

Change-Id: Ia9f627fe32bb4955739b2787210ba18f5de27f4d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 08:09:16 -08:00
Irbe Krumina
8ba9b558d2 envknob,kube/kubetypes,cmd/k8s-operator: add app type for ProxyGroup (#14029)
Sets a custom hostinfo app type for ProxyGroup replicas, similarly
to how we do it for all other Kubernetes Operator managed components.

Updates tailscale/tailscale#13406,tailscale/corp#22920

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-07 12:42:29 +00:00
Percy Wegmann
8dcbd988f7 cmd/derper: show more information on home page
- Basic description of DERP

If configured to do so, also show

- Mailto link to security@tailscale.com
- Link to Tailscale Security Policies
- Link to Tailscale Acceptable Use Policy

Updates tailscale/corp#24092

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-11-06 11:06:08 -06:00
License Updater
065825e94c licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-11-05 15:33:17 -08:00
Brad Fitzpatrick
01185e436f types/result, util/lineiter: add package for a result type, use it
This adds a new generic result type (motivated by golang/go#70084) to
try it out, and uses it in the new lineutil package (replacing the old
lineread package), changing that package to return iterators:
sometimes over []byte (when the input is all in memory), but sometimes
iterators over results of []byte, if errors might happen at runtime.

Updates #12912
Updates golang/go#70084

Change-Id: Iacdc1070e661b5fb163907b1e8b07ac7d51d3f83
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-05 10:27:52 -08:00
Irbe Krumina
809a6eba80 cmd/k8s-operator: allow to optionally configure tailscaled port (#14005)
Updates tailscale/tailscale#13981

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-04 18:42:51 +00:00
61 changed files with 659 additions and 1223 deletions

View File

@@ -1327,6 +1327,17 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
return nil
}
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
// to another replica whilst there is still some grace period for the existing connections to terminate.
func (lc *LocalClient) DisconnectControl(ctx context.Context) error {
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
if err != nil {
return fmt.Errorf("error disconnecting control: %w", err)
}
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {

View File

@@ -116,7 +116,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/net/wsconn from tailscale.com/cmd/derper
tailscale.com/paths from tailscale.com/client/tailscale
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
@@ -140,6 +140,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/ipn
tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/ipn+
@@ -154,7 +155,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/fastuuid from tailscale.com/tsweb
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health+
@@ -263,6 +264,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
html/template from tailscale.com/cmd/derper
io from bufio+
io/fs from crypto/x509+
io/ioutil from github.com/mitchellh/go-ps+
@@ -307,6 +309,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from crypto/x509+

View File

@@ -19,6 +19,7 @@ import (
"expvar"
"flag"
"fmt"
"html/template"
"io"
"log"
"math"
@@ -212,25 +213,16 @@ func main() {
tsweb.AddBrowserHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
io.WriteString(w, `<html><body>
<h1>DERP</h1>
<p>
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
</p>
<p>
Documentation:
</p>
<ul>
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
</ul>
`)
if !*runDERP {
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
}
if tsweb.AllowDebugAccess(r) {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
err := homePageTemplate.Execute(w, templateData{
ShowAbuseInfo: validProdHostname.MatchString(*hostname),
Disabled: !*runDERP,
AllowDebug: tsweb.AllowDebugAccess(r),
})
if err != nil {
if r.Context().Err() == nil {
log.Printf("homePageTemplate.Execute: %v", err)
}
return
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -468,3 +460,52 @@ func init() {
return 0
}))
}
type templateData struct {
ShowAbuseInfo bool
Disabled bool
AllowDebug bool
}
// homePageTemplate renders the home page using [templateData].
var homePageTemplate = template.Must(template.New("home").Parse(`<html><body>
<h1>DERP</h1>
<p>
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
</p>
<p>
It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic
for Tailscale clients.
</p>
{{if .ShowAbuseInfo }}
<p>
If you suspect abuse, please contact <a href="mailto:security@tailscale.com">security@tailscale.com</a>.
</p>
{{end}}
<p>
Documentation:
</p>
<ul>
{{if .ShowAbuseInfo }}
<li><a href="https://tailscale.com/security-policies">Tailscale Security Policies</a></li>
<li><a href="https://tailscale.com/tailscale-aup">Tailscale Acceptable Use Policies</a></li>
{{end}}
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
</ul>
{{if .Disabled}}
<p>Status: <b>disabled</b></p>
{{end}}
{{if .AllowDebug}}
<p>Debug info at <a href='/debug/'>/debug/</a>.</p>
{{end}}
</body>
</html>
`))

View File

@@ -4,7 +4,9 @@
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -110,3 +112,30 @@ func TestDeps(t *testing.T) {
},
}.Check(t)
}
func TestTemplate(t *testing.T) {
buf := &bytes.Buffer{}
err := homePageTemplate.Execute(buf, templateData{
ShowAbuseInfo: true,
Disabled: true,
AllowDebug: true,
})
if err != nil {
t.Fatal(err)
}
str := buf.String()
if !strings.Contains(str, "If you suspect abuse") {
t.Error("Output is missing abuse mailto")
}
if !strings.Contains(str, "Tailscale Security Policies") {
t.Error("Output is missing Tailscale Security Policies link")
}
if !strings.Contains(str, "Status:") {
t.Error("Output is missing disabled status")
}
if !strings.Contains(str, "Debug info") {
t.Error("Output is missing debug info")
}
fmt.Println(buf.String())
}

View File

@@ -80,10 +80,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -658,6 +654,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
@@ -740,7 +737,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -775,6 +771,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/appc+
@@ -792,7 +789,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/appc+
tailscale.com/util/multierr from tailscale.com/control/controlclient+

View File

@@ -11,6 +11,7 @@ import (
"context"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -150,6 +151,13 @@ func initTSNet(zlog *zap.SugaredLogger) (*tsnet.Server, *tailscale.Client) {
Hostname: hostname,
Logf: zlog.Named("tailscaled").Debugf,
}
if p := os.Getenv("TS_PORT"); p != "" {
port, err := strconv.ParseUint(p, 10, 16)
if err != nil {
startlog.Fatalf("TS_PORT %q cannot be parsed as uint16: %v", p, err)
}
s.Port = uint16(port)
}
if kubeSecret != "" {
st, err := kubestore.New(logger.Discard, kubeSecret)
if err != nil {

View File

@@ -47,7 +47,7 @@ const (
reasonProxyGroupInvalid = "ProxyGroupInvalid"
)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupCount)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupEgressCount)
// ProxyGroupReconciler ensures cluster resources for a ProxyGroup definition.
type ProxyGroupReconciler struct {

View File

@@ -15,6 +15,7 @@ import (
"sigs.k8s.io/yaml"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/ptr"
)
@@ -146,6 +147,10 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode, cfgHa
Name: "TS_USERSPACE",
Value: "false",
},
{
Name: "TS_INTERNAL_APP",
Value: kubetypes.AppProxyGroupEgress,
},
}
if tsFirewallMode != "" {

View File

@@ -1,39 +0,0 @@
# Tailscale LOPOWER
"Little Opinionated Proxy Over Wireguard-encrypted Routes"
**STATUS**: in-development alpha (as of 2024-11-03)
## Background
Some small devices such as ESP32 microcontrollers [support WireGuard](https://github.com/ciniml/WireGuard-ESP32-Arduino) but are too small to run Tailscale.
Tailscale LOPOWER is a proxy that you run nearby that bridges a low-power WireGuard-speaking device on one side to Tailscale on the other side. That way network traffic from the low-powered device never hits the network unencrypted but is still able to communicate to/from other Tailscale devices on your Tailnet.
## Diagram
<img src="./lopower.svg">
## Features
* Runs separate Wireguard server with separate keys (unknown to the Tailscale control plane) that proxy on to Tailscale
* Outputs WireGuard-standard configuration to enrolls devices, including in QR code form.
* embeds `tsnet`, with an identity on which the device(s) behind the proxy appear on your Tailnet
* optional IPv4 support. IPv6 is always enabled, as it never conflicts with anything. But IPv4 (or CGNAT) might already be in use on your client's network.
* includes a DNS server (at `fd7a:115c:a1e0:9909::1` by default and optionally also at `10.90.0.1`) to serve both MagicDNS names as well as forwarding non-Tailscale DNS names onwards
* if IPv4 is disabled, MagicDNS `A` records are filtered out, and only `AAAA` records are served.
## Limitations
* this runs in userspace using gVisor's netstack. That means it's portable (and doesn't require kernel/system configuration), but that does mean it doesn't operate at a packet level but rather it stitches together two separate TCP (or UDP) flows and doesn't support IP protocols such as SCTP or other things that aren't TCP or UDP.
* the standard WireGuard configuration doesn't support specifying DNS search domains, so resolving bare names like the `go` in `http://go/foo` won't work and you need to resolve names using the fully qualified `go.your-tailnet.ts.net` names.
* since it's based on userspace tsnet mode, it doesn't pick up your system DNS configuration (yet?) and instead resolves non-tailnet DNS names using either your "Override DNS" tailnet settings for the global DNS resolver, or else defaults to `8.8.8.8` and `1.1.1.1` (using DoH) if that isn't set.
## TODO
* provisioning more than one low-powered device is possible, but requires manual config file edits. It should be possible to enroll multiple devices (including QR code support) easily.
* incoming connections (from Tailscale to `lopower`) don't yet forward to the low-powered devices. When there's only one low-powered device, the mapping policy is obvious. When there are multiple, it's not as obvious. Maybe the answer is supporting [4via6 subnet routers](https://tailscale.com/kb/1201/4via6-subnets).
## Installing
* git clone this repo, switch to `lp` branch, `go install ./cmd/lopower` and see `lopower --help`.

View File

@@ -1,872 +0,0 @@
// The lopower server is a "Little Opinionated Proxy Over
// Wireguard-Encrypted Route". It bridges a static WireGuard
// client into a Tailscale network.
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"os"
"os/signal"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
qrcode "github.com/skip2/go-qrcode"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"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"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tsnet"
"tailscale.com/types/dnstype"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/must"
"tailscale.com/wgengine/wgcfg"
)
var (
wgListenPort = flag.Int("wg-port", 51820, "port number to listen on for WireGuard from the client")
confDir = flag.String("dir", filepath.Join(os.Getenv("HOME"), ".config/lopower"), "directory to store configuration in")
wgPubHost = flag.String("wg-host", "0.0.0.1", "IP address of lopower's WireGuard server that's accessible from the client")
qrListenAddr = flag.String("qr-listen", "127.0.0.1:8014", "HTTP address to serve a QR code for client's WireGuard configuration, or empty for none")
printConfig = flag.Bool("print-config", true, "print the client's WireGuard configuration to stdout on startup")
includeV4 = flag.Bool("include-v4", true, "include IPv4 (CGNAT) in the WireGuard configuration; incompatible with some carriers. IPv6 is always included.")
verbosePackets = flag.Bool("verbose-packets", false, "log packet contents")
)
type config struct {
PrivKey key.NodePrivate // the proxy server's key
Peers []Peer
// V4 and V6 are the local IPs.
V4 netip.Addr
V6 netip.Addr
// CIDRs are used to allocate IPs to peers.
V4CIDR netip.Prefix
V6CIDR netip.Prefix
}
// IsLocalIP reports whether ip is one of the local IPs.
func (c *config) IsLocalIP(ip netip.Addr) bool {
return ip.IsValid() && (ip == c.V4 || ip == c.V6)
}
type Peer struct {
PrivKey key.NodePrivate // e.g. proxy client's
V4 netip.Addr
V6 netip.Addr
}
func (lp *lpServer) storeConfigLocked() {
path := filepath.Join(lp.dir, "config.json")
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
log.Fatalf("os.MkdirAll(%q): %v", filepath.Dir(path), err)
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("os.OpenFile(%q): %v", path, err)
}
defer f.Close()
must.Do(json.NewEncoder(f).Encode(lp.c))
if err := f.Close(); err != nil {
log.Fatalf("f.Close: %v", err)
}
}
func (lp *lpServer) loadConfig() {
path := filepath.Join(lp.dir, "config.json")
f, err := os.Open(path)
if err == nil {
defer f.Close()
var cfg *config
must.Do(json.NewDecoder(f).Decode(&cfg))
if len(cfg.Peers) > 0 { // as early version didn't set this
lp.mu.Lock()
defer lp.mu.Unlock()
lp.c = cfg
}
return
}
if !os.IsNotExist(err) {
log.Fatalf("os.OpenFile(%q): %v", path, err)
}
const defaultV4CIDR = "10.90.0.0/24"
const defaultV6CIDR = "fd7a:115c:a1e0:9909::/64" // 9909 = above QWERTY "LOPO"(wer)
c := &config{
PrivKey: key.NewNode(),
V4CIDR: netip.MustParsePrefix(defaultV4CIDR),
V6CIDR: netip.MustParsePrefix(defaultV6CIDR),
}
c.V4 = c.V4CIDR.Addr().Next()
c.V6 = c.V6CIDR.Addr().Next()
c.Peers = append(c.Peers, Peer{
PrivKey: key.NewNode(),
V4: c.V4.Next(),
V6: c.V6.Next(),
})
lp.mu.Lock()
defer lp.mu.Unlock()
lp.c = c
lp.storeConfigLocked()
return
}
func (lp *lpServer) reconfig() {
lp.mu.Lock()
wc := &wgcfg.Config{
Name: "lopower0",
PrivateKey: lp.c.PrivKey,
ListenPort: uint16(*wgListenPort),
Addresses: []netip.Prefix{
netip.PrefixFrom(lp.c.V4, 32),
netip.PrefixFrom(lp.c.V6, 128),
},
}
for _, p := range lp.c.Peers {
wc.Peers = append(wc.Peers, wgcfg.Peer{
PublicKey: p.PrivKey.Public(),
AllowedIPs: []netip.Prefix{
netip.PrefixFrom(p.V4, 32),
netip.PrefixFrom(p.V6, 128),
},
})
}
lp.mu.Unlock()
must.Do(wgcfg.ReconfigDevice(lp.d, wc, log.Printf))
}
func newLP(ctx context.Context) *lpServer {
logf := log.Printf
deviceLogger := &device.Logger{
Verbosef: logger.Discard,
Errorf: logf,
}
lp := &lpServer{
ctx: ctx,
dir: *confDir,
readCh: make(chan *stack.PacketBuffer, 16),
}
lp.loadConfig()
lp.initNetstack(ctx)
nst := &nsTUN{
lp: lp,
closeCh: make(chan struct{}),
evChan: make(chan tun.Event),
}
wgdev := wgcfg.NewDevice(nst, conn.NewDefaultBind(), deviceLogger)
lp.d = wgdev
must.Do(wgdev.Up())
lp.reconfig()
if *printConfig {
log.Printf("Device Wireguard config is:\n%s", lp.wgConfigForQR())
}
lp.startTSNet(ctx)
return lp
}
type lpServer struct {
dir string
tsnet *tsnet.Server
d *device.Device
ns *stack.Stack
ctx context.Context // canceled on shutdown
linkEP *channel.Endpoint
readCh chan *stack.PacketBuffer // from gvisor/dns server => out to network
// protocolConns tracks the number of active connections for each connection.
// It is used to add and remove protocol addresses from netstack as needed.
protocolConns syncs.Map[tcpip.ProtocolAddress, *atomic.Int32]
mu sync.Mutex // protects following
c *config
}
// MaxPacketSize is the maximum size (in bytes)
// of a packet that can be injected into lpServer.
const MaxPacketSize = device.MaxContentSize
const nicID = 1
func (lp *lpServer) initNetstack(ctx context.Context) error {
ns := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
ipv6.NewProtocol,
},
TransportProtocols: []stack.TransportProtocolFactory{
tcp.NewProtocol,
icmp.NewProtocol4,
udp.NewProtocol,
},
})
lp.ns = ns
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
if tcpipErr := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt); tcpipErr != nil {
return fmt.Errorf("SetTransportProtocolOption SACK: %v", tcpipErr)
}
lp.linkEP = channel.New(512, 1280, "")
if tcpipProblem := ns.CreateNIC(nicID, lp.linkEP); tcpipProblem != nil {
return fmt.Errorf("CreateNIC: %v", tcpipProblem)
}
ns.SetPromiscuousMode(nicID, true)
lp.mu.Lock()
v4, v6 := lp.c.V4, lp.c.V6
lp.mu.Unlock()
prefix := tcpip.AddrFrom4Slice(v4.AsSlice()).WithPrefix()
if *includeV4 {
if tcpProb := ns.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: prefix,
}, stack.AddressProperties{}); tcpProb != nil {
return errors.New(tcpProb.String())
}
}
prefix = tcpip.AddrFrom16Slice(v6.AsSlice()).WithPrefix()
if tcpProb := ns.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: prefix,
}, stack.AddressProperties{}); tcpProb != nil {
return errors.New(tcpProb.String())
}
ipv4Subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(make([]byte, 4)), tcpip.MaskFromBytes(make([]byte, 4)))
if err != nil {
return fmt.Errorf("could not create IPv4 subnet: %v", err)
}
ipv6Subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(make([]byte, 16)), tcpip.MaskFromBytes(make([]byte, 16)))
if err != nil {
return fmt.Errorf("could not create IPv6 subnet: %v", err)
}
routes := []tcpip.Route{{
Destination: ipv4Subnet,
NIC: nicID,
}, {
Destination: ipv6Subnet,
NIC: nicID,
}}
if !*includeV4 {
routes = routes[1:]
}
ns.SetRouteTable(routes)
const tcpReceiveBufferSize = 0 // default
const maxInFlightConnectionAttempts = 8192
tcpFwd := tcp.NewForwarder(ns, tcpReceiveBufferSize, maxInFlightConnectionAttempts, lp.acceptTCP)
udpFwd := udp.NewForwarder(ns, lp.acceptUDP)
ns.SetTransportProtocolHandler(tcp.ProtocolNumber, func(tei stack.TransportEndpointID, pb *stack.PacketBuffer) (handled bool) {
return tcpFwd.HandlePacket(tei, pb)
})
ns.SetTransportProtocolHandler(udp.ProtocolNumber, func(tei stack.TransportEndpointID, pb *stack.PacketBuffer) (handled bool) {
return udpFwd.HandlePacket(tei, pb)
})
go func() {
for {
pkt := lp.linkEP.ReadContext(ctx)
if pkt == nil {
if ctx.Err() != nil {
// Return without logging.
log.Printf("linkEP.ReadContext: %v", ctx.Err())
return
}
continue
}
size := pkt.Size()
if size > MaxPacketSize || size == 0 {
pkt.DecRef()
continue
}
select {
case lp.readCh <- pkt:
case <-ctx.Done():
}
}
}()
return nil
}
func netaddrIPFromNetstackIP(s tcpip.Address) netip.Addr {
switch s.Len() {
case 4:
return netip.AddrFrom4(s.As4())
case 16:
return netip.AddrFrom16(s.As16()).Unmap()
}
return netip.Addr{}
}
func (lp *lpServer) trackProtocolAddr(destIP netip.Addr) (untrack func()) {
pa := tcpip.ProtocolAddress{
AddressWithPrefix: tcpip.AddrFromSlice(destIP.AsSlice()).WithPrefix(),
}
if destIP.Is4() {
pa.Protocol = ipv4.ProtocolNumber
} else if destIP.Is6() {
pa.Protocol = ipv6.ProtocolNumber
}
addrConns, _ := lp.protocolConns.LoadOrInit(pa, func() *atomic.Int32 { return new(atomic.Int32) })
if addrConns.Add(1) == 1 {
lp.ns.AddProtocolAddress(nicID, pa, stack.AddressProperties{
PEB: stack.CanBePrimaryEndpoint, // zero value default
ConfigType: stack.AddressConfigStatic, // zero value default
})
}
return func() {
if addrConns.Add(-1) == 0 {
lp.ns.RemoveAddress(nicID, pa.AddressWithPrefix.Address)
}
}
}
func (lp *lpServer) acceptUDP(r *udp.ForwarderRequest) {
log.Printf("acceptUDP: %v", r.ID())
destIP := netaddrIPFromNetstackIP(r.ID().LocalAddress)
untrack := lp.trackProtocolAddr(destIP)
var wq waiter.Queue
ep, udpErr := r.CreateEndpoint(&wq)
if udpErr != nil {
log.Printf("CreateEndpoint: %v", udpErr)
return
}
go func() {
defer untrack()
defer ep.Close()
reqDetails := r.ID()
clientRemoteIP := netaddrIPFromNetstackIP(reqDetails.RemoteAddress)
destPort := reqDetails.LocalPort
if !clientRemoteIP.IsValid() {
log.Printf("acceptUDP: invalid remote IP %v", reqDetails.RemoteAddress)
return
}
randPort := rand.IntN(65536-1024) + 1024
v4, v6 := lp.tsnet.TailscaleIPs()
var listenAddr netip.Addr
if destIP.Is4() {
listenAddr = v4
} else {
listenAddr = v6
}
backendConn, err := lp.tsnet.ListenPacket("udp", fmt.Sprintf("%s:%d", listenAddr, randPort))
if err != nil {
log.Printf("ListenPacket: %v", err)
return
}
defer backendConn.Close()
clientConn := gonet.NewUDPConn(&wq, ep)
defer clientConn.Close()
errCh := make(chan error, 2)
go func() (err error) {
defer func() { errCh <- err }()
var buf [64]byte
for {
n, _, err := backendConn.ReadFrom(buf[:])
if err != nil {
log.Printf("UDP read: %v", err)
return err
}
_, err = clientConn.Write(buf[:n])
if err != nil {
return err
}
}
}()
dstAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", destIP, destPort))
if err != nil {
log.Printf("ResolveUDPAddr: %v", err)
return
}
go func() (err error) {
defer func() { errCh <- err }()
var buf [2048]byte
for {
n, err := clientConn.Read(buf[:])
if err != nil {
log.Printf("UDP read: %v", err)
return err
}
_, err = backendConn.WriteTo(buf[:n], dstAddr)
if err != nil {
return err
}
}
}()
err = <-errCh
if err != nil {
log.Printf("io.Copy: %v", err)
}
}()
}
func (lp *lpServer) acceptTCP(r *tcp.ForwarderRequest) {
log.Printf("acceptTCP: %v", r.ID())
reqDetails := r.ID()
destIP := netaddrIPFromNetstackIP(reqDetails.LocalAddress)
clientRemoteIP := netaddrIPFromNetstackIP(reqDetails.RemoteAddress)
destPort := reqDetails.LocalPort
if !clientRemoteIP.IsValid() {
log.Printf("acceptTCP: invalid remote IP %v", reqDetails.RemoteAddress)
r.Complete(true) // sends a RST
return
}
untrack := lp.trackProtocolAddr(destIP)
defer untrack()
var wq waiter.Queue
ep, tcpErr := r.CreateEndpoint(&wq)
if tcpErr != nil {
log.Printf("CreateEndpoint: %v", tcpErr)
r.Complete(true)
return
}
defer ep.Close()
ep.SocketOptions().SetKeepAlive(true)
if destPort == 53 && lp.c.IsLocalIP(destIP) {
tc := gonet.NewTCPConn(&wq, ep)
defer tc.Close()
r.Complete(false) // accept TCP connection
lp.handleTCPDNSQuery(tc, netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort))
return
}
dialCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
c, err := lp.tsnet.Dial(dialCtx, "tcp", fmt.Sprintf("%s:%d", destIP, destPort))
cancel()
if err != nil {
log.Printf("Dial(%s:%d): %v", destIP, destPort, err)
r.Complete(true) // sends a RST
return
}
defer c.Close()
tc := gonet.NewTCPConn(&wq, ep)
defer tc.Close()
r.Complete(false) // accept TCP connection
errc := make(chan error, 2)
go func() { _, err := io.Copy(tc, c); errc <- err }()
go func() { _, err := io.Copy(c, tc); errc <- err }()
err = <-errc
if err != nil {
log.Printf("io.Copy: %v", err)
}
}
func (lp *lpServer) wgConfigForQR() string {
var b strings.Builder
p := lp.c.Peers[0]
privHex, _ := p.PrivKey.MarshalText()
privHex = bytes.TrimPrefix(privHex, []byte("privkey:"))
priv := make([]byte, 32)
got, err := hex.Decode(priv, privHex)
if err != nil || got != 32 {
log.Printf("marshal text was: %q", privHex)
log.Fatalf("bad private key: %v, % bytes", err, got)
}
privb64 := base64.StdEncoding.EncodeToString(priv)
fmt.Fprintf(&b, "[Interface]\nPrivateKey = %s\n", privb64)
fmt.Fprintf(&b, "Address = %v,%v\n", p.V6, p.V4)
pubBin, _ := lp.c.PrivKey.Public().MarshalBinary()
if len(pubBin) != 34 {
log.Fatalf("bad pubkey length: %d", len(pubBin))
}
pubBin = pubBin[2:] // trim off "np"
pubb64 := base64.StdEncoding.EncodeToString(pubBin)
fmt.Fprintf(&b, "\n[Peer]\nPublicKey = %v\n", pubb64)
if *includeV4 {
fmt.Fprintf(&b, "AllowedIPs = %v/32,%v/128,%v,%v\n", lp.c.V4, lp.c.V6, tsaddr.TailscaleULARange(), tsaddr.CGNATRange())
} else {
fmt.Fprintf(&b, "AllowedIPs = %v/128,%v\n", lp.c.V6, tsaddr.TailscaleULARange())
}
fmt.Fprintf(&b, "Endpoint = %v\n", net.JoinHostPort(*wgPubHost, fmt.Sprint(*wgListenPort)))
return b.String()
}
func (lp *lpServer) serveQR() {
ln, err := net.Listen("tcp", *qrListenAddr)
if err != nil {
log.Fatalf("qr: %v", err)
}
log.Printf("# Serving QR code at http://%s/", ln.Addr())
hs := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/png")
conf := lp.wgConfigForQR()
v, err := qrcode.Encode(conf, qrcode.Medium, 512)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(v)
}),
}
if err := hs.Serve(ln); err != nil {
log.Fatalf("qr: %v", err)
}
}
type nsTUN struct {
lp *lpServer
closeCh chan struct{}
evChan chan tun.Event
}
func (t *nsTUN) File() *os.File {
panic("nsTUN.File() called, which makes no sense")
}
func (t *nsTUN) Close() error {
close(t.closeCh)
close(t.evChan)
return nil
}
// Read reads packets from gvisor (or the DNS server) to send out to the network.
func (t *nsTUN) Read(out [][]byte, sizes []int, offset int) (int, error) {
select {
case <-t.closeCh:
return 0, io.EOF
case resPacket := <-t.lp.readCh:
defer resPacket.DecRef()
pkt := out[0][offset:]
n := copy(pkt, resPacket.NetworkHeader().Slice())
n += copy(pkt[n:], resPacket.TransportHeader().Slice())
n += copy(pkt[n:], resPacket.Data().AsRange().ToSlice())
if *verbosePackets {
log.Printf("[v] nsTUN.Read (out): % 02x", pkt[:n])
}
sizes[0] = n
return 1, nil
}
}
// Write accepts incoming packets. The packets begin at buffs[:][offset:],
// like wireguard-go/tun.Device.Write. Write is called per-peer via
// wireguard-go/device.Peer.RoutineSequentialReceiver, so it MUST be
// thread-safe.
func (t *nsTUN) Write(buffs [][]byte, offset int) (int, error) {
var pkt packet.Parsed
for _, buff := range buffs {
raw := buff[offset:]
pkt.Decode(raw)
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(slices.Clone(raw)),
})
if *verbosePackets {
log.Printf("[v] nsTUN.Write (in): % 02x", raw)
}
if pkt.IPProto == ipproto.UDP && pkt.Dst.Port() == 53 && t.lp.c.IsLocalIP(pkt.Dst.Addr()) {
// Handle DNS queries before sending to gvisor.
t.lp.handleDNSUDPQuery(raw)
continue
}
if pkt.IPVersion == 4 {
t.lp.linkEP.InjectInbound(ipv4.ProtocolNumber, packetBuf)
} else if pkt.IPVersion == 6 {
t.lp.linkEP.InjectInbound(ipv6.ProtocolNumber, packetBuf)
}
}
return len(buffs), nil
}
func (t *nsTUN) Flush() error { return nil }
func (t *nsTUN) MTU() (int, error) { return 1500, nil }
func (t *nsTUN) Name() (string, error) { return "nstun", nil }
func (t *nsTUN) Events() <-chan tun.Event { return t.evChan }
func (t *nsTUN) BatchSize() int { return 1 }
func (lp *lpServer) startTSNet(ctx context.Context) {
hostname, err := os.Hostname()
if err != nil {
log.Fatal(err)
}
ts := &tsnet.Server{
Dir: filepath.Join(lp.dir, "tsnet"),
Hostname: hostname,
UserLogf: log.Printf,
Ephemeral: false,
}
lp.tsnet = ts
ts.PreStart = func() error {
dnsMgr := ts.Sys().DNSManager.Get()
dnsMgr.SetForceAAAA(true)
// Force fallback resolvers to Google and Cloudflare as an ultimate
// fallback in case the Tailnet DNS servers are not set/forced. Normally
// tailscaled would resort to using the OS DNS resolvers, but
// tsnet/userspace binaries don't do that (yet?), so this is the
// "Opionated" part of the "LOPOWER" name. The opinion is just using
// big providers known to work. (Normally stock tailscaled never
// makes such opinions and never defaults to any big provider, unless
// you're already running on that big provider's network so have
// already indicated you're fine with them.))
dnsMgr.SetForceFallbackResolvers([]*dnstype.Resolver{
{Addr: "8.8.8.8"},
{Addr: "1.1.1.1"},
})
return nil
}
if _, err := ts.Up(ctx); err != nil {
log.Fatal(err)
}
}
// filteredDNSQuery wraps the MagicDNS server response but filters out A record responses
// for *.ts.net if IPv4 is not enabled. This is so the e.g. a phone on a CGNAT-using
// network doesn't prefer the "A" record over AAAA when dialing and dial into the
// the carrier's CGNAT range into of the AAAA record into the Tailscale IPv6 ULA range.
func (lp *lpServer) filteredDNSQuery(ctx context.Context, q []byte, family string, from netip.AddrPort) ([]byte, error) {
m, ok := lp.tsnet.Sys().DNSManager.GetOK()
if !ok {
return nil, errors.New("DNSManager not ready")
}
origRes, err := m.Query(ctx, q, family, from)
if err != nil {
return nil, err
}
if *includeV4 {
return origRes, nil
}
// Filter out *.ts.net A records.
var msg dnsmessage.Message
if err := msg.Unpack(origRes); err != nil {
return nil, err
}
newAnswers := msg.Answers[:0]
for _, a := range msg.Answers {
name := a.Header.Name.String()
if a.Header.Type == dnsmessage.TypeA && strings.HasSuffix(name, ".ts.net.") {
// Drop.
continue
}
newAnswers = append(newAnswers, a)
}
if len(newAnswers) == len(msg.Answers) {
// Nothing was filtered. No need to reencode it.
return origRes, nil
}
msg.Answers = newAnswers
return msg.Pack()
}
func (lp *lpServer) handleTCPDNSQuery(c net.Conn, src netip.AddrPort) {
defer c.Close()
var lenBuf [2]byte
for {
c.SetReadDeadline(time.Now().Add(30 * time.Second))
_, err := io.ReadFull(c, lenBuf[:])
if err != nil {
return
}
n := binary.BigEndian.Uint16(lenBuf[:])
buf := make([]byte, n)
c.SetReadDeadline(time.Now().Add(30 * time.Second))
_, err = io.ReadFull(c, buf[:])
if err != nil {
return
}
res, err := lp.filteredDNSQuery(context.Background(), buf, "tcp", src)
if err != nil {
log.Printf("TCP DNS query error: %v", err)
return
}
binary.BigEndian.PutUint16(lenBuf[:], uint16(len(res)))
c.SetWriteDeadline(time.Now().Add(30 * time.Second))
_, err = c.Write(lenBuf[:])
if err != nil {
return
}
c.SetWriteDeadline(time.Now().Add(30 * time.Second))
_, err = c.Write(res)
if err != nil {
return
}
}
}
// caller owns the raw memory.
func (lp *lpServer) handleDNSUDPQuery(raw []byte) {
var pkt packet.Parsed
pkt.Decode(raw)
if pkt.IPProto != ipproto.UDP || pkt.Dst.Port() != 53 || !lp.c.IsLocalIP(pkt.Dst.Addr()) {
panic("caller error")
}
dnsRes, err := lp.filteredDNSQuery(context.Background(), pkt.Payload(), "udp", pkt.Src)
if err != nil {
log.Printf("DNS query error: %v", err)
return
}
ipLayer := mkIPLayer(layers.IPProtocolUDP, pkt.Dst.Addr(), pkt.Src.Addr())
udpLayer := &layers.UDP{
SrcPort: 53,
DstPort: layers.UDPPort(pkt.Src.Port()),
}
resPkt, err := mkPacket(ipLayer, udpLayer, gopacket.Payload(dnsRes))
if err != nil {
log.Printf("mkPacket: %v", err)
return
}
pktBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(resPkt),
})
select {
case lp.readCh <- pktBuf:
case <-lp.ctx.Done():
}
}
type serializableNetworkLayer interface {
gopacket.SerializableLayer
gopacket.NetworkLayer
}
func mkIPLayer(proto layers.IPProtocol, src, dst netip.Addr) serializableNetworkLayer {
if src.Is4() {
return &layers.IPv4{
Protocol: proto,
SrcIP: src.AsSlice(),
DstIP: dst.AsSlice(),
}
}
if src.Is6() {
return &layers.IPv6{
NextHeader: proto,
SrcIP: src.AsSlice(),
DstIP: dst.AsSlice(),
}
}
panic("invalid src IP")
}
// mkPacket is a serializes a number of layers into a packet.
//
// It's a convenience wrapper around gopacket.SerializeLayers
// that does some things automatically:
//
// * layers.IPv4/IPv6 Version is set to 4/6 if not already set
// * layers.IPv4/IPv6 TTL/HopLimit is set to 64 if not already set
// * the TCP/UDP/ICMPv6 checksum is set based on the network layer
//
// The provided layers in ll must be sorted from lowest (e.g. *layers.Ethernet)
// to highest. (Depending on the need, the first layer will be either *layers.Ethernet
// or *layers.IPv4/IPv6).
func mkPacket(ll ...gopacket.SerializableLayer) ([]byte, error) {
var nl gopacket.NetworkLayer
for _, la := range ll {
switch la := la.(type) {
case *layers.IPv4:
nl = la
if la.Version == 0 {
la.Version = 4
}
if la.TTL == 0 {
la.TTL = 64
}
case *layers.IPv6:
nl = la
if la.Version == 0 {
la.Version = 6
}
if la.HopLimit == 0 {
la.HopLimit = 64
}
}
}
for _, la := range ll {
switch la := la.(type) {
case *layers.TCP:
la.SetNetworkLayerForChecksum(nl)
case *layers.UDP:
la.SetNetworkLayerForChecksum(nl)
case *layers.ICMPv6:
la.SetNetworkLayerForChecksum(nl)
}
}
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}
if err := gopacket.SerializeLayers(buf, opts, ll...); err != nil {
return nil, fmt.Errorf("serializing packet: %v", err)
}
return buf.Bytes(), nil
}
func main() {
flag.Parse()
log.Printf("lopower starting")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
lp := newLP(ctx)
if *qrListenAddr != "" {
go lp.serveQR()
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, unix.SIGTERM, os.Interrupt)
<-sigCh
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -67,6 +67,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/types/logger from tailscale.com/tsweb
tailscale.com/types/opt from tailscale.com/envknob+
tailscale.com/types/ptr from tailscale.com/tailcfg+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/tailcfg+
tailscale.com/types/tkatype from tailscale.com/tailcfg+
tailscale.com/types/views from tailscale.com/net/tsaddr+
@@ -74,7 +75,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/fastuuid from tailscale.com/tsweb
tailscale.com/util/lineread from tailscale.com/version/distro
tailscale.com/util/lineiter from tailscale.com/version/distro
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/slicesx from tailscale.com/tailcfg
tailscale.com/util/vizerror from tailscale.com/tailcfg+

View File

@@ -5,10 +5,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
@@ -86,6 +82,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
@@ -124,7 +121,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/client/web+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
@@ -148,6 +144,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/types/key+
tailscale.com/types/views from tailscale.com/tailcfg+
@@ -162,7 +159,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/groupmember from tailscale.com/client/web
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
@@ -324,7 +321,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from archive/tar+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from github.com/coder/websocket/internal/xsync+
runtime/debug from tailscale.com+
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+

View File

@@ -79,10 +79,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -249,6 +245,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
@@ -327,7 +324,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -364,6 +360,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/control/controlclient+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/tka+
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
@@ -381,7 +378,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+

View File

@@ -0,0 +1,30 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestOmitSSH(t *testing.T) {
const msg = "unexpected with ts_omit_ssh"
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
Tags: "ts_omit_ssh",
BadDeps: map[string]string{
"tailscale.com/ssh/tailssh": msg,
"golang.org/x/crypto/ssh": msg,
"tailscale.com/sessionrecording": msg,
"github.com/anmitsu/go-shlex": msg,
"github.com/creack/pty": msg,
"github.com/kr/fs": msg,
"github.com/pkg/sftp": msg,
"github.com/u-root/u-root/pkg/termios": msg,
"tempfork/gliderlabs/ssh": msg,
},
}.Check(t)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || darwin || freebsd || openbsd
//go:build (linux || darwin || freebsd || openbsd) && !ts_omit_ssh
package main

View File

@@ -15,7 +15,7 @@ import (
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/internal/noiseconn"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
@@ -201,7 +201,7 @@ func (up *Upgrader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return nil
}
cbConn, err := controlhttp.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
cbConn, err := controlhttpserver.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
if err != nil {
up.logf("controlhttp: Accept: %v", err)
return

View File

@@ -38,6 +38,7 @@ import (
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dnscache"
@@ -571,9 +572,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
Method: "POST",
URL: u,
Header: http.Header{
"Upgrade": []string{upgradeHeaderValue},
"Connection": []string{"upgrade"},
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
"Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
"Connection": []string{"upgrade"},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
},
}
req = req.WithContext(ctx)
@@ -597,7 +598,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
return nil, fmt.Errorf("httptrace didn't provide a connection")
}
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue {
if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
resp.Body.Close()
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/wsconn"
)
@@ -42,11 +43,11 @@ func (d *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
// Can't set HTTP headers on the websocket request, so we have to to send
// the handshake via an HTTP header.
RawQuery: url.Values{
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
}.Encode(),
}
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
Subprotocols: []string{upgradeHeaderValue},
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
})
if err != nil {
return nil, err

View File

@@ -18,15 +18,6 @@ import (
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
upgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
handshakeHeaderName = "X-Tailscale-Handshake"
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"

View File

@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package controlhttpcommon contains common constants for used
// by the controlhttp client and controlhttpserver packages.
package controlhttpcommon
// UpgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
const UpgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
const HandshakeHeaderName = "X-Tailscale-Handshake"

View File

@@ -3,7 +3,8 @@
//go:build !ios
package controlhttp
// Package controlhttpserver contains the HTTP server side of the ts2021 control protocol.
package controlhttpserver
import (
"context"
@@ -18,6 +19,7 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
@@ -45,12 +47,12 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != upgradeHeaderValue {
if next != controlhttpcommon.UpgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
}
initB64 := r.Header.Get(handshakeHeaderName)
initB64 := r.Header.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
return nil, errors.New("no tailscale handshake header in HTTP request")
@@ -67,7 +69,7 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
return nil, errors.New("can't hijack client connection")
}
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
@@ -117,7 +119,7 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
@@ -129,7 +131,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != upgradeHeaderValue {
if c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
@@ -137,7 +139,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(handshakeHeaderName)
initB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")

View File

@@ -23,12 +23,15 @@ import (
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -158,7 +161,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
return err
}
}
conn, err := AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
if err != nil {
log.Print(err)
}
@@ -529,7 +532,7 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
func brokenMITMHandler(clock tstime.Clock) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
w.(http.Flusher).Flush()
@@ -574,7 +577,7 @@ func TestDialPlan(t *testing.T) {
close(done)
})
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := AcceptHTTP(context.Background(), w, r, server, nil)
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, nil)
if err != nil {
log.Print(err)
} else {
@@ -816,3 +819,14 @@ func (c *closeTrackConn) Close() error {
c.d.noteClose(c)
return c.Conn.Close()
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
// Only the controlhttpserver needs WebSockets...
"github.com/coder/websocket": "controlhttp client shouldn't need websockets",
},
}.Check(t)
}

View File

@@ -22,6 +22,7 @@ import (
"sync"
"testing"
"time"
"unique"
"go4.org/mem"
"golang.org/x/time/rate"
@@ -1598,3 +1599,40 @@ func TestServerRepliesToPing(t *testing.T) {
}
}
}
func BenchmarkUnique(b *testing.B) {
var key [32]byte
for i := range key {
key[i] = byte(i)
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
h := unique.Make(key)
if h.Value() != key {
b.Fatal("unexpected")
}
}
})
}
func BenchmarkLocalMap(b *testing.B) {
var key [32]byte
for i := range key {
key[i] = byte(i)
}
m := map[[32]byte]bool{
key: true,
}
k2 := key
for i := range k2 {
k2[0] = byte(i + 1)
m[k2] = false
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if !m[key] {
b.Fatal("unexpected")
}
}
})
}

View File

@@ -313,6 +313,9 @@ func (c *Client) preferIPv6() bool {
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
func useWebsockets() bool {
if !canWebsockets {
return false
}
if runtime.GOOS == "js" {
return true
}
@@ -383,7 +386,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
var node *tailcfg.DERPNode // nil when using c.url to dial
var idealNodeInRegion bool
switch {
case useWebsockets():
case canWebsockets && useWebsockets():
var urlStr string
if c.url != nil {
urlStr = c.url.String()

View File

@@ -17,7 +17,9 @@ import (
"tailscale.com/derp"
"tailscale.com/net/netmon"
"tailscale.com/tstest/deptest"
"tailscale.com/types/key"
"tailscale.com/util/set"
)
func TestSendRecv(t *testing.T) {
@@ -485,3 +487,23 @@ func TestProbe(t *testing.T) {
}
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
"github.com/coder/websocket": "shouldn't link websockets except on js/wasm",
},
}.Check(t)
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
Tags: "ts_debug_websockets",
WantDeps: set.Of(
"github.com/coder/websocket",
),
}.Check(t)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || js
//go:build js || ((linux || darwin) && ts_debug_websockets)
package derphttp
@@ -14,6 +14,8 @@ import (
"tailscale.com/net/wsconn"
)
const canWebsockets = true
func init() {
dialWebsocketFunc = dialWebsocket
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(js || ((linux || darwin) && ts_debug_websockets))
package derphttp
const canWebsockets = false

View File

@@ -411,7 +411,7 @@ func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION
// Kubernetes Operator components.
func App() string {
a := os.Getenv("TS_INTERNAL_APP")
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource {
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
return a
}
return ""

View File

@@ -25,7 +25,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -231,12 +231,12 @@ func desktop() (ret opt.Bool) {
}
seenDesktop := false
lineread.File("/proc/net/unix", func(line []byte) error {
for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
return nil
})
}
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
@@ -305,21 +305,21 @@ func inContainer() opt.Bool {
ret.Set(true)
return ret
}
lineread.File("/proc/1/cgroup", func(line []byte) error {
for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true)
return io.EOF // arbitrary non-nil error to stop loop
break
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
}
for lr := range lineiter.File("/proc/mounts") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true)
return io.EOF
break
}
return nil
})
}
return ret
}

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/types/ptr"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version/distro"
)
@@ -106,15 +106,18 @@ func linuxVersionMeta() (meta versionMeta) {
}
m := map[string]string{}
lineread.File(propFile, func(line []byte) error {
for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
eq := bytes.IndexByte(line, '=')
if eq == -1 {
return nil
continue
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v
return nil
})
}
if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v

View File

@@ -350,6 +350,8 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
res.PostureDisabled = true
}
b.logf("c2n: posture identity disabled=%v reported %d serials %d hwaddrs", res.PostureDisabled, len(res.SerialNumbers), len(res.IfaceHardwareAddrs))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}

View File

@@ -800,6 +800,19 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
}
// DisconnectControl shuts down control client. This can be run before node shutdown to force control to consider this ndoe
// inactive. This can be used to ensure that nodes that are HA subnet router or app connector replicas are shutting
// down, clients switch over to other replicas whilst the existing connections are kept alive for some period of time.
func (b *LocalBackend) DisconnectControl() {
b.mu.Lock()
defer b.mu.Unlock()
cc := b.resetControlClientLocked()
if cc == nil {
return
}
cc.Shutdown()
}
// captivePortalDetectionInterval is the duration to wait in an unhealthy state with connectivity broken
// before running captive portal detection.
const captivePortalDetectionInterval = 2 * time.Second
@@ -4194,11 +4207,7 @@ func (b *LocalBackend) authReconfig() {
disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC)
userDialUseRoutes := nm.HasCap(tailcfg.NodeAttrUserDialUseRoutes)
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
var forceAAAA bool
if dm, ok := b.sys.DNSManager.GetOK(); ok {
forceAAAA = dm.GetForceAAAA()
}
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.keyExpired, forceAAAA, b.logf, version.OS())
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.keyExpired, b.logf, version.OS())
// If the current node is an app connector, ensure the app connector machine is started
b.reconfigAppConnectorLocked(nm, prefs)
b.mu.Unlock()
@@ -4298,7 +4307,7 @@ func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs,
//
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
// a runtime.GOOS.
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired, forceAAAA bool, logf logger.Logf, versionOS string) *dns.Config {
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired bool, logf logger.Logf, versionOS string) *dns.Config {
if nm == nil {
return nil
}
@@ -4361,7 +4370,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// https://github.com/tailscale/tailscale/issues/1152
// tracks adding the right capability reporting to
// enable AAAA in MagicDNS.
if addr.Addr().Is6() && have4 && !forceAAAA {
if addr.Addr().Is6() && have4 {
continue
}
ips = append(ips, addr.Addr())

View File

@@ -1298,7 +1298,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
}
prefs := &ipn.Prefs{ExitNodeID: tc.exitNode, CorpDNS: true}
got := dnsConfigForNetmap(nm, peersMap(tc.peers), prefs.View(), false, false, t.Logf, "")
got := dnsConfigForNetmap(nm, peersMap(tc.peers), prefs.View(), false, t.Logf, "")
if !resolversEqual(t, got.DefaultResolvers, tc.wantDefaultResolvers) {
t.Errorf("DefaultResolvers: got %#v, want %#v", got.DefaultResolvers, tc.wantDefaultResolvers)
}

View File

@@ -27,7 +27,7 @@ import (
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/mak"
)
@@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
for line := range lineiter.Bytes(out) {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
continue
}
add(string(line))
return nil
})
}
default:
lineread.File("/etc/passwd", func(line []byte) error {
for lr := range lineiter.File("/etc/passwd") {
line, err := lr.Value()
if err != nil {
break
}
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
continue
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
continue
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
}
return res, nil
}

View File

@@ -100,6 +100,7 @@ var handler = map[string]localAPIHandler{
"derpmap": (*Handler).serveDERPMap,
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"dial": (*Handler).serveDial,
"disconnect-control": (*Handler).disconnectControl,
"dns-osconfig": (*Handler).serveDNSOSConfig,
"dns-query": (*Handler).serveDNSQuery,
"drive/fileserver-address": (*Handler).serveDriveServerAddr,
@@ -952,6 +953,22 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
servePprofFunc(w, r)
}
// disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that
// node no longer communicates with control. Doing this makes control consider this node inactive. This can be used
// before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the
// peers to switch over to another replica whilst still maintaining th existing peer connections.
func (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
h.b.DisconnectControl()
}
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)

View File

@@ -5,12 +5,14 @@ package kubetypes
const (
// Hostinfo App values for the Tailscale Kubernetes Operator components.
AppOperator = "k8s-operator"
AppAPIServerProxy = "k8s-operator-proxy"
AppIngressProxy = "k8s-operator-ingress-proxy"
AppIngressResource = "k8s-operator-ingress-resource"
AppEgressProxy = "k8s-operator-egress-proxy"
AppConnector = "k8s-operator-connector-resource"
AppOperator = "k8s-operator"
AppAPIServerProxy = "k8s-operator-proxy"
AppIngressProxy = "k8s-operator-ingress-proxy"
AppIngressResource = "k8s-operator-ingress-resource"
AppEgressProxy = "k8s-operator-egress-proxy"
AppConnector = "k8s-operator-connector-resource"
AppProxyGroupEgress = "k8s-operator-proxygroup-egress"
AppProxyGroupIngress = "k8s-operator-proxygroup-ingress"
// Clientmetrics for Tailscale Kubernetes Operator components
MetricIngressProxyCount = "k8s_ingress_proxies" // L3
@@ -22,5 +24,6 @@ const (
MetricNameserverCount = "k8s_nameserver_resources"
MetricRecorderCount = "k8s_recorder_resources"
MetricEgressServiceCount = "k8s_egress_service_resources"
MetricProxyGroupCount = "k8s_proxygroup_resources"
MetricProxyGroupEgressCount = "k8s_proxygroup_egress_resources"
MetricProxyGroupIngressCount = "k8s_proxygroup_ingress_resources"
)

View File

@@ -57,8 +57,8 @@ 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/4d49adab4de7/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/52804fd3056a/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/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/8865133fd3ef/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/28f7e73c7afb/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/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))

View File

@@ -63,9 +63,7 @@ type Manager struct {
mu sync.Mutex // guards following
// config is the last configuration we successfully compiled or nil if there
// was any failure applying the last configuration.
config *Config
forceAAAA bool // whether client wants MagicDNS AAAA even if unsure of host's IPv6 status
forceFallbackResolvers []*dnstype.Resolver
config *Config
}
// NewManagers created a new manager from the given config.
@@ -130,28 +128,6 @@ func (m *Manager) GetBaseConfig() (OSConfig, error) {
return m.os.GetBaseConfig()
}
func (m *Manager) GetForceAAAA() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.forceAAAA
}
func (m *Manager) SetForceAAAA(v bool) {
m.mu.Lock()
defer m.mu.Unlock()
m.forceAAAA = v
}
// SetForceFallbackResolvers sets the resolvers to use to override
// the fallback resolvers if the control plane doesn't send any.
//
// It takes ownership of the provided slice.
func (m *Manager) SetForceFallbackResolvers(resolvers []*dnstype.Resolver) {
m.mu.Lock()
defer m.mu.Unlock()
m.forceFallbackResolvers = resolvers
}
// setLocked sets the DNS configuration.
//
// m.mu must be held.
@@ -170,10 +146,6 @@ func (m *Manager) setLocked(cfg Config) error {
return err
}
if _, ok := rcfg.Routes["."]; !ok && len(m.forceFallbackResolvers) > 0 {
rcfg.Routes["."] = m.forceFallbackResolvers
}
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)
}))

View File

@@ -5,7 +5,6 @@ package netmon
import (
"bytes"
"errors"
"log"
"net/netip"
"os/exec"
@@ -15,7 +14,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
var (
@@ -34,11 +33,6 @@ func init() {
var procNetRouteErr atomic.Bool
// errStopReading is a sentinel error value used internally by
// lineread.File callers to stop reading. It doesn't escape to
// callers/users.
var errStopReading = errors.New("stop reading")
/*
Parse 10.0.0.1 out of:
@@ -54,44 +48,42 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error {
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
lineNum++
if lineNum == 1 {
// Skip header line.
return nil
continue
}
if lineNum > maxProcNetRouteRead {
return errStopReading
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
return nil
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
return errStopReading
break
}
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with
@@ -144,23 +136,26 @@ func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
return
}
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
lineread.Reader(out, func(line []byte) error {
for lr := range lineiter.Reader(out) {
line, err := lr.Value()
if err != nil {
break
}
const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
return nil
continue
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
return nil
continue
}
ipb := line[:sp]
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
ret = ip
log.Printf("interfaces: found Android default route %v", ip)
}
return nil
})
}
cmd.Process.Kill()
cmd.Wait()
return ret, netip.Addr{}, ret.IsValid()

View File

@@ -4,14 +4,13 @@
package netmon
import (
"errors"
"io"
"net/netip"
"os/exec"
"testing"
"go4.org/mem"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version"
)
@@ -73,31 +72,34 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
var f []mem.RO
lineread.Reader(stdout, func(lineb []byte) error {
for lr := range lineiter.Reader(stdout) {
lineb, err := lr.Value()
if err != nil {
break
}
line := mem.B(lineb)
if !mem.Contains(line, mem.S("default")) {
return nil
continue
}
f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") {
return nil
continue
}
ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) {
return nil
continue
}
if mem.Contains(flagsm, mem.S("I")) {
return nil
continue
}
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() {
ret = ip
netif = netifm.StringCopy()
// We've found what we're looking for.
return errStopReadingNetstatTable
break
}
return nil
})
}
return ret, netif, ret.IsValid()
}
@@ -110,5 +112,3 @@ func TestFetchRoutingTable(t *testing.T) {
}
}
}
var errStopReadingNetstatTable = errors.New("found private gateway")

View File

@@ -23,7 +23,7 @@ import (
"go4.org/mem"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
func init() {
@@ -32,11 +32,6 @@ func init() {
var procNetRouteErr atomic.Bool
// errStopReading is a sentinel error value used internally by
// lineread.File callers to stop reading. It doesn't escape to
// callers/users.
var errStopReading = errors.New("stop reading")
/*
Parse 10.0.0.1 out of:
@@ -52,44 +47,42 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error {
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
return ret, myIP, false
}
lineNum++
if lineNum == 1 {
// Skip header line.
return nil
continue
}
if lineNum > maxProcNetRouteRead {
return errStopReading
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
return nil
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
return errStopReading
break
}
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with

View File

@@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package netmon
import (

View File

@@ -17,7 +17,7 @@ import (
"sync"
"time"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
// These vars are overridden for tests.
@@ -76,21 +76,22 @@ func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
cfg := map[string]string{}
if err := lineread.Reader(r, func(line []byte) error {
for lr := range lineiter.Reader(r) {
line, err := lr.Value()
if err != nil {
return nil, nil, err
}
// accept and skip over empty lines
line = bytes.TrimSpace(line)
if len(line) == 0 {
return nil
continue
}
key, value, ok := strings.Cut(string(line), "=")
if !ok {
return fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
}
cfg[string(key)] = string(value)
return nil
}); err != nil {
return nil, nil, err
}
if cfg["proxy_enabled"] != "yes" {

View File

@@ -861,22 +861,7 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf
return res, gro
}
}
if resp := t.filtRunOut(p, pc); resp != filter.Accept {
return resp, gro
}
if t.PostFilterPacketOutboundToWireGuard != nil {
if res := t.PostFilterPacketOutboundToWireGuard(p, t); res.IsDrop() {
return res, gro
}
}
return filter.Accept, gro
}
// filtRunOut runs the outbound packet filter on p.
// It uses pc to determine if the packet is to a jailed peer and should be
// filtered with the jailed filter.
func (t *Wrapper) filtRunOut(p *packet.Parsed, pc *peerConfigTable) filter.Response {
// If the outbound packet is to a jailed peer, use our jailed peer
// packet filter.
var filt *filter.Filter
@@ -886,7 +871,7 @@ func (t *Wrapper) filtRunOut(p *packet.Parsed, pc *peerConfigTable) filter.Respo
filt = t.filter.Load()
}
if filt == nil {
return filter.Drop
return filter.Drop, gro
}
if filt.RunOut(p, t.filterFlags) != filter.Accept {
@@ -894,9 +879,15 @@ func (t *Wrapper) filtRunOut(p *packet.Parsed, pc *peerConfigTable) filter.Respo
t.metrics.outboundDroppedPacketsTotal.Add(usermetric.DropLabels{
Reason: usermetric.ReasonACL,
}, 1)
return filter.Drop
return filter.Drop, gro
}
return filter.Accept
if t.PostFilterPacketOutboundToWireGuard != nil {
if res := t.PostFilterPacketOutboundToWireGuard(p, t); res.IsDrop() {
return res, gro
}
}
return filter.Accept, gro
}
// noteActivity records that there was a read or write at the current time.
@@ -1060,11 +1051,6 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(pkt)
response, _ := t.filterPacketOutboundToWireGuard(p, pc, nil)
if response != filter.Accept {
metricPacketOutDrop.Add(1)
return
}
invertGSOChecksum(pkt, gso)
pc.snat(p)

View File

@@ -71,6 +71,7 @@ package safeweb
import (
"cmp"
"context"
crand "crypto/rand"
"fmt"
"log"
@@ -416,3 +417,7 @@ func (s *Server) ListenAndServe(addr string) error {
func (s *Server) Close() error {
return s.h.Close()
}
// Shutdown gracefully shuts down the server without interrupting any active
// connections. It has the same semantics as[http.Server.Shutdown].
func (s *Server) Shutdown(ctx context.Context) error { return s.h.Shutdown(ctx) }

View File

@@ -48,7 +48,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
"tailscale.com/util/cibuild"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/must"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
@@ -1123,14 +1123,11 @@ func TestSSH(t *testing.T) {
func parseEnv(out []byte) map[string]string {
e := map[string]string{}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
i := bytes.IndexByte(line, '=')
if i == -1 {
return nil
for line := range lineiter.Bytes(out) {
if i := bytes.IndexByte(line, '='); i != -1 {
e[string(line[:i])] = string(line[i+1:])
}
e[string(line[:i])] = string(line[i+1:])
return nil
})
}
return e
}

View File

@@ -6,7 +6,6 @@
package tailssh
import (
"io"
"os"
"os/exec"
"os/user"
@@ -18,7 +17,7 @@ import (
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/osuser"
"tailscale.com/version/distro"
)
@@ -110,15 +109,16 @@ func defaultPathForUser(u *user.User) string {
}
func defaultPathForUserOnNixOS(u *user.User) string {
var path string
lineread.File("/etc/pam/environment", func(lineb []byte) error {
if v := pathFromPAMEnvLine(lineb, u); v != "" {
path = v
return io.EOF // stop iteration
for lr := range lineiter.File("/etc/pam/environment") {
lineb, err := lr.Value()
if err != nil {
return ""
}
return nil
})
return path
if v := pathFromPAMEnvLine(lineb, u); v != "" {
return v
}
}
return ""
}
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {

View File

@@ -121,11 +121,6 @@ type Server struct {
// field at zero unless you know what you are doing.
Port uint16
// PreStart is an optional hook to run just before LocalBackend.Start,
// to reconfigure internals. If it returns an error, Server.Start
// will return that error, wrapper.
PreStart func() error
getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error)
initOnce sync.Once
@@ -621,13 +616,6 @@ func (s *Server) start() (reterr error) {
prefs.ControlURL = s.ControlURL
prefs.RunWebClient = s.RunWebClient
authKey := s.getAuthKey()
if f := s.PreStart; f != nil {
if err := f(); err != nil {
return fmt.Errorf("PreStart: %w", err)
}
}
err = lb.Start(ipn.Options{
UpdatePrefs: prefs,
AuthKey: authKey,

View File

@@ -13,14 +13,19 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"testing"
"tailscale.com/util/set"
)
type DepChecker struct {
GOOS string // optional
GOARCH string // optional
BadDeps map[string]string // package => why
GOOS string // optional
GOARCH string // optional
BadDeps map[string]string // package => why
WantDeps set.Set[string] // packages expected
Tags string // comma-separated
}
func (c DepChecker) Check(t *testing.T) {
@@ -29,7 +34,7 @@ func (c DepChecker) Check(t *testing.T) {
t.Skip("skipping dep tests on windows hosts")
}
t.Helper()
cmd := exec.Command("go", "list", "-json", ".")
cmd := exec.Command("go", "list", "-json", "-tags="+c.Tags, ".")
var extraEnv []string
if c.GOOS != "" {
extraEnv = append(extraEnv, "GOOS="+c.GOOS)
@@ -54,6 +59,11 @@ func (c DepChecker) Check(t *testing.T) {
t.Errorf("package %q is not allowed as a dependency (env: %q); reason: %s", dep, extraEnv, why)
}
}
for dep := range c.WantDeps {
if !slices.Contains(res.Deps, dep) {
t.Errorf("expected package %q to be a dependency (env: %q)", dep, extraEnv)
}
}
t.Logf("got %d dependencies", len(res.Deps))
}

View File

@@ -26,7 +26,7 @@ import (
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
@@ -288,7 +288,7 @@ func (s *Server) serveNoiseUpgrade(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
noisePrivate := s.noisePrivKey
s.mu.Unlock()
cc, err := controlhttp.AcceptHTTP(ctx, w, r, noisePrivate, nil)
cc, err := controlhttpserver.AcceptHTTP(ctx, w, r, noisePrivate, nil)
if err != nil {
log.Printf("AcceptHTTP: %v", err)
return

View File

@@ -23,10 +23,16 @@ import (
"tailscale.com/version"
)
// StaticStringVar returns a new expvar.Var that always returns s.
func StaticStringVar(s string) expvar.Var {
var v any = s // box s into an interface just once
return expvar.Func(func() any { return v })
}
func init() {
expvar.Publish("process_start_unix_time", expvar.Func(func() any { return timeStart.Unix() }))
expvar.Publish("version", expvar.Func(func() any { return version.Long() }))
expvar.Publish("go_version", expvar.Func(func() any { return runtime.Version() }))
expvar.Publish("version", StaticStringVar(version.Long()))
expvar.Publish("go_version", StaticStringVar(runtime.Version()))
expvar.Publish("counter_uptime_sec", expvar.Func(func() any { return int64(Uptime().Seconds()) }))
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
}

49
types/result/result.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package result contains the Of result type, which is
// either a value or an error.
package result
// Of is either a T value or an error.
//
// Think of it like Rust or Swift's result types.
// It's named "Of" because the fully qualified name
// for callers reads result.Of[T].
type Of[T any] struct {
v T // valid if Err is nil; invalid if Err is non-nil
err error
}
// Value returns a new result with value v,
// without an error.
func Value[T any](v T) Of[T] {
return Of[T]{v: v}
}
// Error returns a new result with error err.
// If err is nil, the returned result is equivalent
// to calling Value with T's zero value.
func Error[T any](err error) Of[T] {
return Of[T]{err: err}
}
// MustValue returns r's result value.
// It panics if r.Err returns non-nil.
func (r Of[T]) MustValue() T {
if r.err != nil {
panic(r.err)
}
return r.v
}
// Value returns r's result value and error.
func (r Of[T]) Value() (T, error) {
return r.v, r.err
}
// Err returns r's error, if any.
// When r.Err returns nil, it's safe to call r.MustValue without it panicking.
func (r Of[T]) Err() error {
return r.err
}

72
util/lineiter/lineiter.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package lineiter iterates over lines in things.
package lineiter
import (
"bufio"
"bytes"
"io"
"iter"
"os"
"tailscale.com/types/result"
)
// File returns an iterator that reads lines from the named file.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func File(name string) iter.Seq[result.Of[[]byte]] {
f, err := os.Open(name)
return reader(f, f, err)
}
// Bytes returns an iterator over the lines in bs.
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Bytes(bs []byte) iter.Seq[[]byte] {
return func(yield func([]byte) bool) {
for len(bs) > 0 {
i := bytes.IndexByte(bs, '\n')
if i < 0 {
yield(bs)
return
}
if !yield(bs[:i]) {
return
}
bs = bs[i+1:]
}
}
}
// Reader returns an iterator over the lines in r.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Reader(r io.Reader) iter.Seq[result.Of[[]byte]] {
return reader(r, nil, nil)
}
func reader(r io.Reader, c io.Closer, err error) iter.Seq[result.Of[[]byte]] {
return func(yield func(result.Of[[]byte]) bool) {
if err != nil {
yield(result.Error[[]byte](err))
return
}
if c != nil {
defer c.Close()
}
bs := bufio.NewScanner(r)
for bs.Scan() {
if !yield(result.Value(bs.Bytes())) {
return
}
}
if err := bs.Err(); err != nil {
yield(result.Error[[]byte](err))
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package lineiter
import (
"slices"
"strings"
"testing"
)
func TestBytesLines(t *testing.T) {
var got []string
for line := range Bytes([]byte("foo\n\nbar\nbaz")) {
got = append(got, string(line))
}
want := []string{"foo", "", "bar", "baz"}
if !slices.Equal(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}
func TestReader(t *testing.T) {
var got []string
for line := range Reader(strings.NewReader("foo\n\nbar\nbaz")) {
got = append(got, string(line.MustValue()))
}
want := []string{"foo", "", "bar", "baz"}
if !slices.Equal(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}

View File

@@ -8,26 +8,26 @@ import (
"os"
"strings"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
func ownerOfPID(pid int) (userID string, err error) {
file := fmt.Sprintf("/proc/%d/status", pid)
err = lineread.File(file, func(line []byte) error {
for lr := range lineiter.File(file) {
line, err := lr.Value()
if err != nil {
if os.IsNotExist(err) {
return "", ErrProcessNotFound
}
return "", err
}
if len(line) < 4 || string(line[:4]) != "Uid:" {
return nil
continue
}
f := strings.Fields(string(line))
if len(f) >= 2 {
userID = f[1] // real userid
}
return nil
})
if os.IsNotExist(err) {
return "", ErrProcessNotFound
}
if err != nil {
return
}
if userID == "" {
return "", fmt.Errorf("missing Uid line in %s", file)

View File

@@ -6,13 +6,12 @@ package distro
import (
"bytes"
"io"
"os"
"runtime"
"strconv"
"tailscale.com/types/lazy"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
type Distro string
@@ -132,18 +131,19 @@ func DSMVersion() int {
return v
}
// But when run from the command line, we have to read it from the file:
lineread.File("/etc/VERSION", func(line []byte) error {
for lr := range lineiter.File("/etc/VERSION") {
line, err := lr.Value()
if err != nil {
break // but otherwise ignore
}
line = bytes.TrimSpace(line)
if string(line) == `majorversion="7"` {
v = 7
return io.EOF
return 7
}
if string(line) == `majorversion="6"` {
v = 6
return io.EOF
return 6
}
return nil
})
return v
}
return 0
})
}

View File

@@ -21,7 +21,6 @@ type Config struct {
NodeID tailcfg.StableNodeID
PrivateKey key.NodePrivate
Addresses []netip.Prefix
ListenPort uint16 // not used by Tailscale's conn.Bind implementation
MTU uint16
DNS []netip.Addr
Peers []Peer

View File

@@ -124,13 +124,7 @@ func (cfg *Config) handleDeviceLine(k, value mem.RO, valueBytes []byte) error {
if err != nil {
return err
}
case k.EqualString("listen_port"):
port, err := mem.ParseUint(value, 10, 16)
if err != nil {
return fmt.Errorf("failed to parse listen_port: %w", err)
}
cfg.ListenPort = uint16(port)
case k.EqualString("fwmark"):
case k.EqualString("listen_port") || k.EqualString("fwmark"):
// ignore
default:
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())

View File

@@ -39,7 +39,6 @@ var _ConfigCloneNeedsRegeneration = Config(struct {
NodeID tailcfg.StableNodeID
PrivateKey key.NodePrivate
Addresses []netip.Prefix
ListenPort uint16
MTU uint16
DNS []netip.Addr
Peers []Peer

View File

@@ -42,9 +42,6 @@ func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
if !prev.PrivateKey.Equal(cfg.PrivateKey) {
set("private_key", cfg.PrivateKey.UntypedHexString())
}
if prev.ListenPort != cfg.ListenPort {
setUint16("listen_port", cfg.ListenPort)
}
old := make(map[key.NodePublic]Peer)
for _, p := range prev.Peers {
@@ -90,9 +87,7 @@ func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
// See corp issue 3016.
logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey)
}
if cfg.NodeID != "" {
set("endpoint", p.PublicKey.UntypedHexString())
}
set("endpoint", p.PublicKey.UntypedHexString())
}
// TODO: replace_allowed_ips is expensive.