From b048ba2b308bbe8ab5718f35eea71c256716ed6c Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Tue, 31 Jan 2023 22:03:25 -0800 Subject: [PATCH] cmd/containerboot: add support for setting funnel TCP portforward WIP Signed-off-by: Maisem Ali --- cmd/containerboot/main.go | 76 +++++++++++++++++++++++++------ cmd/k8s-operator/operator.go | 13 ++++-- ipn/ipnlocal/profiles.go | 2 +- ipn/store/kubestore/store_kube.go | 3 ++ 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index b074d9fb3..530abbfd3 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -90,6 +90,29 @@ func main() { AuthOnce: defaultBool("TS_AUTH_ONCE", false), Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"), } + funnelForwardPorts := strings.Split(defaultEnv("TS_FUNNEL_TCP_PORTFORWARD", ""), ",") + if len(funnelForwardPorts) > 0 { + ffp := make(map[uint16]uint16) + for _, p := range funnelForwardPorts { + if p == "" { + continue + } + from, to, ok := strings.Cut(p, ":") + if !ok { + log.Fatalf("TS_FUNNEL_TCP_PORTFORWARD: %q is not a valid port pair", p) + } + fp, err := strconv.ParseUint(from, 10, 16) + if err != nil { + log.Fatalf("TS_FUNNEL_TCP_PORTFORWARD: %v", err) + } + tp, err := strconv.ParseUint(to, 10, 16) + if err != nil { + log.Fatalf("TS_FUNNEL_TCP_PORTFORWARD: %v", err) + } + ffp[uint16(fp)] = uint16(tp) + } + cfg.FunnelTCPPorts = ffp + } if cfg.ProxyTo != "" && cfg.UserspaceMode { log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE") @@ -240,10 +263,8 @@ authLoop: } var ( - wantProxy = cfg.ProxyTo != "" wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch startupTasksDone = false - currentIPs deephash.Sum // tailscale IPs assigned to device currentDeviceInfo deephash.Sum // device ID and fqdn ) for { @@ -261,11 +282,6 @@ authLoop: log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State) } if n.NetMap != nil { - if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(¤tIPs, &n.NetMap.Addresses) { - if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil { - log.Fatalf("installing proxy rules: %v", err) - } - } deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name} if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(¤tDeviceInfo, &deviceInfo) { if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil { @@ -274,7 +290,10 @@ authLoop: } } if !startupTasksDone { - if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) { + if (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) { + if err := configureForwarding(ctx, client, cfg); err != nil { + log.Fatalf("configuring forwarding: %v", err) + } // This log message is used in tests to detect when all // post-auth configuration is done. log.Println("Startup complete, waiting for shutdown signal") @@ -305,6 +324,35 @@ authLoop: } } +func configureForwarding(ctx context.Context, client *tailscale.LocalClient, cfg *settings) error { + if cfg.ProxyTo == "" { + return nil + } + st, err := client.StatusWithoutPeers(ctx) + if err != nil { + return err + } + if len(cfg.FunnelTCPPorts) == 0 { + return installIPTablesRule(ctx, cfg.ProxyTo, st.Self.TailscaleIPs) + } + if len(st.CertDomains) == 0 { + return errors.New("no cert domains, cannot configure TCP forwarding") + } + cd := st.CertDomains[0] + sc := &ipn.ServeConfig{ + AllowFunnel: make(map[ipn.HostPort]bool), + TCP: make(map[uint16]*ipn.TCPPortHandler), + } + for f, t := range cfg.FunnelTCPPorts { + sc.TCP[f] = &ipn.TCPPortHandler{ + TCPForward: fmt.Sprintf("%s:%d", cfg.ProxyTo, t), + TerminateTLS: cd, + } + sc.AllowFunnel[ipn.HostPort(fmt.Sprintf("%s:%d", cd, f))] = true + } + return client.SetServeConfig(ctx, sc) +} + func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, int, error) { args := tailscaledArgs(cfg) sigCh := make(chan os.Signal, 1) @@ -488,7 +536,7 @@ func ensureIPForwarding(root, proxyTo, routes string) error { return nil } -func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error { +func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Addr) error { dst, err := netip.ParseAddr(dstStr) if err != nil { return err @@ -498,14 +546,11 @@ func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefi argv0 = "ip6tables" } var local string - for _, pfx := range tsIPs { - if !pfx.IsSingleIP() { + for _, addr := range tsIPs { + if addr.Is4() != dst.Is4() { continue } - if pfx.Addr().Is4() != dst.Is4() { - continue - } - local = pfx.Addr().String() + local = addr.String() break } if local == "" { @@ -529,6 +574,7 @@ type settings struct { Hostname string Routes string ProxyTo string + FunnelTCPPorts map[uint16]uint16 // from Tailscale port -> to ProxyTo port DaemonExtraArgs string ExtraArgs string InKubernetes bool diff --git a/cmd/k8s-operator/operator.go b/cmd/k8s-operator/operator.go index 3ef70539c..cb8c183d0 100644 --- a/cmd/k8s-operator/operator.go +++ b/cmd/k8s-operator/operator.go @@ -235,9 +235,10 @@ const ( FinalizerName = "tailscale.com/finalizer" - AnnotationExpose = "tailscale.com/expose" - AnnotationTags = "tailscale.com/tags" - AnnotationHostname = "tailscale.com/hostname" + AnnotationExpose = "tailscale.com/expose" + AnnotationTags = "tailscale.com/tags" + AnnotationHostname = "tailscale.com/hostname" + AnnotationFunnelPorts = "tailscale.com/funnel-tcp-portforward" ) // ServiceReconciler is a simple ControllerManagedBy example implementation. @@ -584,6 +585,12 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare Name: "TS_HOSTNAME", Value: hostname, }) + if len(parentSvc.Annotations[AnnotationFunnelPorts]) > 0 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: "TS_FUNNEL_TCP_PORTFORWARD", + Value: parentSvc.Annotations[AnnotationFunnelPorts], + }) + } ss.ObjectMeta = metav1.ObjectMeta{ Name: headlessSvc.Name, Namespace: a.operatorNamespace, diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index 55df0b60d..9450a9c5e 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -185,7 +185,7 @@ func init() { func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error { prefs := prefsIn.AsStruct().View() newPersist := prefs.Persist().AsStruct() - if newPersist == nil || newPersist.LoginName == "" { + if newPersist == nil || newPersist.NodeID == "" { return pm.setPrefsLocked(prefs) } up := newPersist.UserProfile diff --git a/ipn/store/kubestore/store_kube.go b/ipn/store/kubestore/store_kube.go index 0d3a01f7b..f4da8f8b2 100644 --- a/ipn/store/kubestore/store_kube.go +++ b/ipn/store/kubestore/store_kube.go @@ -7,6 +7,7 @@ package kubestore import ( "context" + "strings" "time" "tailscale.com/ipn" @@ -36,6 +37,7 @@ func (s *Store) String() string { return "kube.Store" } // ReadState implements the StateStore interface. func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) { + id = ipn.StateKey(strings.ReplaceAll(string(id), "/", "__")) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -55,6 +57,7 @@ func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) { // WriteState implements the StateStore interface. func (s *Store) WriteState(id ipn.StateKey, bs []byte) error { + id = ipn.StateKey(strings.ReplaceAll(string(id), "/", "__")) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()