Compare commits
28 Commits
irbekrm/de
...
lp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bfccc5a49 | ||
|
|
1528c94a79 | ||
|
|
5bbff79412 | ||
|
|
3a3fc3e8ac | ||
|
|
2e70eef65c | ||
|
|
c0ca41daeb | ||
|
|
c2e1e9e68e | ||
|
|
510f0745ea | ||
|
|
44570dabda | ||
|
|
744e386d26 | ||
|
|
da517a04d2 | ||
|
|
9065c7fb00 | ||
|
|
6f97d4b26b | ||
|
|
d08c776848 | ||
|
|
ec8feb6aad | ||
|
|
7fdcab4dec | ||
|
|
cc326ea820 | ||
|
|
0e39594ef1 | ||
|
|
b1d8872d10 | ||
|
|
406bcadc65 | ||
|
|
1a45fae577 | ||
|
|
0fec73082a | ||
|
|
e1f6f6f8c2 | ||
|
|
a4b4656879 | ||
|
|
522124c5a7 | ||
|
|
cf45d98caa | ||
|
|
1f9b619588 | ||
|
|
2b4594c3ff |
@@ -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
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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>
|
||||
`))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
39
cmd/lopower/README.md
Normal 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
872
cmd/lopower/lopower.go
Normal 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
1
cmd/lopower/lopower.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 114 KiB |
@@ -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+
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
32
go.mod
@@ -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
72
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !android
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user