Compare commits

..

28 Commits

Author SHA1 Message Date
Brad Fitzpatrick
8bfccc5a49 cmd/lopower: fix typo
Change-Id: Ifebcd361d80f093e93be4646badaebd856316018
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-04 08:18:08 -08:00
Brad Fitzpatrick
1528c94a79 cmd/lopower: flesh out README
Change-Id: Iece542369f4124a3e7e46653211b081bce601a29
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-04 07:56:44 -08:00
Maisem Ali
5bbff79412 Update README.md 2024-11-03 17:26:40 -08:00
Brad Fitzpatrick
3a3fc3e8ac cmd/lopower: flesh out Limitations
Change-Id: Ifef0f10e8a516caa5ba63c37605f3af4f183c1e0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 16:57:21 -08:00
Brad Fitzpatrick
2e70eef65c cmd/lopower: add Limitations to README
Change-Id: I6baaa7fa45eaa0c90482123acc51d0f402205e96
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 16:55:02 -08:00
Brad Fitzpatrick
c0ca41daeb cmd/lopower: add start of a README
Change-Id: Ie4d5a8e0a53dafdf3b479457b309ac963b78f9bc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 15:11:57 -08:00
Brad Fitzpatrick
c2e1e9e68e cmd/lopower: add TCP DNS support
Change-Id: I3288bfd538e2662d644c75e62e6c5cdb24464386
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 14:33:19 -08:00
Brad Fitzpatrick
510f0745ea cmd/lopower: set ultimate fallback DNS servers
Change-Id: I044d6c7b9dc4b874f3c60d2c6ce2105bafc00639
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 14:21:57 -08:00
Brad Fitzpatrick
44570dabda cmd/lopower: filter MagicDNS A records when IPv4 disabled
Change-Id: I5c34707e9918b1fc5401075dd6b69fc582de77d9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 12:33:12 -08:00
Brad Fitzpatrick
744e386d26 cmd/lopower: add TODO for TCP DNS
Change-Id: I6027c41cbec2b4b146b8b96ad9c6b60f2ef2c520
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 12:10:47 -08:00
Brad Fitzpatrick
da517a04d2 cmd/lopower: refactor in prep for DNS filtering
Change-Id: I4a4c2d8331fbc2a7aaab4e75c27b29afaaeb0300
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 12:08:37 -08:00
Brad Fitzpatrick
9065c7fb00 cmd/lopower: force AAAA records in DNS server
Change-Id: I79582e4ffd468c217df3e8c02bbf65d911a4f0fe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 11:52:50 -08:00
Brad Fitzpatrick
6f97d4b26b cmd/lopower: add start of DNS server
Change-Id: Ibc9b1ee914a649a4f032062c23d700cfbef84312
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 10:36:04 -08:00
Brad Fitzpatrick
d08c776848 Merge remote-tracking branch 'origin/main' into lp
Change-Id: Id4025fee947d4695fce2b18dc04c42df8f4ac177
2024-11-03 10:00:58 -08:00
Brad Fitzpatrick
ec8feb6aad cmd/lopower: add verbose-packets and include-v4 flags
Change-Id: I523feb2a1d4b409689f6011ae03cbc336c695ecf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 06:57:30 -08:00
Maisem Ali
7fdcab4dec cmd/lopower: add interface addresses to netstack
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-03 06:51:15 -08:00
Maisem Ali
cc326ea820 cmd/lopower: add udp forwarding
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 18:00:24 -07:00
Maisem Ali
0e39594ef1 cmd/lopower: get e2e packets working
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 15:34:12 -07:00
Brad Fitzpatrick
b1d8872d10 cmd/lopower: add QR code handler
Change-Id: I0c379cfeff9855b745ba705beb574dab6d26b305
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-02 14:52:50 -07:00
Maisem Ali
406bcadc65 cmd/lopower: add tcp forwarding/stitching
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 14:34:14 -07:00
Maisem Ali
1a45fae577 cmd/lopower: move lp init to newLP
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 14:16:40 -07:00
Brad Fitzpatrick
0fec73082a cmd/lopower: wire up --wg-port flag
Change-Id: I406cf97c4af27c2ac4b4fe28eb8a3b30fac37e5b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-02 14:01:29 -07:00
Brad Fitzpatrick
e1f6f6f8c2 wgengine/wgcfg: restore Config.ListenPort for cmd/lopower
It was removed for being unnecessary in 69cdc30c6d in 2021
but now we need it for lopower.

Change-Id: I62aca850ad820a83475c8ef7655a08981f23a27f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-02 13:59:16 -07:00
Maisem Ali
a4b4656879 cmd/lopower: hook up read/write
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 13:50:48 -07:00
Brad Fitzpatrick
522124c5a7 cmd/lopower: add start of some flags
Change-Id: Ic633e0d158e689b4108fecf0fc353d37fb22f375
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-02 13:42:39 -07:00
Maisem Ali
cf45d98caa cmd/lopower: move reconfig to lpServer
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 13:13:16 -07:00
Maisem Ali
1f9b619588 cmd/lopower: add skeleton wg bringup
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-02 13:01:23 -07:00
Brad Fitzpatrick
2b4594c3ff cmd/lopower: start of a shell
Change-Id: I07eebc68cb48aa956b9eb8e67b06375df6458a48
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-02 12:55:49 -07:00
97 changed files with 1435 additions and 1263 deletions

View File

@@ -17,20 +17,12 @@ eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.18"
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
# annotations defined here will be overriden by release scripts that call this script.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
DEFAULT_ANNOTATIONS="org.opencontainers.image.source=https://github.com/tailscale/tailscale/blob/main/build_docker.sh,org.opencontainers.image.vendor=Tailscale"
PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
# OCI annotations that will be added to the image.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}"
case "$TARGET" in
client)
@@ -51,7 +43,6 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/containerboot
;;
operator)
@@ -69,7 +60,6 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/operator
;;
k8s-nameserver)
@@ -87,7 +77,6 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/k8s-nameserver
;;
*)

View File

@@ -1327,17 +1327,6 @@ 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,7 +140,6 @@ 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+
@@ -155,7 +154,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/lineiter from tailscale.com/hostinfo+
tailscale.com/util/lineread 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+
@@ -264,7 +263,6 @@ 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+
@@ -309,8 +307,6 @@ 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,7 +19,6 @@ import (
"expvar"
"flag"
"fmt"
"html/template"
"io"
"log"
"math"
@@ -213,16 +212,25 @@ func main() {
tsweb.AddBrowserHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
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
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")
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -460,52 +468,3 @@ 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,9 +4,7 @@
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -112,30 +110,3 @@ 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

@@ -13,8 +13,7 @@ import (
"sync"
"time"
"errors"
"github.com/pkg/errors"
"go.uber.org/zap"
xslices "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
@@ -59,7 +58,6 @@ type ConnectorReconciler struct {
subnetRouters set.Slice[types.UID] // for subnet routers gauge
exitNodes set.Slice[types.UID] // for exit nodes gauge
appConnectors set.Slice[types.UID] // for app connectors gauge
}
var (
@@ -69,8 +67,6 @@ var (
gaugeConnectorSubnetRouterResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithSubnetRouterCount)
// gaugeConnectorExitNodeResources tracks the number of Connectors currently managed by this operator instance that are exit nodes.
gaugeConnectorExitNodeResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithExitNodeCount)
// gaugeConnectorAppConnectorResources tracks the number of Connectors currently managed by this operator instance that are app connectors.
gaugeConnectorAppConnectorResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithAppConnectorCount)
)
func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
@@ -112,12 +108,13 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
oldCnStatus := cn.Status.DeepCopy()
setStatus := func(cn *tsapi.Connector, _ tsapi.ConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, status, reason, message, cn.Generation, a.clock, logger)
var updateErr error
if !apiequality.Semantic.DeepEqual(oldCnStatus, cn.Status) {
// An error encountered here should get returned by the Reconcile function.
updateErr = a.Client.Status().Update(ctx, cn)
if updateErr := a.Client.Status().Update(ctx, cn); updateErr != nil {
err = errors.Wrap(err, updateErr.Error())
}
}
return res, errors.Join(err, updateErr)
return res, err
}
if !slices.Contains(cn.Finalizers, FinalizerName) {
@@ -153,9 +150,6 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
cn.Status.SubnetRoutes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
if cn.Spec.AppConnector != nil {
cn.Status.IsAppConnector = true
}
cn.Status.SubnetRoutes = ""
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
@@ -195,37 +189,23 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
sts.Connector.routes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
}
if cn.Spec.AppConnector != nil {
sts.Connector.isAppConnector = true
if len(cn.Spec.AppConnector.Routes) != 0 {
sts.Connector.routes = cn.Spec.AppConnector.Routes.Stringify()
}
}
a.mu.Lock()
if cn.Spec.ExitNode {
if sts.Connector.isExitNode {
a.exitNodes.Add(cn.UID)
} else {
a.exitNodes.Remove(cn.UID)
}
if cn.Spec.SubnetRouter != nil {
if sts.Connector.routes != "" {
a.subnetRouters.Add(cn.GetUID())
} else {
a.subnetRouters.Remove(cn.GetUID())
}
if cn.Spec.AppConnector != nil {
a.appConnectors.Add(cn.GetUID())
} else {
a.appConnectors.Remove(cn.GetUID())
}
a.mu.Unlock()
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
gaugeConnectorAppConnectorResources.Set(int64(a.appConnectors.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
connectors.AddSlice(a.appConnectors.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
_, err := a.ssr.Provision(ctx, logger, sts)
@@ -268,15 +248,12 @@ func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger
a.mu.Lock()
a.subnetRouters.Remove(cn.UID)
a.exitNodes.Remove(cn.UID)
a.appConnectors.Remove(cn.UID)
a.mu.Unlock()
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorAppConnectorResources.Set(int64(a.appConnectors.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
connectors.AddSlice(a.appConnectors.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
return true, nil
}
@@ -285,14 +262,8 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
// Connector fields are already validated at apply time with CEL validation
// on custom resource fields. The checks here are a backup in case the
// CEL validation breaks without us noticing.
if cn.Spec.SubnetRouter == nil && !cn.Spec.ExitNode && cn.Spec.AppConnector == nil {
return errors.New("invalid spec: a Connector must be configured as at least one of subnet router, exit node or app connector")
}
if (cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) && cn.Spec.AppConnector != nil {
return errors.New("invalid spec: a Connector that is configured as an app connector must not be also configured as a subnet router or exit node")
}
if cn.Spec.AppConnector != nil {
return validateAppConnector(cn.Spec.AppConnector)
if !(cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) {
return errors.New("invalid spec: a Connector must expose subnet routes or act as an exit node (or both)")
}
if cn.Spec.SubnetRouter == nil {
return nil
@@ -301,27 +272,19 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
}
func validateSubnetRouter(sb *tsapi.SubnetRouter) error {
if len(sb.AdvertiseRoutes) == 0 {
if len(sb.AdvertiseRoutes) < 1 {
return errors.New("invalid subnet router spec: no routes defined")
}
return validateRoutes(sb.AdvertiseRoutes)
}
func validateAppConnector(ac *tsapi.AppConnector) error {
return validateRoutes(ac.Routes)
}
func validateRoutes(routes tsapi.Routes) error {
var errs []error
for _, route := range routes {
var err error
for _, route := range sb.AdvertiseRoutes {
pfx, e := netip.ParsePrefix(string(route))
if e != nil {
errs = append(errs, fmt.Errorf("route %v is invalid: %v", route, e))
err = errors.Wrap(err, fmt.Sprintf("route %s is invalid: %v", route, err))
continue
}
if pfx.Masked() != pfx {
errs = append(errs, fmt.Errorf("route %s has non-address bits set; expected %s", pfx, pfx.Masked()))
err = errors.Wrap(err, fmt.Sprintf("route %s has non-address bits set; expected %s", pfx, pfx.Masked()))
}
}
return errors.Join(errs...)
return err
}

View File

@@ -8,14 +8,12 @@ package main
import (
"context"
"testing"
"time"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
@@ -298,100 +296,3 @@ func TestConnectorWithProxyClass(t *testing.T) {
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
}
func TestConnectorWithAppConnector(t *testing.T) {
// Setup
cn := &tsapi.Connector{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
UID: types.UID("1234-UID"),
},
TypeMeta: metav1.TypeMeta{
Kind: tsapi.ConnectorKind,
APIVersion: "tailscale.io/v1alpha1",
},
Spec: tsapi.ConnectorSpec{
AppConnector: &tsapi.AppConnector{},
},
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(cn).
WithStatusSubresource(cn).
Build()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
cl := tstest.NewClock(tstest.ClockOpts{})
fr := record.NewFakeRecorder(1)
cr := &ConnectorReconciler{
Client: fc,
clock: cl,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
recorder: fr,
}
// 1. Connector with app connnector is created and becomes ready
expectReconciled(t, cr, "", "test")
fullName, shortName := findGenName(t, fc, "", "test", "connector")
opts := configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
app: kubetypes.AppConnector,
isAppConnector: true,
}
expectEqual(t, fc, expectedSecret(t, fc, opts), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Connector's ready condition should be set to true
cn.ObjectMeta.Finalizers = append(cn.ObjectMeta.Finalizers, "tailscale.com/finalizer")
cn.Status.IsAppConnector = true
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorCreated,
Message: reasonConnectorCreated,
}}
expectEqual(t, fc, cn, nil)
// 2. Connector with invalid app connector routes has status set to invalid
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("1.2.3.4/5")}
})
cn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("1.2.3.4/5")}
expectReconciled(t, cr, "", "test")
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorInvalid,
Message: "Connector is invalid: route 1.2.3.4/5 has non-address bits set; expected 0.0.0.0/5",
}}
expectEqual(t, fc, cn, nil)
// 3. Connector with valid app connnector routes becomes ready
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("10.88.2.21/32")}
})
cn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("10.88.2.21/32")}
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorCreated,
Message: reasonConnectorCreated,
}}
expectReconciled(t, cr, "", "test")
}

View File

@@ -80,6 +80,10 @@ 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+
@@ -654,7 +658,6 @@ 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+
@@ -737,6 +740,7 @@ 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
@@ -771,7 +775,6 @@ 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+
@@ -789,7 +792,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/lineiter from tailscale.com/hostinfo+
tailscale.com/util/lineread 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

@@ -24,10 +24,6 @@ spec:
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Whether this Connector instance is an app connector.
jsonPath: .status.isAppConnector
name: IsAppConnector
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
@@ -70,40 +66,10 @@ spec:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
type: object
properties:
appConnector:
description: |-
AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
Connector does not act as an app connector.
Note that you will need to manually configure the permissions and the domains for the app connector via the
Admin panel.
Note also that the main tested and supported use case of this config option is to deploy an app connector on
Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
tested or optimised for.
If you are using the app connector to access SaaS applications because you need a predictable egress IP that
can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
device with a static IP address.
https://tailscale.com/kb/1281/app-connectors
type: object
properties:
routes:
description: |-
Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
type: array
minItems: 1
items:
type: string
format: cidr
exitNode:
description: |-
ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
This field is mutually exclusive with the appConnector field.
ExitNode defines whether the Connector node should act as a
Tailscale exit node. Defaults to false.
https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
@@ -124,11 +90,9 @@ spec:
type: string
subnetRouter:
description: |-
SubnetRouter defines subnet routes that the Connector device should
expose to tailnet as a Tailscale subnet router.
SubnetRouter defines subnet routes that the Connector node should
expose to tailnet. If unset, none are exposed.
https://tailscale.com/kb/1019/subnets/
If this field is unset, the device does not get configured as a Tailscale subnet router.
This field is mutually exclusive with the appConnector field.
type: object
required:
- advertiseRoutes
@@ -161,10 +125,8 @@ spec:
type: string
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
x-kubernetes-validations:
- rule: has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)
message: A Connector needs to have at least one of exit node, subnet router or app connector configured.
- rule: '!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))'
message: The appConnector field is mutually exclusive with exitNode and subnetRouter fields.
- rule: has(self.subnetRouter) || self.exitNode == true
message: A Connector needs to be either an exit node or a subnet router, or both.
status:
description: |-
ConnectorStatus describes the status of the Connector. This is set
@@ -238,9 +200,6 @@ spec:
If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the
node.
type: string
isAppConnector:
description: IsAppConnector is set to true if the Connector acts as an app connector.
type: boolean
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean

View File

@@ -53,10 +53,6 @@ spec:
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Whether this Connector instance is an app connector.
jsonPath: .status.isAppConnector
name: IsAppConnector
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
@@ -95,40 +91,10 @@ spec:
More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
appConnector:
description: |-
AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
Connector does not act as an app connector.
Note that you will need to manually configure the permissions and the domains for the app connector via the
Admin panel.
Note also that the main tested and supported use case of this config option is to deploy an app connector on
Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
tested or optimised for.
If you are using the app connector to access SaaS applications because you need a predictable egress IP that
can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
device with a static IP address.
https://tailscale.com/kb/1281/app-connectors
properties:
routes:
description: |-
Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
items:
format: cidr
type: string
minItems: 1
type: array
type: object
exitNode:
description: |-
ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
This field is mutually exclusive with the appConnector field.
ExitNode defines whether the Connector node should act as a
Tailscale exit node. Defaults to false.
https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
@@ -149,11 +115,9 @@ spec:
type: string
subnetRouter:
description: |-
SubnetRouter defines subnet routes that the Connector device should
expose to tailnet as a Tailscale subnet router.
SubnetRouter defines subnet routes that the Connector node should
expose to tailnet. If unset, none are exposed.
https://tailscale.com/kb/1019/subnets/
If this field is unset, the device does not get configured as a Tailscale subnet router.
This field is mutually exclusive with the appConnector field.
properties:
advertiseRoutes:
description: |-
@@ -187,10 +151,8 @@ spec:
type: array
type: object
x-kubernetes-validations:
- message: A Connector needs to have at least one of exit node, subnet router or app connector configured.
rule: has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)
- message: The appConnector field is mutually exclusive with exitNode and subnetRouter fields.
rule: '!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))'
- message: A Connector needs to be either an exit node or a subnet router, or both.
rule: has(self.subnetRouter) || self.exitNode == true
status:
description: |-
ConnectorStatus describes the status of the Connector. This is set
@@ -263,9 +225,6 @@ spec:
If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the
node.
type: string
isAppConnector:
description: IsAppConnector is set to true if the Connector acts as an app connector.
type: boolean
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean

View File

@@ -11,7 +11,6 @@ import (
"context"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -151,13 +150,6 @@ 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

@@ -1388,7 +1388,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "362360188dac62bca8013c8134929fed8efd84b1f410c00873d14a05709b5647",
confFileHash: "e09bededa0379920141cbd0b0dbdf9b8b66545877f9e8397423f5ce3e1ba439e",
app: kubetypes.AppIngressProxy,
}
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
@@ -1399,7 +1399,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
mak.Set(&svc.Annotations, AnnotationHostname, "another-test")
})
o.hostname = "another-test"
o.confFileHash = "20db57cfabc3fc6490f6bb1dc85994e61d255cdfa2a56abb0141736e59f263ef"
o.confFileHash = "5d754cf55463135ee34aa9821f2fd8483b53eb0570c3740c84a086304f427684"
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
}

View File

@@ -47,7 +47,7 @@ const (
reasonProxyGroupInvalid = "ProxyGroupInvalid"
)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupEgressCount)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupCount)
// ProxyGroupReconciler ensures cluster resources for a ProxyGroup definition.
type ProxyGroupReconciler struct {
@@ -353,7 +353,7 @@ func (r *ProxyGroupReconciler) deleteTailnetDevice(ctx context.Context, id tailc
func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, pg *tsapi.ProxyGroup, proxyClass *tsapi.ProxyClass) (hash string, err error) {
logger := r.logger(pg.Name)
var configSHA256Sum string
var allConfigs []tailscaledConfigs
for i := range pgReplicas(pg) {
cfgSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -389,6 +389,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
if err != nil {
return "", fmt.Errorf("error creating tailscaled config: %w", err)
}
allConfigs = append(allConfigs, configs)
for cap, cfg := range configs {
cfgJSON, err := json.Marshal(cfg)
@@ -398,32 +399,6 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
mak.Set(&cfgSecret.StringData, tsoperator.TailscaledConfigFileName(cap), string(cfgJSON))
}
// The config sha256 sum is a value for a hash annotation used to trigger
// pod restarts when tailscaled config changes. Any config changes apply
// to all replicas, so it is sufficient to only hash the config for the
// first replica.
//
// In future, we're aiming to eliminate restarts altogether and have
// pods dynamically reload their config when it changes.
if i == 0 {
sum := sha256.New()
for _, cfg := range configs {
// Zero out the auth key so it doesn't affect the sha256 hash when we
// remove it from the config after the pods have all authed. Otherwise
// all the pods will need to restart immediately after authing.
cfg.AuthKey = nil
b, err := json.Marshal(cfg)
if err != nil {
return "", err
}
if _, err := sum.Write(b); err != nil {
return "", err
}
}
configSHA256Sum = fmt.Sprintf("%x", sum.Sum(nil))
}
if existingCfgSecret != nil {
logger.Debugf("patching the existing ProxyGroup config Secret %s", cfgSecret.Name)
if err := r.Patch(ctx, cfgSecret, client.MergeFrom(existingCfgSecret)); err != nil {
@@ -437,7 +412,16 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
}
}
return configSHA256Sum, nil
sum := sha256.New()
b, err := json.Marshal(allConfigs)
if err != nil {
return "", err
}
if _, err := sum.Write(b); err != nil {
return "", err
}
return fmt.Sprintf("%x", sum.Sum(nil)), nil
}
func pgTailscaledConfig(pg *tsapi.ProxyGroup, class *tsapi.ProxyClass, idx int32, authKey string, oldSecret *corev1.Secret) (tailscaledConfigs, error) {

View File

@@ -15,7 +15,6 @@ import (
"sigs.k8s.io/yaml"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/ptr"
)
@@ -93,10 +92,6 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode, cfgHa
c.Image = image
c.VolumeMounts = func() []corev1.VolumeMount {
var mounts []corev1.VolumeMount
// TODO(tomhjp): Read config directly from the secret instead. The
// mounts change on scaling up/down which causes unnecessary restarts
// for pods that haven't meaningfully changed.
for i := range pgReplicas(pg) {
mounts = append(mounts, corev1.VolumeMount{
Name: fmt.Sprintf("tailscaledconfig-%d", i),
@@ -151,10 +146,6 @@ 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

@@ -35,8 +35,6 @@ var defaultProxyClassAnnotations = map[string]string{
}
func TestProxyGroup(t *testing.T) {
const initialCfgHash = "6632726be70cf224049580deb4d317bba065915b5fd415461d60ed621c91b196"
pc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "default-pc",
@@ -82,7 +80,6 @@ func TestProxyGroup(t *testing.T) {
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "the ProxyGroup's ProxyClass default-pc is not yet in a ready state, waiting...", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, false, "")
})
t.Run("observe_ProxyGroupCreating_status_reason", func(t *testing.T) {
@@ -103,11 +100,10 @@ func TestProxyGroup(t *testing.T) {
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "0/2 ProxyGroup pods running", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
if expected := 1; reconciler.proxyGroups.Len() != expected {
t.Fatalf("expected %d recorders, got %d", expected, reconciler.proxyGroups.Len())
}
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
expectProxyGroupResources(t, fc, pg, true)
keyReq := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
@@ -139,7 +135,7 @@ func TestProxyGroup(t *testing.T) {
}
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionTrue, reasonProxyGroupReady, reasonProxyGroupReady, 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
expectProxyGroupResources(t, fc, pg, true)
})
t.Run("scale_up_to_3", func(t *testing.T) {
@@ -150,7 +146,6 @@ func TestProxyGroup(t *testing.T) {
expectReconciled(t, reconciler, "", pg.Name)
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "2/3 ProxyGroup pods running", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
addNodeIDToStateSecrets(t, fc, pg)
expectReconciled(t, reconciler, "", pg.Name)
@@ -160,7 +155,7 @@ func TestProxyGroup(t *testing.T) {
TailnetIPs: []string{"1.2.3.4", "::1"},
})
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
expectProxyGroupResources(t, fc, pg, true)
})
t.Run("scale_down_to_1", func(t *testing.T) {
@@ -168,26 +163,11 @@ func TestProxyGroup(t *testing.T) {
mustUpdate(t, fc, "", pg.Name, func(p *tsapi.ProxyGroup) {
p.Spec = pg.Spec
})
expectReconciled(t, reconciler, "", pg.Name)
pg.Status.Devices = pg.Status.Devices[:1] // truncate to only the first device.
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
})
t.Run("trigger_config_change_and_observe_new_config_hash", func(t *testing.T) {
pc.Spec.TailscaleConfig = &tsapi.TailscaleConfig{
AcceptRoutes: true,
}
mustUpdate(t, fc, "", pc.Name, func(p *tsapi.ProxyClass) {
p.Spec = pc.Spec
})
expectReconciled(t, reconciler, "", pg.Name)
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, "518a86e9fae64f270f8e0ec2a2ea6ca06c10f725035d3d6caca132cd61e42a74")
expectProxyGroupResources(t, fc, pg, true)
})
t.Run("delete_and_cleanup", func(t *testing.T) {
@@ -211,13 +191,13 @@ func TestProxyGroup(t *testing.T) {
})
}
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool, cfgHash string) {
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool) {
t.Helper()
role := pgRole(pg, tsNamespace)
roleBinding := pgRoleBinding(pg, tsNamespace)
serviceAccount := pgServiceAccount(pg, tsNamespace)
statefulSet, err := pgStatefulSet(pg, tsNamespace, testProxyImage, "auto", cfgHash)
statefulSet, err := pgStatefulSet(pg, tsNamespace, testProxyImage, "auto", "")
if err != nil {
t.Fatal(err)
}
@@ -227,7 +207,9 @@ func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.Prox
expectEqual(t, fc, role, nil)
expectEqual(t, fc, roleBinding, nil)
expectEqual(t, fc, serviceAccount, nil)
expectEqual(t, fc, statefulSet, nil)
expectEqual(t, fc, statefulSet, func(ss *appsv1.StatefulSet) {
ss.Spec.Template.Annotations[podAnnotationLastSetConfigFileHash] = ""
})
} else {
expectMissing[rbacv1.Role](t, fc, role.Namespace, role.Name)
expectMissing[rbacv1.RoleBinding](t, fc, roleBinding.Namespace, roleBinding.Name)
@@ -236,13 +218,11 @@ func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.Prox
}
var expectedSecrets []string
if shouldExist {
for i := range pgReplicas(pg) {
expectedSecrets = append(expectedSecrets,
fmt.Sprintf("%s-%d", pg.Name, i),
fmt.Sprintf("%s-%d-config", pg.Name, i),
)
}
for i := range pgReplicas(pg) {
expectedSecrets = append(expectedSecrets,
fmt.Sprintf("%s-%d", pg.Name, i),
fmt.Sprintf("%s-%d-config", pg.Name, i),
)
}
expectSecrets(t, fc, expectedSecrets)
}

View File

@@ -132,13 +132,10 @@ type tailscaleSTSConfig struct {
}
type connector struct {
// routes is a list of routes that this Connector should advertise either as a subnet router or as an app
// connector.
// routes is a list of subnet routes that this Connector should expose.
routes string
// isExitNode defines whether this Connector should act as an exit node.
isExitNode bool
// isAppConnector defines whether this Connector should act as an app connector.
isAppConnector bool
}
type tsnetServer interface {
CertDomains() []string
@@ -677,7 +674,7 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
}
if stsCfg != nil && pc.Spec.Metrics != nil && pc.Spec.Metrics.Enable {
if stsCfg.TailnetTargetFQDN == "" && stsCfg.TailnetTargetIP == "" && !stsCfg.ForwardClusterTrafficViaL7IngressProxy {
enableMetrics(ss)
enableMetrics(ss, pc)
} else if stsCfg.ForwardClusterTrafficViaL7IngressProxy {
// TODO (irbekrm): fix this
// For Ingress proxies that have been configured with
@@ -766,7 +763,7 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
return ss
}
func enableMetrics(ss *appsv1.StatefulSet) {
func enableMetrics(ss *appsv1.StatefulSet, pc *tsapi.ProxyClass) {
for i, c := range ss.Spec.Template.Spec.Containers {
if c.Name == "tailscale" {
// Serve metrics on on <pod-ip>:9001/debug/metrics. If
@@ -806,13 +803,11 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
Locked: "false",
Hostname: &stsC.Hostname,
NoStatefulFiltering: "false",
AppConnector: &ipn.AppConnectorPrefs{Advertise: false},
}
// For egress proxies only, we need to ensure that stateful filtering is
// not in place so that traffic from cluster can be forwarded via
// Tailscale IPs.
// TODO (irbekrm): set it to true always as this is now the default in core.
if stsC.TailnetTargetFQDN != "" || stsC.TailnetTargetIP != "" {
conf.NoStatefulFiltering = "true"
}
@@ -822,9 +817,6 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
return nil, fmt.Errorf("error calculating routes: %w", err)
}
conf.AdvertiseRoutes = routes
if stsC.Connector.isAppConnector {
conf.AppConnector.Advertise = true
}
}
if shouldAcceptRoutes(stsC.ProxyClass) {
conf.AcceptRoutes = "true"
@@ -839,15 +831,9 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
}
conf.AuthKey = key
}
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
capVerConfigs[107] = *conf
// AppConnector config option is only understood by clients of capver 107 and newer.
conf.AppConnector = nil
capVerConfigs[95] = *conf
// StatefulFiltering is only understood by clients of capver 95 and newer.
// legacy config should not contain NoStatefulFiltering field.
conf.NoStatefulFiltering.Clear()
capVerConfigs[94] = *conf
return capVerConfigs, nil

View File

@@ -48,7 +48,6 @@ type configOpts struct {
clusterTargetDNS string
subnetRoutes string
isExitNode bool
isAppConnector bool
confFileHash string
serveConfig *ipn.ServeConfig
shouldEnableForwardingClusterTrafficViaIngress bool
@@ -357,7 +356,6 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
AcceptRoutes: "false",
AppConnector: &ipn.AppConnectorPrefs{Advertise: false},
}
if opts.proxyClass != "" {
t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
@@ -372,9 +370,6 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
if opts.shouldRemoveAuthKey {
conf.AuthKey = nil
}
if opts.isAppConnector {
conf.AppConnector = &ipn.AppConnectorPrefs{Advertise: true}
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {
r := opts.subnetRoutes
@@ -389,29 +384,22 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
routes = append(routes, prefix)
}
}
conf.AdvertiseRoutes = routes
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
if opts.tailnetTargetFQDN != "" || opts.tailnetTargetIP != "" {
conf.NoStatefulFiltering = "true"
} else {
conf.NoStatefulFiltering = "false"
}
conf.AdvertiseRoutes = routes
bnn, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
conf.AppConnector = nil
bn, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
conf.NoStatefulFiltering.Clear()
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
mak.Set(&s.StringData, "tailscaled", string(b))
mak.Set(&s.StringData, "cap-95.hujson", string(bn))
mak.Set(&s.StringData, "cap-107.hujson", string(bnn))
labels := map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
@@ -686,17 +674,5 @@ func removeAuthKeyIfExistsModifier(t *testing.T) func(s *corev1.Secret) {
}
mak.Set(&secret.StringData, "cap-95.hujson", string(b))
}
if len(secret.StringData["cap-107.hujson"]) != 0 {
conf := &ipn.ConfigVAlpha{}
if err := json.Unmarshal([]byte(secret.StringData["cap-107.hujson"]), conf); err != nil {
t.Fatalf("error umarshalling 'cap-107.hujson' contents: %v", err)
}
conf.AuthKey = nil
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling 'cap-107.huson' contents: %v", err)
}
mak.Set(&secret.StringData, "cap-107.hujson", string(b))
}
}
}

39
cmd/lopower/README.md Normal file
View File

@@ -0,0 +1,39 @@
# 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`.

872
cmd/lopower/lopower.go Normal file
View File

@@ -0,0 +1,872 @@
// 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
}

1
cmd/lopower/lopower.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -67,7 +67,6 @@ 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+
@@ -75,7 +74,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/lineiter from tailscale.com/version/distro
tailscale.com/util/lineread 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,6 +5,10 @@ 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
@@ -82,7 +86,6 @@ 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
@@ -121,6 +124,7 @@ 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+
@@ -144,7 +148,6 @@ 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+
@@ -159,7 +162,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/lineiter from tailscale.com/hostinfo+
tailscale.com/util/lineread 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+
@@ -321,7 +324,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 tailscale.com+
runtime/debug from github.com/coder/websocket/internal/xsync+
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+

View File

@@ -79,6 +79,10 @@ 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+
@@ -245,7 +249,6 @@ 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+
@@ -324,6 +327,7 @@ 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
@@ -360,7 +364,6 @@ 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+
@@ -378,7 +381,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/lineiter from tailscale.com/hostinfo+
tailscale.com/util/lineread 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

@@ -1,30 +0,0 @@
// 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) && !ts_omit_ssh
//go:build linux || darwin || freebsd || openbsd
package main

View File

@@ -272,8 +272,8 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
name = p.Hostinfo().Hostname()
}
addrs := make([]string, p.Addresses().Len())
for i, ap := range p.Addresses().All() {
addrs[i] = ap.Addr().String()
for i := range p.Addresses().Len() {
addrs[i] = p.Addresses().At(i).Addr().String()
}
return jsNetMapPeerNode{
jsNetMapNode: jsNetMapNode{
@@ -589,8 +589,8 @@ func mapSlice[T any, M any](a []T, f func(T) M) []M {
func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
n := make([]M, a.Len())
for i, v := range a.All() {
n[i] = f(v)
for i := range a.Len() {
n[i] = f(a.At(i))
}
return n
}

View File

@@ -564,12 +564,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
case opt.URL != "":
// Nothing.
case regen || persist.PrivateNodeKey.IsZero():
if regen {
c.logf("TEST: need to regenerate")
} else {
c.logf("TEST: private node key is zero, persist is %v", persist)
c.logf("TEST: private node key is zero, persist is %v", persist)
}
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
tryingNewKey = key.NewNode()

View File

@@ -15,7 +15,7 @@ import (
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/control/controlhttp"
"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 := controlhttpserver.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
cbConn, err := controlhttp.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
if err != nil {
up.logf("controlhttp: Accept: %v", err)
return

View File

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

View File

@@ -12,7 +12,6 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/wsconn"
)
@@ -43,11 +42,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{
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
}.Encode(),
}
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
Subprotocols: []string{upgradeHeaderValue},
})
if err != nil {
return nil, err

View File

@@ -18,6 +18,15 @@ 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

@@ -1,15 +0,0 @@
// 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

@@ -23,15 +23,12 @@ 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"
@@ -161,7 +158,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
return err
}
}
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
conn, err := AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
if err != nil {
log.Print(err)
}
@@ -532,7 +529,7 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
func brokenMITMHandler(clock tstime.Clock) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
w.(http.Flusher).Flush()
@@ -577,7 +574,7 @@ func TestDialPlan(t *testing.T) {
close(done)
})
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, nil)
conn, err := AcceptHTTP(context.Background(), w, r, server, nil)
if err != nil {
log.Print(err)
} else {
@@ -819,14 +816,3 @@ 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

@@ -3,8 +3,7 @@
//go:build !ios
// Package controlhttpserver contains the HTTP server side of the ts2021 control protocol.
package controlhttpserver
package controlhttp
import (
"context"
@@ -19,7 +18,6 @@ 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"
@@ -47,12 +45,12 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != controlhttpcommon.UpgradeHeaderValue {
if next != upgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
}
initB64 := r.Header.Get(controlhttpcommon.HandshakeHeaderName)
initB64 := r.Header.Get(handshakeHeaderName)
if initB64 == "" {
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
return nil, errors.New("no tailscale handshake header in HTTP request")
@@ -69,7 +67,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", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
@@ -119,7 +117,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{controlhttpcommon.UpgradeHeaderValue},
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
@@ -131,7 +129,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() != controlhttpcommon.UpgradeHeaderValue {
if c.Subprotocol() != upgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
@@ -139,7 +137,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(controlhttpcommon.HandshakeHeaderName)
initB64 := r.Form.Get(handshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")

View File

@@ -313,9 +313,6 @@ 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
}
@@ -386,7 +383,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 canWebsockets && useWebsockets():
case useWebsockets():
var urlStr string
if c.url != nil {
urlStr = c.url.String()

View File

@@ -17,9 +17,7 @@ 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) {
@@ -487,23 +485,3 @@ 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 js || ((linux || darwin) && ts_debug_websockets)
//go:build linux || js
package derphttp
@@ -14,8 +14,6 @@ import (
"tailscale.com/net/wsconn"
)
const canWebsockets = true
func init() {
dialWebsocketFunc = dialWebsocket
}

View File

@@ -1,8 +0,0 @@
// 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 || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource {
return a
}
return ""

32
go.mod
View File

@@ -42,7 +42,7 @@ require (
github.com/golang/snappy v0.0.4
github.com/golangci/golangci-lint v1.57.1
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.20.2
github.com/google/go-containerregistry v0.18.0
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806
github.com/google/uuid v1.6.0
@@ -55,7 +55,7 @@ require (
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/jsimonetti/rtnetlink v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.11
github.com/klauspost/compress v1.17.4
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.20
@@ -80,7 +80,7 @@ require (
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1
@@ -100,8 +100,8 @@ require (
golang.org/x/mod v0.19.0
golang.org/x/net v0.27.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.9.0
golang.org/x/sys v0.27.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.22.0
golang.org/x/term v0.22.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.23.0
@@ -125,7 +125,7 @@ require (
github.com/Antonboom/testifylint v1.2.0 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
@@ -138,7 +138,7 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
github.com/dave/brenda v1.1.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghostiam/protogetter v0.3.5 // indirect
@@ -160,10 +160,10 @@ require (
github.com/ykadowak/zerologlint v0.1.5 // indirect
go-simpler.org/musttag v0.9.0 // indirect
go-simpler.org/sloglint v0.5.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
)
@@ -220,10 +220,10 @@ require (
github.com/daixiang0/gci v0.12.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/docker/cli v27.3.1+incompatible // indirect
github.com/docker/cli v25.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/docker v26.1.4+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ettle/strcase v0.2.0 // indirect
@@ -322,7 +322,7 @@ require (
github.com/nunnatsa/ginkgolinter v0.16.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
@@ -376,7 +376,7 @@ require (
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.1.0 // indirect
github.com/uudashr/gocognit v1.1.2 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect

72
go.sum
View File

@@ -79,8 +79,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
@@ -277,16 +277,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpFowZBX6GoQ=
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
@@ -490,8 +490,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-containerregistry v0.18.0 h1:ShE7erKNPqRh5ue6Z9DUOlk04WsnFWPO6YGr3OxnfoQ=
github.com/google/go-containerregistry v0.18.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -627,8 +627,8 @@ github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8=
github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -749,8 +749,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
@@ -931,8 +931,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10 h1:ZB47BgnHcEHQJODkDubs5ZiNeJxMhcgzefV3lykRwVQ=
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10/go.mod h1:iDx/0Rr9VV/KanSUDpJ6I/ROf0sQ7OqljXc/esl0UIA=
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba h1:uNo1VCm/xg4alMkIKo8RWTKNx5y1otfVOcKbp+irkL4=
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba/go.mod h1:DxnqIXBplij66U2ZkL688xy07q97qQ83P+TVueLiHq4=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
@@ -981,8 +981,8 @@ github.com/ultraware/whitespace v0.1.0 h1:O1HKYoh0kIeqE8sFqZf1o0qbORXUCOQFrlaQyZ
github.com/ultraware/whitespace v0.1.0/go.mod h1:/se4r3beMFNmewJ4Xmz0nMQ941GJt+qmSHGP9emHYe0=
github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI=
github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
@@ -1022,20 +1022,20 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
@@ -1176,8 +1176,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1239,8 +1239,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@@ -25,7 +25,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineiter"
"tailscale.com/util/lineread"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -231,12 +231,12 @@ func desktop() (ret opt.Bool) {
}
seenDesktop := false
for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
lineread.File("/proc/net/unix", func(line []byte) error {
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
}
for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
lineread.File("/proc/1/cgroup", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true)
break
return io.EOF // arbitrary non-nil error to stop loop
}
}
for lr := range lineiter.File("/proc/mounts") {
line, _ := lr.Value()
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true)
break
return io.EOF
}
}
return nil
})
return ret
}

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/types/ptr"
"tailscale.com/util/lineiter"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
@@ -106,18 +106,15 @@ func linuxVersionMeta() (meta versionMeta) {
}
m := map[string]string{}
for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
lineread.File(propFile, func(line []byte) error {
eq := bytes.IndexByte(line, '=')
if eq == -1 {
continue
return nil
}
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,8 +350,6 @@ 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

@@ -354,8 +354,9 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
// Check that the peer is allowed to share with us.
addresses := peer.Addresses()
for _, p := range addresses.All() {
capsMap := b.PeerCaps(p.Addr())
for i := range addresses.Len() {
addr := addresses.At(i)
capsMap := b.PeerCaps(addr.Addr())
if capsMap.HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
return true
}

View File

@@ -800,19 +800,6 @@ 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
@@ -1811,7 +1798,8 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap, lastSuggestedExitNod
}
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses().All() {
for i := range peer.Addresses().Len() {
addr := peer.Addresses().At(i)
if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
continue
}
@@ -1995,7 +1983,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
defer unlock()
if opts.UpdatePrefs != nil {
log.Printf("TESTPREFS: update prefs non-nil")
if err := b.checkPrefsLocked(opts.UpdatePrefs); err != nil {
return err
}
@@ -2062,10 +2049,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
prefs := b.pm.CurrentPrefs()
log.Printf("TESTPREFS persistent prefs: %v", prefs.Persist())
if s := prefs.Persist().AsStruct(); s != nil {
log.Printf("TESTPREFS persistent prefs private key is %v", s.PrivateNodeKey)
}
wantRunning := prefs.WantRunning()
if wantRunning {
if err := b.initMachineKeyLocked(); err != nil {
@@ -4211,7 +4194,11 @@ func (b *LocalBackend) authReconfig() {
disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC)
userDialUseRoutes := nm.HasCap(tailcfg.NodeAttrUserDialUseRoutes)
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.keyExpired, b.logf, version.OS())
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())
// If the current node is an app connector, ensure the app connector machine is started
b.reconfigAppConnectorLocked(nm, prefs)
b.mu.Unlock()
@@ -4311,7 +4298,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 bool, logf logger.Logf, versionOS string) *dns.Config {
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired, forceAAAA bool, logf logger.Logf, versionOS string) *dns.Config {
if nm == nil {
return nil
}
@@ -4374,7 +4361,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 {
if addr.Addr().Is6() && have4 && !forceAAAA {
continue
}
ips = append(ips, addr.Addr())
@@ -5001,8 +4988,8 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
case ipn.Running:
var addrStrs []string
addrs := netMap.GetAddresses()
for _, p := range addrs.All() {
addrStrs = append(addrStrs, p.Addr().String())
for i := range addrs.Len() {
addrStrs = append(addrStrs, addrs.At(i).Addr().String())
}
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrStrs, " "))
case ipn.NoState:
@@ -6093,7 +6080,8 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo().Services()
for _, s := range svcs.All() {
for i := range svcs.Len() {
s := svcs.At(i)
switch s.Proto {
case tailcfg.PeerAPI4:
p4 = s.Port
@@ -6125,7 +6113,8 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
var have4, have6 bool
addrs := nm.GetAddresses()
for _, a := range addrs.All() {
for i := range addrs.Len() {
a := addrs.At(i)
if !a.IsSingleIP() {
continue
}
@@ -6147,9 +6136,10 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
}
func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr {
for _, pfx := range n.Addresses().All() {
if pfx.IsSingleIP() && pred(pfx.Addr()) {
return pfx.Addr()
for i := range n.Addresses().Len() {
a := n.Addresses().At(i)
if a.IsSingleIP() && pred(a.Addr()) {
return a.Addr()
}
}
return netip.Addr{}
@@ -6379,8 +6369,8 @@ func peerCanProxyDNS(p tailcfg.NodeView) bool {
// If p.Cap is not populated (e.g. older control server), then do the old
// thing of searching through services.
services := p.Hostinfo().Services()
for _, s := range services.All() {
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
for i := range services.Len() {
if s := services.At(i); s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
return true
}
}
@@ -6783,7 +6773,6 @@ func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
// NewProfile creates and switches to the new profile.
func (b *LocalBackend) NewProfile() error {
log.Printf("TESTPREFS: NewProfile LB")
unlock := b.lockAndGetUnlock()
defer unlock()

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, t.Logf, "")
got := dnsConfigForNetmap(nm, peersMap(tc.peers), prefs.View(), false, false, t.Logf, "")
if !resolversEqual(t, got.DefaultResolvers, tc.wantDefaultResolvers) {
t.Errorf("DefaultResolvers: got %#v, want %#v", got.DefaultResolvers, tc.wantDefaultResolvers)
}
@@ -3041,10 +3041,12 @@ func deterministicNodeForTest(t testing.TB, want views.Slice[tailcfg.StableNodeI
var ret tailcfg.NodeView
gotIDs := make([]tailcfg.StableNodeID, got.Len())
for i, nv := range got.All() {
for i := range got.Len() {
nv := got.At(i)
if !nv.Valid() {
t.Fatalf("invalid node at index %v", i)
}
gotIDs[i] = nv.StableID()
if nv.StableID() == use {
ret = nv

View File

@@ -430,7 +430,8 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
}
bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
for _, stateID := range persist.DisallowedTKAStateIDs().All() {
for i := range persist.DisallowedTKAStateIDs().Len() {
stateID := persist.DisallowedTKAStateIDs().At(i)
if stateID == bootstrapStateID {
return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
}
@@ -571,7 +572,8 @@ func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
NodeKey: p.Key(),
}
for _, addr := range p.Addresses().All() {
for i := range p.Addresses().Len() {
addr := p.Addresses().At(i)
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
}

View File

@@ -9,7 +9,6 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"runtime"
"slices"
"strings"
@@ -204,7 +203,6 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
// Reset unloads the current profile, if any.
func (pm *profileManager) Reset() {
log.Printf("TESTPREFS: Reset")
pm.currentUserID = ""
pm.NewProfile()
}
@@ -217,7 +215,6 @@ func (pm *profileManager) Reset() {
// is logged into so that we can keep track of things like their domain name
// across user switches to disambiguate the same account but a different tailnet.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
log.Printf("TESTPREFS: SetPrefs with prefs %v", prefsIn)
cp := pm.currentProfile
if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" {
// We don't know anything about this profile, so ignore it for now.
@@ -226,7 +223,6 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
// Check if we already have an existing profile that matches the user/node.
if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 {
log.Printf("TESTPREFS: SetPrefs found existing profile")
// We already have a profile for this user/node we should reuse it. Also
// cleanup any other duplicate profiles.
cp = existing[0]
@@ -234,7 +230,6 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
for _, p := range existing {
// Clear the state.
if err := pm.store.WriteState(p.Key, nil); err != nil {
log.Printf("TESTPREFS: SetPrefs found existing profile, error writing state: %v", err)
// We couldn't delete the state, so keep the profile around.
continue
}
@@ -242,8 +237,6 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
// in [profileManager.setProfilePrefs] below.
delete(pm.knownProfiles, p.ID)
}
} else {
log.Printf("TESTPREFS: SetPrefs not found existing profile")
}
pm.currentProfile = cp
if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
@@ -334,7 +327,6 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
// profile, such as verifying the caller's access rights or checking
// if another profile for the same node already exists.
func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error {
log.Printf("TESTPREFS: setProfilePrefsNoPerm")
isCurrentProfile := pm.currentProfile == profile
if isCurrentProfile {
pm.prefs = clonedPrefs
@@ -431,7 +423,6 @@ func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, erro
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
// If the profile does not exist, it returns an [errProfileNotFound].
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
log.Printf("TESTPREFS: SwitchProfile")
metricSwitchProfile.Add(1)
kp, ok := pm.knownProfiles[id]
@@ -459,7 +450,6 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
// It creates a new one and switches to it if the current user does not have a default profile,
// or returns an error if the default profile is inaccessible or could not be loaded.
func (pm *profileManager) SwitchToDefaultProfile() error {
log.Printf("TESTPREFS: SwitchToDefault")
if id := pm.DefaultUserProfileID(pm.currentUserID); id != "" {
return pm.SwitchProfile(id)
}
@@ -557,7 +547,6 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
}
func (pm *profileManager) deleteCurrentProfile() error {
log.Printf("TESTPREFS: deleteCurrent")
if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
return err
}
@@ -638,7 +627,6 @@ func (pm *profileManager) NewProfile() {
// NewProfileForUser is like [profileManager.NewProfile], but it switches to the
// specified user and sets that user as the profile owner for the new profile.
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
log.Printf("TESTPREFS: NewProfileForUser")
pm.currentUserID = uid
metricNewProfile.Add(1)
@@ -653,7 +641,6 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
// newly created profile immediately. It returns the newly created profile on success,
// or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) {
log.Printf("TESTPREFS: newProfileWithPrefs")
metricNewProfile.Add(1)
profile := &ipn.LoginProfile{LocalUserID: uid}
@@ -746,7 +733,6 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
if err != nil {
return nil, err
}
log.Printf("TESTPREFS: newProfileWithGOOS with state key %v", stateKey)
knownProfiles, err := readKnownProfiles(store)
if err != nil {
@@ -762,15 +748,12 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
}
if stateKey != "" {
log.Printf("TESTPREFS: state key %v exists", stateKey)
for _, v := range knownProfiles {
log.Printf("TESTPREFS: state key %v exists looking at matching profile %s", stateKey, v)
if v.Key == stateKey {
pm.currentProfile = v
}
}
if pm.currentProfile == nil {
log.Printf("TESTPREFS: current profile is nil")
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf)
}
@@ -793,14 +776,12 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
// uid passed in from the unix tests. The uid's used for Windows tests
// and runtime must be valid Windows security identifier structures.
} else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" {
log.Printf("TESTPREFS: no known profiles")
// No known profiles, try a migration.
pm.dlogf("no known profiles; trying to migrate from legacy prefs")
if _, err := pm.migrateFromLegacyPrefs(pm.currentUserID, true); err != nil {
return nil, err
}
} else {
log.Printf("TESTPREFS: newProfileWithGOOS new profile")
pm.NewProfile()
}

View File

@@ -242,7 +242,8 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
}
addrs := nm.GetAddresses()
for _, a := range addrs.All() {
for i := range addrs.Len() {
a := addrs.At(i)
for _, p := range ports {
addrPort := netip.AddrPortFrom(a.Addr(), p)
if _, ok := b.serveListeners[addrPort]; ok {

View File

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

View File

@@ -121,8 +121,8 @@ func (b *LocalBackend) updateWebClientListenersLocked() {
}
addrs := b.netMap.GetAddresses()
for _, pfx := range addrs.All() {
addrPort := netip.AddrPortFrom(pfx.Addr(), webClientPort)
for i := range addrs.Len() {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort)
if _, ok := b.webClientListeners[addrPort]; ok {
continue // already listening
}

View File

@@ -100,7 +100,6 @@ 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,
@@ -953,22 +952,6 @@ 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

@@ -7,7 +7,6 @@ package kubestore
import (
"context"
"fmt"
"log"
"net"
"os"
"strings"
@@ -143,7 +142,6 @@ func (s *Store) loadState() error {
}
return err
}
log.Printf("TEST: kube store: got secret: %#+v", secret.Data)
s.memory.LoadFromMap(secret.Data)
return nil
}

View File

@@ -7,7 +7,6 @@ package mem
import (
"bytes"
"encoding/json"
"log"
"sync"
xmaps "golang.org/x/exp/maps"
@@ -33,21 +32,18 @@ func (s *Store) String() string { return "mem.Store" }
// ReadState implements the StateStore interface.
// It returns ipn.ErrStateNotExist if the state does not exist.
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
log.Printf("TEST: ReadState key %v ", id)
s.mu.Lock()
defer s.mu.Unlock()
bs, ok := s.cache[id]
if !ok {
return nil, ipn.ErrStateNotExist
}
log.Printf("TEST: ReadState key %v val %v", id, string(bs))
return bs, nil
}
// WriteState implements the StateStore interface.
// It never returns an error.
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
log.Printf("TEST: WriteState key %v ", id)
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
@@ -61,12 +57,10 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
// Any existing content is cleared, and the provided map is
// copied into the cache.
func (s *Store) LoadFromMap(m map[string][]byte) {
log.Printf("Store: LoadFromMap")
s.mu.Lock()
defer s.mu.Unlock()
xmaps.Clear(s.cache)
for k, v := range m {
log.Printf("TEST: setting state key %v %+#v", k, string(v))
mak.Set(&s.cache, ipn.StateKey(k), v)
}
return

View File

@@ -21,22 +21,6 @@
#### AppConnector
AppConnector defines a Tailscale app connector node configured via Connector.
_Appears in:_
- [ConnectorSpec](#connectorspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `routes` _[Routes](#routes)_ | Routes are optional preconfigured routes for the domains routed via the app connector.<br />If not set, routes for the domains will be discovered dynamically.<br />If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may<br />also dynamically discover other routes.<br />https://tailscale.com/kb/1332/apps-best-practices#preconfiguration | | Format: cidr <br />MinItems: 1 <br />Type: string <br /> |
#### Connector
@@ -102,9 +86,8 @@ _Appears in:_
| `tags` _[Tags](#tags)_ | Tags that the Tailscale node will be tagged with.<br />Defaults to [tag:k8s].<br />To autoapprove the subnet routes or exit node defined by a Connector,<br />you can configure Tailscale ACLs to give these tags the necessary<br />permissions.<br />See https://tailscale.com/kb/1337/acl-syntax#autoapprovers.<br />If you specify custom tags here, you must also make the operator an owner of these tags.<br />See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator.<br />Tags cannot be changed once a Connector node has been created.<br />Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$. | | Pattern: `^tag:[a-zA-Z][a-zA-Z0-9-]*$` <br />Type: string <br /> |
| `hostname` _[Hostname](#hostname)_ | Hostname is the tailnet hostname that should be assigned to the<br />Connector node. If unset, hostname defaults to <connector<br />name>-connector. Hostname can contain lower case letters, numbers and<br />dashes, it must not start or end with a dash and must be between 2<br />and 63 characters long. | | Pattern: `^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$` <br />Type: string <br /> |
| `proxyClass` _string_ | ProxyClass is the name of the ProxyClass custom resource that<br />contains configuration options that should be applied to the<br />resources created for this Connector. If unset, the operator will<br />create resources with the default configuration. | | |
| `subnetRouter` _[SubnetRouter](#subnetrouter)_ | SubnetRouter defines subnet routes that the Connector device should<br />expose to tailnet as a Tailscale subnet router.<br />https://tailscale.com/kb/1019/subnets/<br />If this field is unset, the device does not get configured as a Tailscale subnet router.<br />This field is mutually exclusive with the appConnector field. | | |
| `appConnector` _[AppConnector](#appconnector)_ | AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is<br />configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the<br />Connector does not act as an app connector.<br />Note that you will need to manually configure the permissions and the domains for the app connector via the<br />Admin panel.<br />Note also that the main tested and supported use case of this config option is to deploy an app connector on<br />Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose<br />cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have<br />tested or optimised for.<br />If you are using the app connector to access SaaS applications because you need a predictable egress IP that<br />can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows<br />via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT<br />device with a static IP address.<br />https://tailscale.com/kb/1281/app-connectors | | |
| `exitNode` _boolean_ | ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.<br />This field is mutually exclusive with the appConnector field.<br />https://tailscale.com/kb/1103/exit-nodes | | |
| `subnetRouter` _[SubnetRouter](#subnetrouter)_ | SubnetRouter defines subnet routes that the Connector node should<br />expose to tailnet. If unset, none are exposed.<br />https://tailscale.com/kb/1019/subnets/ | | |
| `exitNode` _boolean_ | ExitNode defines whether the Connector node should act as a<br />Tailscale exit node. Defaults to false.<br />https://tailscale.com/kb/1103/exit-nodes | | |
#### ConnectorStatus
@@ -123,7 +106,6 @@ _Appears in:_
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#condition-v1-meta) array_ | List of status conditions to indicate the status of the Connector.<br />Known condition types are `ConnectorReady`. | | |
| `subnetRoutes` _string_ | SubnetRoutes are the routes currently exposed to tailnet via this<br />Connector instance. | | |
| `isExitNode` _boolean_ | IsExitNode is set to true if the Connector acts as an exit node. | | |
| `isAppConnector` _boolean_ | IsAppConnector is set to true if the Connector acts as an app connector. | | |
| `tailnetIPs` _string array_ | TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6)<br />assigned to the Connector node. | | |
| `hostname` _string_ | Hostname is the fully qualified domain name of the Connector node.<br />If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the<br />node. | | |
@@ -764,7 +746,6 @@ _Validation:_
- Type: string
_Appears in:_
- [AppConnector](#appconnector)
- [SubnetRouter](#subnetrouter)

View File

@@ -22,7 +22,6 @@ var ConnectorKind = "Connector"
// +kubebuilder:resource:scope=Cluster,shortName=cn
// +kubebuilder:printcolumn:name="SubnetRoutes",type="string",JSONPath=`.status.subnetRoutes`,description="CIDR ranges exposed to tailnet by a subnet router defined via this Connector instance."
// +kubebuilder:printcolumn:name="IsExitNode",type="string",JSONPath=`.status.isExitNode`,description="Whether this Connector instance defines an exit node."
// +kubebuilder:printcolumn:name="IsAppConnector",type="string",JSONPath=`.status.isAppConnector`,description="Whether this Connector instance is an app connector."
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type == "ConnectorReady")].reason`,description="Status of the deployed Connector resources."
// Connector defines a Tailscale node that will be deployed in the cluster. The
@@ -56,8 +55,7 @@ type ConnectorList struct {
}
// ConnectorSpec describes a Tailscale node to be deployed in the cluster.
// +kubebuilder:validation:XValidation:rule="has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)",message="A Connector needs to have at least one of exit node, subnet router or app connector configured."
// +kubebuilder:validation:XValidation:rule="!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))",message="The appConnector field is mutually exclusive with exitNode and subnetRouter fields."
// +kubebuilder:validation:XValidation:rule="has(self.subnetRouter) || self.exitNode == true",message="A Connector needs to be either an exit node or a subnet router, or both."
type ConnectorSpec struct {
// Tags that the Tailscale node will be tagged with.
// Defaults to [tag:k8s].
@@ -84,31 +82,13 @@ type ConnectorSpec struct {
// create resources with the default configuration.
// +optional
ProxyClass string `json:"proxyClass,omitempty"`
// SubnetRouter defines subnet routes that the Connector device should
// expose to tailnet as a Tailscale subnet router.
// SubnetRouter defines subnet routes that the Connector node should
// expose to tailnet. If unset, none are exposed.
// https://tailscale.com/kb/1019/subnets/
// If this field is unset, the device does not get configured as a Tailscale subnet router.
// This field is mutually exclusive with the appConnector field.
// +optional
SubnetRouter *SubnetRouter `json:"subnetRouter,omitempty"`
// AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
// configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
// Connector does not act as an app connector.
// Note that you will need to manually configure the permissions and the domains for the app connector via the
// Admin panel.
// Note also that the main tested and supported use case of this config option is to deploy an app connector on
// Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
// cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
// tested or optimised for.
// If you are using the app connector to access SaaS applications because you need a predictable egress IP that
// can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
// via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
// device with a static IP address.
// https://tailscale.com/kb/1281/app-connectors
// +optional
AppConnector *AppConnector `json:"appConnector,omitempty"`
// ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
// This field is mutually exclusive with the appConnector field.
SubnetRouter *SubnetRouter `json:"subnetRouter"`
// ExitNode defines whether the Connector node should act as a
// Tailscale exit node. Defaults to false.
// https://tailscale.com/kb/1103/exit-nodes
// +optional
ExitNode bool `json:"exitNode"`
@@ -124,17 +104,6 @@ type SubnetRouter struct {
AdvertiseRoutes Routes `json:"advertiseRoutes"`
}
// AppConnector defines a Tailscale app connector node configured via Connector.
type AppConnector struct {
// Routes are optional preconfigured routes for the domains routed via the app connector.
// If not set, routes for the domains will be discovered dynamically.
// If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
// also dynamically discover other routes.
// https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
// +optional
Routes Routes `json:"routes"`
}
type Tags []Tag
func (tags Tags) Stringify() []string {
@@ -187,9 +156,6 @@ type ConnectorStatus struct {
// IsExitNode is set to true if the Connector acts as an exit node.
// +optional
IsExitNode bool `json:"isExitNode"`
// IsAppConnector is set to true if the Connector acts as an app connector.
// +optional
IsAppConnector bool `json:"isAppConnector"`
// TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6)
// assigned to the Connector node.
// +optional

View File

@@ -13,26 +13,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppConnector) DeepCopyInto(out *AppConnector) {
*out = *in
if in.Routes != nil {
in, out := &in.Routes, &out.Routes
*out = make(Routes, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConnector.
func (in *AppConnector) DeepCopy() *AppConnector {
if in == nil {
return nil
}
out := new(AppConnector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Connector) DeepCopyInto(out *Connector) {
*out = *in
@@ -105,11 +85,6 @@ func (in *ConnectorSpec) DeepCopyInto(out *ConnectorSpec) {
*out = new(SubnetRouter)
(*in).DeepCopyInto(*out)
}
if in.AppConnector != nil {
in, out := &in.AppConnector, &out.AppConnector
*out = new(AppConnector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorSpec.

View File

@@ -5,14 +5,12 @@ 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"
AppProxyGroupEgress = "k8s-operator-proxygroup-egress"
AppProxyGroupIngress = "k8s-operator-proxygroup-ingress"
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"
// Clientmetrics for Tailscale Kubernetes Operator components
MetricIngressProxyCount = "k8s_ingress_proxies" // L3
@@ -21,10 +19,8 @@ const (
MetricConnectorResourceCount = "k8s_connector_resources"
MetricConnectorWithSubnetRouterCount = "k8s_connector_subnetrouter_resources"
MetricConnectorWithExitNodeCount = "k8s_connector_exitnode_resources"
MetricConnectorWithAppConnectorCount = "k8s_connector_appconnector_resources"
MetricNameserverCount = "k8s_nameserver_resources"
MetricRecorderCount = "k8s_recorder_resources"
MetricEgressServiceCount = "k8s_egress_service_resources"
MetricProxyGroupEgressCount = "k8s_proxygroup_egress_resources"
MetricProxyGroupIngressCount = "k8s_proxygroup_ingress_resources"
MetricProxyGroupCount = "k8s_proxygroup_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/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/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/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,7 +63,9 @@ 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
config *Config
forceAAAA bool // whether client wants MagicDNS AAAA even if unsure of host's IPv6 status
forceFallbackResolvers []*dnstype.Resolver
}
// NewManagers created a new manager from the given config.
@@ -128,6 +130,28 @@ 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.
@@ -146,6 +170,10 @@ 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

@@ -82,8 +82,8 @@ func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool
pathForTest("bart")
// Built a bart table.
t := &bart.Table[struct{}]{}
for _, p := range addrs.All() {
t.Insert(p, struct{}{})
for i := range addrs.Len() {
t.Insert(addrs.At(i), struct{}{})
}
return bartLookup(t)
}
@@ -99,8 +99,8 @@ func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool
// General case:
pathForTest("ip-map")
m := set.Set[netip.Addr]{}
for _, p := range addrs.All() {
m.Add(p.Addr())
for i := range addrs.Len() {
m.Add(addrs.At(i).Addr())
}
return ipInMap(m)
}

View File

@@ -5,6 +5,7 @@ package netmon
import (
"bytes"
"errors"
"log"
"net/netip"
"os/exec"
@@ -14,7 +15,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineiter"
"tailscale.com/util/lineread"
)
var (
@@ -33,6 +34,11 @@ 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:
@@ -48,42 +54,44 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
err := lineread.File(procNetRoutePath, func(line []byte) error {
lineNum++
if lineNum == 1 {
// Skip header line.
continue
return nil
}
if lineNum > maxProcNetRouteRead {
break
return errStopReading
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
continue
return nil
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
continue // ignore error, skip line and keep going
return nil // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
continue
return nil
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
continue // ignore error, skip line and keep going
return nil // 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
break
return errStopReading
}
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
@@ -136,26 +144,23 @@ 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 "
for lr := range lineiter.Reader(out) {
line, err := lr.Value()
if err != nil {
break
}
lineread.Reader(out, func(line []byte) error {
const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
continue
return nil
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
continue
return nil
}
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,13 +4,14 @@
package netmon
import (
"errors"
"io"
"net/netip"
"os/exec"
"testing"
"go4.org/mem"
"tailscale.com/util/lineiter"
"tailscale.com/util/lineread"
"tailscale.com/version"
)
@@ -72,34 +73,31 @@ 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
for lr := range lineiter.Reader(stdout) {
lineb, err := lr.Value()
if err != nil {
break
}
lineread.Reader(stdout, func(lineb []byte) error {
line := mem.B(lineb)
if !mem.Contains(line, mem.S("default")) {
continue
return nil
}
f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") {
continue
return nil
}
ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) {
continue
return nil
}
if mem.Contains(flagsm, mem.S("I")) {
continue
return nil
}
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.
break
return errStopReadingNetstatTable
}
}
return nil
})
return ret, netif, ret.IsValid()
}
@@ -112,3 +110,5 @@ 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/lineiter"
"tailscale.com/util/lineread"
)
func init() {
@@ -32,6 +32,11 @@ 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:
@@ -47,42 +52,44 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
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
}
err := lineread.File(procNetRoutePath, func(line []byte) error {
lineNum++
if lineNum == 1 {
// Skip header line.
continue
return nil
}
if lineNum > maxProcNetRouteRead {
break
return errStopReading
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
continue
return nil
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
continue // ignore error, skip line and keep going
return nil // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
continue
return nil
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
continue // ignore error, skip line and keep going
return nil // 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
break
return errStopReading
}
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,8 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package netmon
import (

View File

@@ -180,7 +180,8 @@ func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() }
// IPv6 /0 route.
func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
var v4, v6 bool
for _, r := range rr.All() {
for i := range rr.Len() {
r := rr.At(i)
if r == allIPv4 {
v4 = true
} else if r == allIPv6 {
@@ -193,8 +194,8 @@ func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
// ContainsExitRoute reports whether rr contains at least one of IPv4 or
// IPv6 /0 (exit) routes.
func ContainsExitRoute(rr views.Slice[netip.Prefix]) bool {
for _, r := range rr.All() {
if r.Bits() == 0 {
for i := range rr.Len() {
if rr.At(i).Bits() == 0 {
return true
}
}
@@ -204,8 +205,8 @@ func ContainsExitRoute(rr views.Slice[netip.Prefix]) bool {
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
// Routes other than ExitNode Routes.
func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool {
for _, r := range rr.All() {
if r.Bits() != 0 {
for i := range rr.Len() {
if rr.At(i).Bits() != 0 {
return true
}
}

View File

@@ -42,8 +42,8 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if dnsname.HasSuffix(nm.Name, suffix) {
ret[canonMapKey(dnsname.TrimSuffix(nm.Name, suffix))] = ip
}
for _, p := range addrs.All() {
if p.Addr().Is4() {
for i := range addrs.Len() {
if addrs.At(i).Addr().Is4() {
have4 = true
}
}
@@ -52,8 +52,9 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if p.Name() == "" {
continue
}
for _, pfx := range p.Addresses().All() {
ip := pfx.Addr()
for i := range p.Addresses().Len() {
a := p.Addresses().At(i)
ip := a.Addr()
if ip.Is4() && !have4 {
continue
}

View File

@@ -17,7 +17,7 @@ import (
"sync"
"time"
"tailscale.com/util/lineiter"
"tailscale.com/util/lineread"
)
// These vars are overridden for tests.
@@ -76,22 +76,21 @@ func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
cfg := map[string]string{}
for lr := range lineiter.Reader(r) {
line, err := lr.Value()
if err != nil {
return nil, nil, err
}
if err := lineread.Reader(r, func(line []byte) error {
// accept and skip over empty lines
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
return nil
}
key, value, ok := strings.Cut(string(line), "=")
if !ok {
return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
return 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,7 +861,22 @@ 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
@@ -871,7 +886,7 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf
filt = t.filter.Load()
}
if filt == nil {
return filter.Drop, gro
return filter.Drop
}
if filt.RunOut(p, t.filterFlags) != filter.Accept {
@@ -879,15 +894,9 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf
t.metrics.outboundDroppedPacketsTotal.Add(usermetric.DropLabels{
Reason: usermetric.ReasonACL,
}, 1)
return filter.Drop, gro
return filter.Drop
}
if t.PostFilterPacketOutboundToWireGuard != nil {
if res := t.PostFilterPacketOutboundToWireGuard(p, t); res.IsDrop() {
return res, gro
}
}
return filter.Accept, gro
return filter.Accept
}
// noteActivity records that there was a read or write at the current time.
@@ -1051,6 +1060,11 @@ 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,7 +71,6 @@ package safeweb
import (
"cmp"
"context"
crand "crypto/rand"
"fmt"
"log"
@@ -417,7 +416,3 @@ 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/lineiter"
"tailscale.com/util/lineread"
"tailscale.com/util/must"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
@@ -1123,11 +1123,14 @@ func TestSSH(t *testing.T) {
func parseEnv(out []byte) map[string]string {
e := map[string]string{}
for line := range lineiter.Bytes(out) {
if i := bytes.IndexByte(line, '='); i != -1 {
e[string(line[:i])] = string(line[i+1:])
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
i := bytes.IndexByte(line, '=')
if i == -1 {
return nil
}
}
e[string(line[:i])] = string(line[i+1:])
return nil
})
return e
}

View File

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

View File

@@ -121,6 +121,11 @@ 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
@@ -433,7 +438,8 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
return
}
addrs := nm.GetAddresses()
for _, addr := range addrs.All() {
for i := range addrs.Len() {
addr := addrs.At(i)
ip := addr.Addr()
if ip.Is6() {
ip6 = ip
@@ -615,6 +621,13 @@ 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,19 +13,14 @@ 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
WantDeps set.Set[string] // packages expected
Tags string // comma-separated
GOOS string // optional
GOARCH string // optional
BadDeps map[string]string // package => why
}
func (c DepChecker) Check(t *testing.T) {
@@ -34,7 +29,7 @@ func (c DepChecker) Check(t *testing.T) {
t.Skip("skipping dep tests on windows hosts")
}
t.Helper()
cmd := exec.Command("go", "list", "-json", "-tags="+c.Tags, ".")
cmd := exec.Command("go", "list", "-json", ".")
var extraEnv []string
if c.GOOS != "" {
extraEnv = append(extraEnv, "GOOS="+c.GOOS)
@@ -59,11 +54,6 @@ 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/controlhttpserver"
"tailscale.com/control/controlhttp"
"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 := controlhttpserver.AcceptHTTP(ctx, w, r, noisePrivate, nil)
cc, err := controlhttp.AcceptHTTP(ctx, w, r, noisePrivate, nil)
if err != nil {
log.Printf("AcceptHTTP: %v", err)
return

View File

@@ -23,16 +23,10 @@ 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", StaticStringVar(version.Long()))
expvar.Publish("go_version", StaticStringVar(runtime.Version()))
expvar.Publish("version", expvar.Func(func() any { return version.Long() }))
expvar.Publish("go_version", expvar.Func(func() any { return 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() }))
}

View File

@@ -279,14 +279,15 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
// in nodeConciseEqual in sync.
func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
aip := make([]string, p.AllowedIPs().Len())
for i, a := range p.AllowedIPs().All() {
s := strings.TrimSuffix(a.String(), "/32")
for i := range aip {
a := p.AllowedIPs().At(i)
s := strings.TrimSuffix(fmt.Sprint(a), "/32")
aip[i] = s
}
epStrs := make([]string, p.Endpoints().Len())
for i, ep := range p.Endpoints().All() {
e := ep.String()
ep := make([]string, p.Endpoints().Len())
for i := range ep {
e := p.Endpoints().At(i).String()
// Align vertically on the ':' between IP and port
colon := strings.IndexByte(e, ':')
spaces := 0
@@ -294,7 +295,7 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
spaces++
colon--
}
epStrs[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
}
derp := p.DERP()
@@ -315,7 +316,7 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
discoShort,
derp,
strings.Join(aip, " "),
strings.Join(epStrs, " "))
strings.Join(ep, " "))
}
// nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise.

View File

@@ -1,49 +0,0 @@
// 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
}

View File

@@ -277,16 +277,11 @@ func IsInvalid(t types.Type) bool {
// It has special handling for some types that contain pointers
// that we know are free from memory aliasing/mutation concerns.
func ContainsPointers(typ types.Type) bool {
s := typ.String()
switch s {
switch typ.String() {
case "time.Time":
// time.Time contains a pointer that does not need cloning.
// time.Time contains a pointer that does not need copying
return false
case "inet.af/netip.Addr":
return false
}
if strings.HasPrefix(s, "unique.Handle[") {
// unique.Handle contains a pointer that does not need cloning.
case "inet.af/netip.Addr", "net/netip.Addr", "net/netip.Prefix", "net/netip.AddrPort":
return false
}
switch ft := typ.Underlying().(type) {

View File

@@ -10,8 +10,6 @@ import (
"strings"
"sync"
"testing"
"time"
"unique"
"unsafe"
"golang.org/x/exp/constraints"
@@ -86,16 +84,6 @@ type PointerUnionParam[T netip.Prefix | BasicType | IntPtr] struct {
V T
}
type StructWithUniqueHandle struct{ _ unique.Handle[[32]byte] }
type StructWithTime struct{ _ time.Time }
type StructWithNetipTypes struct {
_ netip.Addr
_ netip.AddrPort
_ netip.Prefix
}
type Interface interface {
Method()
}
@@ -173,18 +161,6 @@ func TestGenericContainsPointers(t *testing.T) {
typ: "PointerUnionParam",
wantPointer: true,
},
{
typ: "StructWithUniqueHandle",
wantPointer: false,
},
{
typ: "StructWithTime",
wantPointer: false,
},
{
typ: "StructWithNetipTypes",
wantPointer: false,
},
}
for _, tt := range tests {

View File

@@ -1,72 +0,0 @@
// 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

@@ -1,32 +0,0 @@
// 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/lineiter"
"tailscale.com/util/lineread"
)
func ownerOfPID(pid int) (userID string, err error) {
file := fmt.Sprintf("/proc/%d/status", pid)
for lr := range lineiter.File(file) {
line, err := lr.Value()
if err != nil {
if os.IsNotExist(err) {
return "", ErrProcessNotFound
}
return "", err
}
err = lineread.File(file, func(line []byte) error {
if len(line) < 4 || string(line[:4]) != "Uid:" {
continue
return nil
}
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

@@ -67,7 +67,7 @@ func (ss *Slice[T]) Add(vs ...T) {
// AddSlice adds all elements in vs to the set.
func (ss *Slice[T]) AddSlice(vs views.Slice[T]) {
for _, v := range vs.All() {
ss.Add(v)
for i := range vs.Len() {
ss.Add(vs.At(i))
}
}

View File

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

View File

@@ -102,7 +102,8 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
peers := map[key.NodePublic]tailcfg.NodeView{}
for _, p := range c.peers.All() {
for i := range c.peers.Len() {
p := c.peers.At(i)
peers[p.Key()] = p
}

View File

@@ -9,7 +9,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"iter"
"math"
"math/rand/v2"
"net"
@@ -1385,18 +1384,20 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
}
func (de *endpoint) setEndpointsLocked(eps interface {
All() iter.Seq2[int, netip.AddrPort]
Len() int
At(i int) netip.AddrPort
}) {
for _, st := range de.endpointState {
st.index = indexSentinelDeleted // assume deleted until updated in next loop
}
var newIpps []netip.AddrPort
for i, ipp := range eps.All() {
for i := range eps.Len() {
if i > math.MaxInt16 {
// Seems unlikely.
break
}
ipp := eps.At(i)
if !ipp.IsValid() {
de.c.logf("magicsock: bogus netmap endpoint from %v", eps)
continue

View File

@@ -1120,8 +1120,8 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
// re-run.
eps = c.endpointTracker.update(time.Now(), eps)
for _, ep := range c.staticEndpoints.All() {
addAddr(ep, tailcfg.EndpointExplicitConf)
for i := range c.staticEndpoints.Len() {
addAddr(c.staticEndpoints.At(i), tailcfg.EndpointExplicitConf)
}
if localAddr := c.pconn4.LocalAddr(); localAddr.IP.IsUnspecified() {
@@ -2360,14 +2360,16 @@ func (c *Conn) logEndpointCreated(n tailcfg.NodeView) {
fmt.Fprintf(w, "derp=%v%s ", regionID, code)
}
for _, a := range n.AllowedIPs().All() {
for i := range n.AllowedIPs().Len() {
a := n.AllowedIPs().At(i)
if a.IsSingleIP() {
fmt.Fprintf(w, "aip=%v ", a.Addr())
} else {
fmt.Fprintf(w, "aip=%v ", a)
}
}
for _, ep := range n.Endpoints().All() {
for i := range n.Endpoints().Len() {
ep := n.Endpoints().At(i)
fmt.Fprintf(w, "ep=%v ", ep)
}
}))

View File

@@ -643,11 +643,13 @@ func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
newPfx := make(map[netip.Prefix]bool)
if selfNode.Valid() {
for _, p := range selfNode.Addresses().All() {
for i := range selfNode.Addresses().Len() {
p := selfNode.Addresses().At(i)
newPfx[p] = true
}
if ns.ProcessSubnets {
for _, p := range selfNode.AllowedIPs().All() {
for i := range selfNode.AllowedIPs().Len() {
p := selfNode.AllowedIPs().At(i)
newPfx[p] = true
}
}

View File

@@ -207,7 +207,8 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
ps, found := e.getPeerStatusLite(n.Key())
if !found {
onlyZeroRoute := true // whether peerForIP returned n only because its /0 route matched
for _, r := range n.AllowedIPs().All() {
for i := range n.AllowedIPs().Len() {
r := n.AllowedIPs().At(i)
if r.Bits() != 0 && r.Contains(flow.DstAddr()) {
onlyZeroRoute = false
break

View File

@@ -852,7 +852,8 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackNodes []key.NodePublic,
// hasOverlap checks if there is a IPPrefix which is common amongst the two
// provided slices.
func hasOverlap(aips, rips views.Slice[netip.Prefix]) bool {
for _, aip := range aips.All() {
for i := range aips.Len() {
aip := aips.At(i)
if views.SliceContains(rips, aip) {
return true
}
@@ -1328,9 +1329,9 @@ func (e *userspaceEngine) mySelfIPMatchingFamily(dst netip.Addr) (src netip.Addr
if addrs.Len() == 0 {
return zero, errors.New("no self address in netmap")
}
for _, p := range addrs.All() {
if p.IsSingleIP() && p.Addr().BitLen() == dst.BitLen() {
return p.Addr(), nil
for i := range addrs.Len() {
if a := addrs.At(i); a.IsSingleIP() && a.Addr().BitLen() == dst.BitLen() {
return a.Addr(), nil
}
}
return zero, errors.New("no self address in netmap matching address family")

View File

@@ -21,6 +21,7 @@ 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

@@ -40,7 +40,8 @@ func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool {
if !cidr.IsSingleIP() {
return true
}
for _, selfCIDR := range node.Addresses().All() {
for i := range node.Addresses().Len() {
selfCIDR := node.Addresses().At(i)
if cidr == selfCIDR {
return false
}
@@ -109,7 +110,8 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer()
cpeer.V6MasqAddr = peer.SelfNodeV6MasqAddrForThisPeer()
cpeer.IsJailed = peer.IsJailed()
for _, allowedIP := range peer.AllowedIPs().All() {
for i := range peer.AllowedIPs().Len() {
allowedIP := peer.AllowedIPs().At(i)
if allowedIP.Bits() == 0 && peer.StableID() != exitNode {
if didExitNodeWarn {
// Don't log about both the IPv4 /0 and IPv6 /0.

View File

@@ -124,7 +124,13 @@ func (cfg *Config) handleDeviceLine(k, value mem.RO, valueBytes []byte) error {
if err != nil {
return err
}
case k.EqualString("listen_port") || k.EqualString("fwmark"):
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"):
// ignore
default:
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())

View File

@@ -39,6 +39,7 @@ 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,6 +42,9 @@ 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 {
@@ -87,7 +90,9 @@ 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)
}
set("endpoint", p.PublicKey.UntypedHexString())
if cfg.NodeID != "" {
set("endpoint", p.PublicKey.UntypedHexString())
}
}
// TODO: replace_allowed_ips is expensive.