From 16bc855d6c3a320977bfd88b71f42823fb55e127 Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Wed, 4 Dec 2024 16:23:24 +0100 Subject: [PATCH 1/4] Use netip.Addr instead of net.IP in DNS resolver. Signed-off-by: Thomas Hallgren --- pkg/client/rootd/dns/resolved_linux.go | 7 ++++--- pkg/client/rootd/dns/server_darwin.go | 5 +++-- pkg/client/rootd/dns/server_linux.go | 2 +- pkg/client/rootd/dns/server_windows.go | 4 ++-- pkg/client/rootd/session.go | 10 +++------- pkg/client/rootd/stream_creator.go | 9 +++++---- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/pkg/client/rootd/dns/resolved_linux.go b/pkg/client/rootd/dns/resolved_linux.go index b2667742fe..2f9bdd4fc1 100644 --- a/pkg/client/rootd/dns/resolved_linux.go +++ b/pkg/client/rootd/dns/resolved_linux.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "strings" "time" @@ -15,7 +16,7 @@ import ( "github.com/telepresenceio/telepresence/v2/pkg/vif" ) -func (s *Server) tryResolveD(c context.Context, dev vif.Device, configureDNS func(net.IP, *net.UDPAddr)) error { +func (s *Server) tryResolveD(c context.Context, dev vif.Device, configureDNS func(netip.Addr, *net.UDPAddr)) error { // Connect to ResolveD via DBUS. if !dbus.IsResolveDRunning(c) { dlog.Error(c, "systemd-resolved is not running") @@ -35,7 +36,7 @@ func (s *Server) tryResolveD(c context.Context, dev vif.Device, configureDNS fun return err } dnsIP := s.RemoteIP - configureDNS(dnsIP.AsSlice(), dnsResolverAddr) + configureDNS(dnsIP, dnsResolverAddr) g := dgroup.NewGroup(c, dgroup.GroupConfig{}) @@ -55,7 +56,7 @@ func (s *Server) tryResolveD(c context.Context, dev vif.Device, configureDNS fun c, cancel := context.WithTimeout(context.WithoutCancel(c), time.Second) defer cancel() dlog.Debugf(c, "Reverting Link settings for %s", dev.Name()) - configureDNS(nil, nil) // Don't route from TUN-device + configureDNS(netip.Addr{}, nil) // Don't route from TUN-device if err = dbus.RevertLink(c, int(dev.Index())); err != nil { dlog.Error(c, err) } diff --git a/pkg/client/rootd/dns/server_darwin.go b/pkg/client/rootd/dns/server_darwin.go index fde8cf1543..d300e62e89 100644 --- a/pkg/client/rootd/dns/server_darwin.go +++ b/pkg/client/rootd/dns/server_darwin.go @@ -3,6 +3,7 @@ package dns import ( "context" "net" + "net/netip" "os" "path/filepath" "strings" @@ -29,7 +30,7 @@ const ( // man 5 resolver // // or, if not on a Mac, follow this link: https://www.manpagez.com/man/5/resolver/ -func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net.IP, *net.UDPAddr)) error { +func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(netip.Addr, *net.UDPAddr)) error { resolverDirName := filepath.Join("/etc", "resolver") listener, err := newLocalUDPListener(c) @@ -40,7 +41,7 @@ func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net if err != nil { return err } - configureDNS(nil, dnsAddr) + configureDNS(netip.Addr{}, dnsAddr) err = os.MkdirAll(resolverDirName, 0o755) if err != nil { diff --git a/pkg/client/rootd/dns/server_linux.go b/pkg/client/rootd/dns/server_linux.go index acfdf5981a..0883822fd6 100644 --- a/pkg/client/rootd/dns/server_linux.go +++ b/pkg/client/rootd/dns/server_linux.go @@ -32,7 +32,7 @@ const ( var errResolveDNotConfigured = errors.New("resolved not configured") -func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net.IP, *net.UDPAddr)) error { +func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(netip.Addr, *net.UDPAddr)) error { if proc.RunningInContainer() { // Don't bother with systemd-resolved when running in a docker container return s.runOverridingServer(c, dev) diff --git a/pkg/client/rootd/dns/server_windows.go b/pkg/client/rootd/dns/server_windows.go index d00733eddc..754242852a 100644 --- a/pkg/client/rootd/dns/server_windows.go +++ b/pkg/client/rootd/dns/server_windows.go @@ -21,7 +21,7 @@ const ( recursionTestTimeout = 1500 * time.Millisecond ) -func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net.IP, *net.UDPAddr)) error { +func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(netip.Addr, *net.UDPAddr)) error { listener, err := newLocalUDPListener(c) if err != nil { return err @@ -30,7 +30,7 @@ func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net if err != nil { return err } - configureDNS(s.RemoteIP.AsSlice(), dnsAddr) + configureDNS(s.RemoteIP, dnsAddr) var pool FallbackPool if client.GetConfig(c).OSSpecific().Network.DNSWithFallback { diff --git a/pkg/client/rootd/session.go b/pkg/client/rootd/session.go index 6fadb3fd37..b3ccc6d45d 100644 --- a/pkg/client/rootd/session.go +++ b/pkg/client/rootd/session.go @@ -116,7 +116,7 @@ type Session struct { // used in conjunction with systemd-resolved. The current macOS and the overriding solution // will dispatch directly to the local DNS Service without going through the TUN device, but // that may change later if we decide to dispatch to the DNS-server in the cluster. - remoteDnsIP net.IP + remoteDnsIP netip.Addr // dnsLocalAddr is the address of the local DNS Service. dnsLocalAddr *net.UDPAddr @@ -513,11 +513,7 @@ func (s *Session) getNetworkConfig(ctx context.Context) *rpc.NetworkConfig { } else { d.LocalIP = netip.Addr{} } - if len(s.remoteDnsIP) > 0 { - d.RemoteIP, _ = netip.AddrFromSlice(s.remoteDnsIP) - } else { - d.RemoteIP = netip.Addr{} - } + d.RemoteIP = s.remoteDnsIP js, _ := client.MarshalJSON(mc) return &rpc.NetworkConfig{ @@ -526,7 +522,7 @@ func (s *Session) getNetworkConfig(ctx context.Context) *rpc.NetworkConfig { } } -func (s *Session) configureDNS(dnsIP net.IP, dnsLocalAddr *net.UDPAddr) { +func (s *Session) configureDNS(dnsIP netip.Addr, dnsLocalAddr *net.UDPAddr) { s.remoteDnsIP = dnsIP s.dnsLocalAddr = dnsLocalAddr } diff --git a/pkg/client/rootd/stream_creator.go b/pkg/client/rootd/stream_creator.go index 078bfa874b..4d37274584 100644 --- a/pkg/client/rootd/stream_creator.go +++ b/pkg/client/rootd/stream_creator.go @@ -17,8 +17,8 @@ import ( const dnsConnTTL = 5 * time.Second -func (s *Session) isForDNS(ip net.IP, port uint16) bool { - return s.remoteDnsIP != nil && port == 53 && s.remoteDnsIP.Equal(ip) +func (s *Session) isForDNS(ip netip.Addr, port uint16) bool { + return s.remoteDnsIP == ip && port == 53 } // checkRecursion checks that the given IP is not contained in any of the subnets @@ -47,8 +47,10 @@ func (s *Session) streamCreator(ctx context.Context) tunnel.StreamCreator { return nil, err } } + + destAddr, _ := netip.AddrFromSlice(id.Destination()) if p == ipproto.UDP { - if s.isForDNS(id.Destination(), id.DestinationPort()) { + if s.isForDNS(destAddr, id.DestinationPort()) { pipeId := tunnel.NewConnID(p, id.Source(), s.dnsLocalAddr.IP, id.SourcePort(), uint16(s.dnsLocalAddr.Port)) dlog.Tracef(c, "Intercept DNS %s to %s", id, pipeId.DestinationAddr()) from, to := tunnel.NewPipe(pipeId, s.session.SessionId) @@ -57,7 +59,6 @@ func (s *Session) streamCreator(ctx context.Context) tunnel.StreamCreator { } } - destAddr, _ := netip.AddrFromSlice(id.Destination()) if recursionBlockDuration > 0 { dst := netip.AddrPortFrom(destAddr, id.DestinationPort()) _, recursive := recursionBlockMap.LoadOrCompute(dst, func() struct{} { From e6275d0bd5abcaffc5c8eb5c045569856db39abf Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Wed, 4 Dec 2024 16:28:45 +0100 Subject: [PATCH 2/4] Introduce proxy-via using direct local NAT. Signed-off-by: Thomas Hallgren --- pkg/client/agentpf/clients.go | 9 +++---- pkg/client/rootd/session.go | 43 ++++++++++++++++++++++++------ pkg/client/rootd/stream_creator.go | 31 ++++++++++++--------- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/pkg/client/agentpf/clients.go b/pkg/client/agentpf/clients.go index 134d1949e3..b8c2882942 100644 --- a/pkg/client/agentpf/clients.go +++ b/pkg/client/agentpf/clients.go @@ -527,13 +527,12 @@ func (s *clients) updateClients(ctx context.Context, ais []*manager.AgentPodInfo }) // Ensure that we have at least one client (if at least one agent exists) - if s.clients.Size() == 0 && len(aim) > 0 { - var ai *manager.AgentPodInfo - for _, ai = range aim { + if s.clients.Size() == 0 { + for _, ai := range aim { + k := ai.PodName + "." + ai.Namespace + addClient(k, ai) break } - k := ai.PodName + "." + ai.Namespace - addClient(k, ai) } return nil } diff --git a/pkg/client/rootd/session.go b/pkg/client/rootd/session.go index b3ccc6d45d..855200d092 100644 --- a/pkg/client/rootd/session.go +++ b/pkg/client/rootd/session.go @@ -748,6 +748,12 @@ func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInf if !ok { return fmt.Errorf("invalid traffic-manager pod ip address") } + if s.vipGenerator != nil { + dnsAddr, err = s.maybeGetVirtualIP(ctx, dnsAddr) + if err != nil { + return err + } + } dnsRouted := false for _, sn := range subnets { if sn.Contains(dnsAddr) { @@ -760,10 +766,26 @@ func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInf // from cluster subnets. But not on darwin systems, because there the DNS is controlled by /etc/resolver // entries appointing the DNS service directly via localhost:. if s.vipGenerator != nil { - var err error - dnsAddr, err = s.vipGenerator.Next() + if !s.dnsServerSubnet.IsValid() { + s.createSubnetForDNSOnly(ctx, mgrInfo) + } + dlog.Infof(ctx, "Adding Service subnet %s (for DNS only)", s.dnsServerSubnet) + var wl string + for _, snw := range s.subnetViaWorkloads { + if sn, err := netip.ParsePrefix(snw.Subnet); err == nil && sn.Contains(dnsAddr) { + wl = snw.Workload + if wl == "local" { + wl = "" + } + } + } + s.localTranslationSubnets = append(s.localTranslationSubnets, agentSubnet{ + Prefix: s.dnsServerSubnet, + workload: wl, + }) + dnsAddr, err = s.maybeGetVirtualIP(ctx, dnsAddr) if err != nil { - return nil + return err } } else { if !s.dnsServerSubnet.IsValid() { @@ -1193,17 +1215,20 @@ func (s *Session) consolidateProxyViaWorkloads(ctx context.Context) []string { } } - wlNames := make([]string, len(desiredVips)) + wlNames := make([]string, 0, len(desiredVips)) lcs := make([]agentSubnet, 0, snCount) - i := 0 for wlName, sns := range desiredVips { - wlNames[i] = wlName - i++ + if wlName == "local" { + wlName = "" + } else { + wlNames = append(wlNames, wlName) + } for _, sn := range sns { lcs = append(lcs, agentSubnet{Prefix: sn, workload: wlName}) } } s.localTranslationSubnets = lcs + dlog.Debugf(ctx, "Local translation subnets: %v", s.localTranslationSubnets) return wlNames } @@ -1218,7 +1243,9 @@ func (s *Session) waitForProxyViaWorkloads(ctx context.Context) error { // Need unique workload names ws := make([]string, 0, len(s.subnetViaWorkloads)) for _, svw := range s.subnetViaWorkloads { - ws = slice.AppendUnique(ws, svw.Workload) + if svw.Workload != "local" { + ws = slice.AppendUnique(ws, svw.Workload) + } } for _, wl := range ws { s.agentClients.SetProxyVia(wl) diff --git a/pkg/client/rootd/stream_creator.go b/pkg/client/rootd/stream_creator.go index 4d37274584..f6fae7a827 100644 --- a/pkg/client/rootd/stream_creator.go +++ b/pkg/client/rootd/stream_creator.go @@ -74,17 +74,25 @@ func (s *Session) streamCreator(ctx context.Context) tunnel.StreamCreator { var err error var tp tunnel.Provider - if a, ok := s.getAgentVIP(id); ok { + if a, ok := s.getAgentVIP(destAddr); ok { // s.agentClients is never nil when agentVIPs are used. - tp = s.agentClients.GetWorkloadClient(a.workload) - if tp == nil { - return nil, fmt.Errorf("unable to connect to a traffic-agent for workload %q", a.workload) + if a.workload != "" { + tp = s.agentClients.GetWorkloadClient(a.workload) + if tp == nil { + return nil, fmt.Errorf("unable to connect to a traffic-agent for workload %q", a.workload) + } + // Replace the virtual IP with the original destination IP. This will ensure that the agent + // dials the original destination when the tunnel is established. + id = tunnel.NewConnID(id.Protocol(), id.Source(), a.destinationIP.AsSlice(), id.SourcePort(), id.DestinationPort()) + dlog.Debugf(c, "Opening proxy-via %s tunnel for id %s", a.workload, id) + } else { + dlog.Debugf(c, "Translating proxy-via %s to %s", destAddr, a.destinationIP) + destAddr = a.destinationIP + id = tunnel.NewConnID(id.Protocol(), id.Source(), destAddr.AsSlice(), id.SourcePort(), id.DestinationPort()) } - // Replace the virtual IP with the original destination IP. This will ensure that the agent - // dials the original destination when the tunnel is established. - id = tunnel.NewConnID(id.Protocol(), id.Source(), a.destinationIP.AsSlice(), id.SourcePort(), id.DestinationPort()) - dlog.Debugf(c, "Opening proxy-via %s tunnel for id %s", a.workload, id) - } else { + } + + if tp == nil { tp = s.getAgentClient(destAddr) if tp != nil { dlog.Debugf(c, "Opening traffic-agent tunnel for id %s", id) @@ -103,10 +111,9 @@ func (s *Session) streamCreator(ctx context.Context) tunnel.StreamCreator { } } -func (s *Session) getAgentVIP(id tunnel.ConnID) (a agentVIP, ok bool) { +func (s *Session) getAgentVIP(dest netip.Addr) (a agentVIP, ok bool) { if s.virtualIPs != nil { - key, _ := netip.AddrFromSlice(id.Destination()) - a, ok = s.virtualIPs.Load(key) + a, ok = s.virtualIPs.Load(dest) } return } From d779f3a17f455d44f72fd5168680f8a26a9a7bd5 Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Thu, 5 Dec 2024 01:35:58 +0100 Subject: [PATCH 3/4] docs w.i.p. 2 Signed-off-by: Thomas Hallgren --- CHANGELOG.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.yml b/CHANGELOG.yml index 4b7c519bba..471618b433 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -131,6 +131,17 @@ items: To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment. + - type: feature + title: Prevent recursion in the Telepresence Virtual Network Interface (VIF) + body: >- + Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), + because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a + cluster resource might be routed back into the VIF and cause a recursion. + + These recursions can now be prevented by setting the client configuration property + `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific + IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion. + docs: https://telepresence.io/docs/howtos/cluster-in-vm - type: feature title: Allow Helm chart to be included as a sub-chart body: >- From 00d3b1ee42b9cefd222c21f451b0a8fa7518d14c Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Fri, 6 Dec 2024 17:00:58 +0100 Subject: [PATCH 4/4] Introduce Virtual Network Address Translation (VNAT) Adds the `telepresence connect --vnat CIDR` flag, and a default conflict resolution behavior that relies on its function. Signed-off-by: Thomas Hallgren --- CHANGELOG.yml | 15 + cmd/traffic/cmd/manager/config/config.go | 2 +- docs/images/vpn-proxy-via.jpg | Bin 55502 -> 0 bytes docs/images/vpn-vnat.jpg | Bin 0 -> 52895 bytes docs/reference/config.md | 15 +- docs/reference/vpn.md | 185 ++++++++---- docs/release-notes.md | 19 ++ docs/release-notes.mdx | 13 + integration_test/cidr_conflict_test.go | 148 ++++++++++ integration_test/itest/cluster.go | 5 +- integration_test/itest/namespace.go | 12 +- integration_test/proxy_via_test.go | 23 +- .../testdata/scripts/veth-down.sh | 15 + integration_test/testdata/scripts/veth-up.sh | 18 ++ pkg/client/cli/daemon/request.go | 17 +- pkg/client/config.go | 102 +++++-- pkg/client/config_test.go | 7 +- pkg/client/config_unix.go | 6 +- pkg/client/config_windows.go | 10 +- pkg/client/rootd/in_process.go | 5 + pkg/client/rootd/service.go | 10 +- pkg/client/rootd/session.go | 210 +++++++------ pkg/client/rootd/vip/env_nat.go | 50 ++++ pkg/client/rootd/vip/env_nat_test.go | 123 ++++++++ pkg/client/userd/daemon/service.go | 2 +- pkg/client/userd/k8s/k8s_cluster.go | 2 +- pkg/client/userd/trafficmgr/agents.go | 8 +- pkg/client/userd/trafficmgr/ingest.go | 18 ++ pkg/client/userd/trafficmgr/intercept.go | 6 + pkg/vif/router.go | 24 +- pkg/vif/testdata/router/main.go | 5 + rpc/daemon/daemon.pb.go | 278 +++++++++++------- rpc/daemon/daemon.proto | 7 + rpc/daemon/daemon_grpc.pb.go | 40 +++ 34 files changed, 1075 insertions(+), 325 deletions(-) delete mode 100644 docs/images/vpn-proxy-via.jpg create mode 100644 docs/images/vpn-vnat.jpg create mode 100644 integration_test/cidr_conflict_test.go create mode 100755 integration_test/testdata/scripts/veth-down.sh create mode 100755 integration_test/testdata/scripts/veth-up.sh create mode 100644 pkg/client/rootd/vip/env_nat.go create mode 100644 pkg/client/rootd/vip/env_nat_test.go diff --git a/CHANGELOG.yml b/CHANGELOG.yml index 471618b433..9f2b436b79 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -36,6 +36,21 @@ items: - version: 2.21.0 date: TBD notes: + - type: feature + title: Automatic subnet conflict avoidance + body: -> + Telepresence not only detects when the cluster's subnets are in conflict with subnets on the workstation, it + will also avoid such conflicts by doing network address translations, placing a conflicting subnet in a + virtual subnet. + docs: https://telepresence.io/docs/reference/vpn + - type: feature + title: Virtual Address Translation (VNAT). + body: -> + It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new + `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of + cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without + the need to specify a workload. + docs: https://telepresence.io/docs/reference/vpn - type: feature title: Intercepts targeting a specific container body: -> diff --git a/cmd/traffic/cmd/manager/config/config.go b/cmd/traffic/cmd/manager/config/config.go index b6d3c53f9a..d0fc7bdd5c 100644 --- a/cmd/traffic/cmd/manager/config/config.go +++ b/cmd/traffic/cmd/manager/config/config.go @@ -45,7 +45,7 @@ func (c *config) Run(ctx context.Context) error { dlog.Infof(ctx, "Started watcher for ConfigMap %s", cfgConfigMapName) defer dlog.Infof(ctx, "Ended watcher for ConfigMap %s", cfgConfigMapName) - // The Watch will perform a http GET call to the kubernetes API server, and that connection will not remain open forever + // The WatchConfig will perform a http GET call to the kubernetes API server, and that connection will not remain open forever // so when it closes, the watch must start over. This goes on until the context is cancelled. api := k8sapi.GetK8sInterface(ctx).CoreV1() for ctx.Err() == nil { diff --git a/docs/images/vpn-proxy-via.jpg b/docs/images/vpn-proxy-via.jpg deleted file mode 100644 index 338e088bd810f6977ce10366ab0ef1e652233dfe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55502 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 CKD6Bc diff --git a/docs/images/vpn-vnat.jpg b/docs/images/vpn-vnat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c7d0551576746bfee95b795c07ddeb8e60d3d39 GIT binary patch literal 52895 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ⅈ-Xq^V~_Mi_NBK47)EFeU{^7eYBT%Z8ov$ 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
hE;{4^F@x*Zs#u|_|Bie JuFub@{{j`ZlF$GE literal 0 HcmV?d00001 diff --git a/docs/reference/config.md b/docs/reference/config.md index ee4924a215..d19708313a 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -51,7 +51,6 @@ Values for `client.cluster` controls aspects on how client's connection to the t | `mappedNamespaces` | Namespaces that will be mapped by default. | [sequence][yaml-seq] of [strings][yaml-str] | `[]` | | `connectFromRootDaeamon` | Make connections to the cluster directly from the root daemon. | [boolean][yaml-bool] | `true` | | `agentPortForward` | Let telepresence-client use port-forwards directly to agents | [boolean][yaml-bool] | `true` | -| `virtualIPSubnet` | The CIDR to use when generating virtual IPs | [CIDR][cidr] | platform dependent | ### DNS @@ -208,12 +207,14 @@ Then all of the `alsoProxySubnets` of `10.0.0.0/16` will be proxied, with the ex These are the valid fields for the `client.routing` key: -| Field | Description | Type | -|---------------------------|----------------------------------------------------------------------------------------|-------------------------| -| `alsoProxySubnets` | Proxy these subnets in addition to the service and pod subnets | [CIDR][cidr] | -| `neverProxySubnets` | Do not proxy these subnets | [CIDR][cidr] | -| `allowConflictingSubnets` | Give Telepresence precedence when these subnets conflict with other network interfaces | [CIDR][cidr] | -| `recursionBlockDuration` | Prevent recursion in VIF for this duration after a connect | [duration][go-duration] | +| Field | Description | Type | Default | +|---------------------------|----------------------------------------------------------------------------------------|-------------------------|--------------------| +| `alsoProxySubnets` | Proxy these subnets in addition to the service and pod subnets | [CIDR][cidr] | | +| `neverProxySubnets` | Do not proxy these subnets | [CIDR][cidr] | | +| `allowConflictingSubnets` | Give Telepresence precedence when these subnets conflict with other network interfaces | [CIDR][cidr] | | +| `recursionBlockDuration` | Prevent recursion in VIF for this duration after a connect | [duration][go-duration] | | +| `virtualSubnet` | The CIDR to use when generating virtual IPs | [CIDR][cidr] | platform dependent | +| `autoResolveConflicts` | Auto resolve conflicts using a virtual subnet | [bool][yaml-bool] | true | ### Timeouts diff --git a/docs/reference/vpn.md b/docs/reference/vpn.md index 3aa6a656f4..7e7987c769 100644 --- a/docs/reference/vpn.md +++ b/docs/reference/vpn.md @@ -4,10 +4,15 @@ title: Telepresence and VPNs # Telepresence and VPNs -It is often important to set up Kubernetes API server endpoints to be only accessible via a VPN. -In setups like these, users need to connect first to their VPN, and then use Telepresence to connect -to their cluster. As Telepresence uses many of the same underlying technologies that VPNs use, -the two can sometimes conflict. This page will help you identify and resolve such VPN conflicts. +Telepresence creates a virtual network interface (VIF) when it connects. This VIF is configured to route the cluster's +service subnet and pod subnets so that the user can access resources in the cluster. It's not uncommon that the +workstation where Telepresence runs already has network interfaces that route subnets that will overlap. Such +conflicts must be resolved deterministically. + +Unless configured otherwise, Telepresence will resolve subnet conflicts by simply moving the cluster's subnet using +network address translation. For a majority of use-cases, this will be enough. + +For more info, see the section on how to [avoid the conflict](#avoiding-the-conflict) below. ## VPN Configuration @@ -39,7 +44,7 @@ cluster will place resources in. Let's imagine your cluster is configured to pla ![VPN Kubernetes config](../images/vpn-k8s-config.jpg) -## Telepresence conflicts +# Telepresence conflicts When you run `telepresence connect` to connect to a cluster, it talks to the API server to figure out what pod and service CIDRs it needs to map in your machine. If it detects @@ -53,53 +58,31 @@ telepresence connect: error: connector.Connect: failed to connect to root daemon Telepresence offers three different ways to resolve this: -- [Allow the conflict](#allowing-the-conflict) in a controlled manner - [Avoid the conflict](#avoiding-the-conflict) using the `--proxy-via` connect flag +- [Allow the conflict](#allowing-the-conflict) in a controlled manner - [Use docker](#using-docker) to make telepresence run in a container with its own network config -### Allowing the conflict - -One way to resolve this, is to carefully consider what your network layout looks like, and -then allow Telepresence to override the conflicting subnets. -Telepresence is refusing to map them, because mapping them could render certain hosts that -are inside the VPN completely unreachable. However, you (or your network admin) know better -than anyone how hosts are spread out inside your VPN. -Even if the private route routes ALL of `10.0.0.0/8`, it's possible that hosts are only -being spun up in one of the subblocks of the `/8` space. Let's say, for example, -that you happen to know that all your hosts in the VPN are bunched up in the first -half of the space -- `10.0.0.0/9` (and that you know that any new hosts will -only be assigned IP addresses from the `/9` block). In this case you -can configure Telepresence to override the other half of this CIDR block, which is where the -services and pods happen to be. -To do this, all you have to do is configure the `client.routing.allowConflictingSubnets` flag -in the Telepresence helm chart. You can do this directly via `telepresence helm upgrade`: -```console -$ telepresence helm upgrade --set client.routing.allowConflictingSubnets="{10.128.0.0/9}" -``` +## Avoiding the conflict -You can also choose to be more specific about this, and only allow the CIDRs that you KNOW -are in use by the cluster: - -```console -$ telepresence helm upgrade --set client.routing.allowConflictingSubnets="{10.130.0.0/16,10.132.0.0/16}" -``` - -The end result of this (assuming an allow list of `/9`) will be a configuration like this: - -![VPN Telepresence](../images/vpn-with-tele.jpg) +Telepresence can perform Virtual Network Address Translation (henceforth referred to as VNAT) of the cluster's subnets +when routing them from the workstation, thus moving those subnets so that conflicts are avoided. Unless configured not +to, Telepresence will use VNAT by default when it detects conflicts. -### Avoiding the conflict +VNAT is enabled by passing a `--vnat` flag (introduced in Telepresence 2.21) to`teleprence connect`. When using this +flag, Telepresence will take the following actions: -An alternative to allowing the conflict is to remap the cluster's CIDRs to virtual CIRDs -on the workstation by passing a `--proxy-via` flag to `teleprence connect`. +- The local DNS-server will translate any IP contained in a VNAT subnet to a virtual IP. +- All access to a virtual IP will be translated back to its original when routed to the cluster. +- The container environment retrieved when using `ingest` or `intercept` will be mangled, so that all IPs contained + in VNAT subnets are replaced with corresponding virtual IPs. -The `telepresence connect` flag `--proxy-via`, introduced in Telepresence 2.19, will allow the local DNS-server to translate cluster subnets to virtual subnets on the workstation, and the VIF to do the reverse translation. The syntax for this new flag, which can be repeated, is: +The `--vnat` flag can be repeated to make Telepresence translate more than one subnet. ```console -$ telepresence connect --proxy-via CIDR=WORKLOAD +$ telepresence connect --vnat CIDR ``` -Cluster DNS responses matching CIDR to virtual IPs that are routed (with reverse translation) via WORKLOAD. The CIDR can also be a symbolic name that identifies a subnet or list of subnets: +The CIDR can also be a symbolic name that identifies a well-known subnet or list of subnets: | Symbol | Meaning | |-----------|-------------------------------------| @@ -108,38 +91,128 @@ Cluster DNS responses matching CIDR to virtual IPs that are routed (with reverse | `pods` | The cluster's pod subnets. | | `all` | All of the above. | -The WORKLOAD is the deployment, replicaset, statefulset, or argo-rollout in the cluster whose agent will be used for targeting the routed subnets. -This is useful in two situations: +### Virtual Subnet Configuration -1. The cluster's subnets collide with subnets otherwise available on the workstation. This is common when using a VPN, in particular if the VPN has a small subnet mask, making the subnet itself very large. The new `--proxy-via` flag can be used as an alternative to [allowing the conflict](#allowing-the-conflict) to take place, give Telepresence precedence, and thus hide the corresponding subnets from the conflicting subnet. The `--proxy-via` will instead reroute the cluster's subnet and hence, avoid the conflict. -2. The cluster's DNS is configured with domains that resolve to loop-back addresses (this is sometimes the case when the cluster uses a mesh configured to listen to a loopback address and then reroute from there). A loop-back address is not useful on the client, but the `--proxy-via` can reroute the loop-back address to a virtual IP that the client can use. +Telepresence will use a special subnet when it generates the virtual IPs that are used locally. On a Linux or macOS +workstation, this subnet will be a class E subnet (not normally used for any other purposes). On Windows, the class E is +not routed, and Telepresence will instead default to `211.55.48.0/20`. -Subnet proxying is done by the client's DNS-resolver which translates the IPs returned by the cluster's DNS resolver to a virtual IP (VIP) to use on the client. Telepresence's VIF will detect when the VIP is used, and translate it back to the loop-back address on the pod. +The default subnet used can be overridden in the client configuration. + +In `config.yml` on the workstation: +```yaml +routing: + virtualSubnet: 100.10.20.0/24 +``` + +Or as a Helm chart value to be applied on all clients: +```yaml +client: + routing: + virtualSubnet: 100.10.20.0/24 +``` + +#### Example + +Let's assume that we have a conflict between the cluster's subnets, all covered by the CIDR `10.124.0.0/9` and a VPN +using `10.0.0.0/9`. We avoid the conflict using: + +```console +$ telepresence connect --vnat all +``` + +The cluster's subnets are now hidden behind a virtual subnet, and the resulting configuration will look like this: -#### Proxy-via and using IP-addresses directly +![VPN Telepresence](../images/vpn-vnat.jpg) -If the service is using IP-addresses instead of domain-names when connecting to other cluster resources, then such connections will fail when running locally. The `--proxy-via` relies on the local DNS-server to translate the cluster's DNS responses, so that the IP of an `A` or `AAAA` response is replaced with a virtual IP from the configured subnet. If connections are made using an IP instead of a domain-name, then no such lookup is made. Telepresence has no way of detecting the direct use of IP-addresses. +### Proxying via a specific workload -#### Virtual IP Configuration +Telepresence is capable of routing all traffic to a VNAT to a specific workload. This is particularly useful when the +cluster's DNS is configured with domains that resolve to loop-back addresses. This is sometimes the case when the +cluster uses a mesh configured to listen to a loopback address and then reroute from there. -Telepresence will use a special subnet when it generates the virtual IPs that are used locally. On a Linux or macOS workstation, this subnet will be -a class E subnet (not normally used for any other purposes). On Windows, the class E is not routed, and Telepresence will instead default to `211.55.48.0/20`. +The `--proxy-via` flag (introduced in Telepresenc 2.19) is similar to `--vnat`, but the argument must be in the form +CIDR=WORKLOAD. When using this flag, all traffic to the given CIDR will be routed via the given workstation. -The default can be changed using the configuration `cluster.virtualIPSubnet`. +The WORKLOAD is the deployment, replicaset, statefulset, or argo-rollout in the cluster whose traffic-agent will be used +for targeting the routed subnets. #### Example -Let's assume that we have a conflict between the cluster's subnets, all covered by the CIDR `10.124.0.0/9` and a VPN using `10.0.0.0/9`. We avoid the conflict using: +Let's assume that we have a conflict between the cluster's subnets, all covered by the CIDR `10.124.0.0/9` and a VPN +using `10.0.0.0/9`. We avoid the conflict using: ```console $ telepresence connect --proxy-via all=echo ``` -The cluster's subnets are now hidden behind a virtual subnet, and the resulting configuration will look like this: +The cluster's subnets are now hidden behind a virtual subnet, and all traffic is routed to the echo workload. + +### Caveats when using VNAT + +Telepresence may not accurately detect cluster-side IP addresses being used by services running locally on a workstation +in certain scenarios. This limitation arises when local services obtain IP addresses from remote sources such as +databases or configmaps, or when IP addresses are sent to it in API calls. + +### Disabling default VNAT + +The default behavior of using VNAT to resolve conflicts can be disabled by adding the following to the client config. + +In `config.yml` on the workstation: +```yaml +routing: + autoResolveConflicts: false +``` + +Or as a Helm chart value to be applied on all clients: +```yaml +client: + routing: + autoResolveConflicts: false +``` + +Explicitly allowing all conflicts will also effectively prevent the default VNAT behavior. + +## Allowing the conflict + +A conflict can be resolved by carefully considering what your network layout looks like, and then allow Telepresence to +override the conflicting subnets. Telepresence is refusing to map them, because mapping them could render certain hosts +that are inside the VPN completely unreachable. However, you (or your network admin) know better than anyone how hosts +are spread out inside your VPN. -![VPN Telepresence](../images/vpn-proxy-via.jpg) +Even if the private route routes ALL of `10.0.0.0/8`, it's possible that hosts are only being spun up in one of the +sub-blocks of the `/8` space. Let's say, for example, that you happen to know that all your hosts in the VPN are bunched +up in the first half of the space -- `10.0.0.0/9` (and that you know that any new hosts will only be assigned IP +addresses from the `/9` block). In this case you can configure Telepresence to override the other half of this CIDR +block, which is where the services and pods happen to be. + +To do this, all you have to do is configure the `client.routing.allowConflictingSubnets` flag in the Telepresence helm +chart. You can do this directly via `telepresence helm upgrade`: + +In `config.yml` on the workstation: +```yaml +routing: + allowConflictingSubnets: 10.128.0.0/9 +``` + +Or as a Helm chart configuration value to be applied on all clients: +```yaml +client: + routing: + allowConflictingSubnets: 10.128.0.0/9 +``` + +Or pass the Helm chart configuration using the `--set` flag +```console +$ telepresence helm upgrade --set client.routing.allowConflictingSubnets="{10.128.0.0/9}" +``` + +The end result of this (assuming an allowlist of `/9`) will be a configuration like this: + +![VPN Telepresence](../images/vpn-with-tele.jpg) ### Using docker -Use `telepresence connect --docker` to make the Telepresence daemon containerized, which means that it has its own network configuration and therefore no conflict with a VPN. Read more about docker [here](docker-run.md). +Use `telepresence connect --docker` to make the Telepresence daemon containerized, which means that it has its own +network configuration and therefore no conflict with a VPN. Read more about docker [here](docker-run.md). diff --git a/docs/release-notes.md b/docs/release-notes.md index c6ee108ca1..1fad42c70b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,18 @@ [comment]: # (Code generated by relnotesgen. DO NOT EDIT.) # Telepresence Release Notes ## Version 2.21.0 +##
feature
[Automatic subnet conflict avoidance](https://telepresence.io/docs/reference/vpn)
+
+ +-> Telepresence not only detects when the cluster's subnets are in conflict with subnets on the workstation, it will also avoid such conflicts by doing network address translations, placing a conflicting subnet in a virtual subnet. +
+ +##
feature
[Virtual Address Translation (VNAT).](https://telepresence.io/docs/reference/vpn)
+
+ +-> It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without the need to specify a workload. +
+ ##
feature
[Intercepts targeting a specific container](https://telepresence.io/docs/reference/intercepts/container)
@@ -75,6 +87,13 @@ Normally, Docker has a limitation that prevents combining a shared network confi To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment.
+##
feature
[Prevent recursion in the Telepresence Virtual Network Interface (VIF)](https://telepresence.io/docs/howtos/cluster-in-vm)
+
+ +Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a cluster resource might be routed back into the VIF and cause a recursion. +These recursions can now be prevented by setting the client configuration property `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion. +
+ ##
feature
Allow Helm chart to be included as a sub-chart
diff --git a/docs/release-notes.mdx b/docs/release-notes.mdx index be5571e3c2..0ce7b203e7 100644 --- a/docs/release-notes.mdx +++ b/docs/release-notes.mdx @@ -8,6 +8,14 @@ import { Note, Title, Body } from '@site/src/components/ReleaseNotes' # Telepresence Release Notes ## Version 2.21.0 + + Automatic subnet conflict avoidance + -> Telepresence not only detects when the cluster's subnets are in conflict with subnets on the workstation, it will also avoid such conflicts by doing network address translations, placing a conflicting subnet in a virtual subnet. + + + Virtual Address Translation (VNAT). + -> It is now possible to use a virtual subnet without routing the affected IPs to a specific workload. A new `telepresence connect --vnat CIDR` flag was added that will perform virtual network address translation of cluster IPs. This flag is very similar to the `--proxy-via CIDR=WORKLOAD` introduced in 2.19, but without the need to specify a workload. + Intercepts targeting a specific container -> In certain scenarios, the container owning the intercepted port differs from the container the intercept targets. This port owner's sole purpose is to route traffic from the service to the intended container, often using a direct localhost connection. @@ -59,6 +67,11 @@ See [Streaming Transitions from SPDY to WebSockets](https://kubernetes.io/blog/2 Normally, Docker has a limitation that prevents combining a shared network configuration with custom networks and exposing ports. However, Telepresence now elegantly circumvents this limitation so that a container started with `telepresence docker-run`, `telepresence intercept --docker-run`, or `telepresence ingest --docker-run` can use flags like `--network`, `--publish`, or `--expose`. To achieve this, Telepresence temporarily adds the necessary network to the containerized daemon. This allows the new container to join the same network. Additionally, Telepresence starts extra socat containers to handle port mapping, ensuring that the desired ports are exposed to the local environment. + + Prevent recursion in the Telepresence Virtual Network Interface (VIF) + Network problems may arise when running Kubernetes locally (e.g., Docker Desktop, Kind, Minikube, k3s), because the VIF on the host is also accessible from the cluster's nodes. A request that isn't handled by a cluster resource might be routed back into the VIF and cause a recursion. +These recursions can now be prevented by setting the client configuration property `routing.recursionBlockDuration` so that new connection attempts are temporarily blocked for a specific IP:PORT pair immediately after an initial attempt, thereby effectively ending the recursion. + Allow Helm chart to be included as a sub-chart The Helm chart previously had the unnecessary restriction that the .Release.Name under which telepresence is installed is literally called "traffic-manager". This restriction was preventing telepresence from being included as a sub-chart in a parent chart called anything but "traffic-manager". This restriction has been lifted. diff --git a/integration_test/cidr_conflict_test.go b/integration_test/cidr_conflict_test.go new file mode 100644 index 0000000000..dfbe9e7a76 --- /dev/null +++ b/integration_test/cidr_conflict_test.go @@ -0,0 +1,148 @@ +package integration_test + +import ( + "fmt" + "net/netip" + "os" + "path/filepath" + "runtime" + + "github.com/go-json-experiment/json" + core "k8s.io/api/core/v1" + + "github.com/telepresenceio/telepresence/v2/integration_test/itest" + "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" + "github.com/telepresenceio/telepresence/v2/pkg/client" +) + +type cidrConflictSuite struct { + itest.Suite + itest.TrafficManager + vipSubnet netip.Prefix + subnets []netip.Prefix + scripts string +} + +func (s *cidrConflictSuite) SuiteName() string { + return "CIDRConflict" +} + +func init() { + itest.AddTrafficManagerSuite("", func(h itest.TrafficManager) itest.TestingSuite { + return &cidrConflictSuite{Suite: itest.Suite{Harness: h}, TrafficManager: h} + }) +} + +func (s *cidrConflictSuite) SetupSuite() { + if runtime.GOOS != "linux" { + s.T().Skip("we can only create veth interfaces on linux") + } + const svc = "echo" + s.Suite.SetupSuite() + tpl := &itest.Generic{ + Name: svc, + Registry: "ghcr.io/telepresenceio", + Image: "echo-server:latest", + Environment: []core.EnvVar{ + { + Name: "PORTS", + Value: "8080", + }, + { + Name: "LISTEN_ADDRESS", + ValueFrom: &core.EnvVarSource{ + FieldRef: &core.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + }, + Annotations: map[string]string{ + agentconfig.InjectAnnotation: "enabled", + }, + } + s.ApplyTemplate(s.Context(), filepath.Join("testdata", "k8s", "generic.goyaml"), &tpl) + s.NoError(s.RolloutStatusWait(s.Context(), "deploy/echo")) + + ctx := s.Context() + s.TelepresenceConnect(ctx) + st := itest.TelepresenceStatusOk(ctx) + itest.TelepresenceQuitOk(ctx) + s.subnets = st.RootDaemon.Subnets + if len(s.subnets) < 2 { + s.T().Skip("Test cannot run unless client maps at least two subnets") + } + var err error + s.scripts, err = filepath.Abs(filepath.Join("testdata", "scripts")) + if s.NoError(err) { + // Create an interface that will be in conflict with the service and pod subnets. + s.NoError(itest.Run(ctx, "sudo", filepath.Join(s.scripts, "veth-up.sh"), s.subnets[0].String(), s.subnets[1].String())) + s.NoError(err) + } + s.vipSubnet = client.GetConfig(ctx).Routing().VirtualSubnet +} + +func (s *cidrConflictSuite) TearDownSuite() { + ctx := s.Context() + s.NoError(itest.Run(ctx, "sudo", filepath.Join(s.scripts, "veth-down.sh"), s.subnets[0].String(), s.subnets[1].String())) + s.DeleteSvcAndWorkload(ctx, "deploy", "echo") +} + +func (s *cidrConflictSuite) Test_AutoConflictResolution() { + ctx := s.Context() + s.TelepresenceConnect(ctx) + st := itest.TelepresenceStatusOk(ctx) + defer itest.TelepresenceQuitOk(ctx) + sns := st.RootDaemon.Subnets + rq := s.Require() + rq.Less(len(sns), len(s.subnets), "pod and service subnets should be combined into one virtual subnet") + + // The first subnet must now be virtual. + viSn := sns[0] + rq.Equalf(s.vipSubnet, viSn, "expected %s to be a virtual CIDR", viSn) + + // Ingest to get a container environment. + envFile := filepath.Join(s.T().TempDir(), "echo.env") + itest.TelepresenceOk(ctx, "ingest", "echo", "--env-file", envFile, "--env-syntax", "json") + itest.TelepresenceOk(ctx, "leave", "echo") + var env map[string]string + envData, err := os.ReadFile(envFile) + rq.NoError(err) + err = json.Unmarshal(envData, &env) + rq.NoError(err) + + // Verify that these IPs in the environment have been translated into virtual IPs. + for _, key := range []string{"LISTEN_ADDRESS", "ECHO_SERVICE_HOST"} { + addrVal, ok := env[key] + rq.True(ok) + addr, err := netip.ParseAddr(addrVal) + rq.NoError(err) + rq.Truef(viSn.Contains(addr), "virtual subnet %s does not contain %s %s", viSn, key, addr) + } +} + +func (s *cidrConflictSuite) Test_AutoConflictAvoidance() { + ctx := s.Context() + s.TelepresenceConnect(ctx, "--allow-conflicting-subnets", fmt.Sprintf("%s,%s", s.subnets[0], s.subnets[1])) + st := itest.TelepresenceStatusOk(ctx) + defer itest.TelepresenceQuitOk(ctx) + sns := st.RootDaemon.Subnets + s.Require().Equal(s.subnets, sns, "subnet conflict should not be resolved using VNAT") +} + +func (s *cidrConflictSuite) Test_AutoConflictResolution_CloudDisable() { + ctx := s.Context() + s.TelepresenceHelmInstallOK(ctx, true, "--set", "client.routing.autoResolveConflicts=false") + defer s.RollbackTM(ctx) + + _, err := s.TelepresenceTryConnect(ctx) + s.Require().Error(err) +} + +func (s *cidrConflictSuite) Test_AutoConflictResolution_ClientDisable() { + ctx := itest.WithConfig(s.Context(), func(cfg client.Config) { + cfg.Routing().AutoResolveConflicts = false + }) + _, err := s.TelepresenceTryConnect(ctx) + s.Require().Error(err) +} diff --git a/integration_test/itest/cluster.go b/integration_test/itest/cluster.go index ddcaaf1cb7..fd5c6083be 100644 --- a/integration_test/itest/cluster.go +++ b/integration_test/itest/cluster.go @@ -605,7 +605,6 @@ func (s *cluster) GetValuesForHelm(ctx context.Context, values map[string]string nss := GetNamespaces(ctx) settings := []string{ "logLevel=debug", - "client.routing.allowConflictingSubnets={10.0.0.0/8}", } reg := s.self.Registry() if reg == "local" { @@ -743,9 +742,7 @@ func (s *cluster) TelepresenceHelmInstall(ctx context.Context, upgrade bool, set Namespaces: nsl, }, Client: xClient{ - Routing: map[string][]string{ - "allowConflictingSubnets": {"10.0.0.0/8"}, - }, + Routing: map[string][]string{}, }, Timeouts: xTimeouts{AgentArrival: "60s"}, } diff --git a/integration_test/itest/namespace.go b/integration_test/itest/namespace.go index 185a8d6911..945e7e43fb 100644 --- a/integration_test/itest/namespace.go +++ b/integration_test/itest/namespace.go @@ -25,6 +25,7 @@ type NamespacePair interface { DeleteTemplate(ctx context.Context, path string, values any) AppNamespace() string TelepresenceConnect(ctx context.Context, args ...string) string + TelepresenceTryConnect(ctx context.Context, args ...string) (string, error) DeleteSvcAndWorkload(ctx context.Context, workload, name string) Kubectl(ctx context.Context, args ...string) error KubectlOk(ctx context.Context, args ...string) string @@ -85,7 +86,7 @@ type nsPair struct { Namespaces } -// TelepresenceConnect connects using the AppNamespace and ManagerNamespace. +// TelepresenceConnect connects using the AppNamespace and ManagerNamespace and requires the result to be OK. func (s *nsPair) TelepresenceConnect(ctx context.Context, args ...string) string { return TelepresenceOk(ctx, append( @@ -93,6 +94,15 @@ func (s *nsPair) TelepresenceConnect(ctx context.Context, args ...string) string args...)...) } +// TelepresenceTryConnect connects using the AppNamespace and ManagerNamespace and returns an error on failure. +func (s *nsPair) TelepresenceTryConnect(ctx context.Context, args ...string) (string, error) { + stdout, _, err := Telepresence(ctx, + append( + []string{"connect", "--namespace", s.AppNamespace(), "--manager-namespace", s.ManagerNamespace()}, + args...)...) + return stdout, err +} + func WithNamespacePair(ctx context.Context, suffix string, f func(NamespacePair)) { s := &nsPair{} var namespace string diff --git a/integration_test/proxy_via_test.go b/integration_test/proxy_via_test.go index 7d851777e3..cc52cabe36 100644 --- a/integration_test/proxy_via_test.go +++ b/integration_test/proxy_via_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "net/netip" "os" "path/filepath" "regexp" @@ -63,7 +64,7 @@ func (s *proxyViaSuite) Test_ProxyViaLoopBack() { ctx := s.Context() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { - config.Cluster().VirtualIPSubnet = "abac:0de0::/64" + config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } @@ -77,8 +78,7 @@ func (s *proxyViaSuite) Test_ProxyViaLoopBack() { } defer itest.TelepresenceQuitOk(ctx) - _, virtualIPSubnet, err := net.ParseCIDR(client.GetConfig(ctx).Cluster().VirtualIPSubnet) - s.Require().NoError(err) + virtualSubnet := client.GetConfig(ctx).Routing().VirtualSubnet tests := []struct { name string @@ -100,10 +100,10 @@ func (s *proxyViaSuite) Test_ProxyViaLoopBack() { tt := tt s.Run(tt.name, func() { rq := s.Require() - var ips []net.IP + var vip netip.Addr rq.Eventually(func() bool { // hostname will resolve to 127.0.0.1 remotely and then be translated into a virtual IP - ips, err = net.LookupIP(tt.hostName) + ips, err := net.LookupIP(tt.hostName) if err != nil { dlog.Error(ctx, err) return false @@ -112,11 +112,12 @@ func (s *proxyViaSuite) Test_ProxyViaLoopBack() { dlog.Error(ctx, "LookupIP did not return one IP") return false } - return true + var ok bool + vip, ok = netip.AddrFromSlice(ips[0]) + return ok }, 30*time.Second, 2*time.Second) - vip := ips[0] dlog.Infof(ctx, "%s uses IP %s", tt.hostName, vip) - rq.Truef(virtualIPSubnet.Contains(vip), "virtualIPSubnet %s does not contain %s", virtualIPSubnet, vip) + rq.Truef(virtualSubnet.Contains(vip), "virtualIPSubnet %s does not contain %s", virtualSubnet, vip) rq.Eventually(func() bool { out, err := itest.Output(ctx, "curl", "--silent", "--max-time", "2", net.JoinHostPort(tt.hostName, "8080")) @@ -139,7 +140,7 @@ func (s *proxyViaSuite) Test_ProxyViaEverything() { if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { - config.Cluster().VirtualIPSubnet = "abac:0de0::/64" + config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } @@ -165,7 +166,7 @@ func (s *proxyViaSuite) Test_ProxyViaAll() { rq := s.Require() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { - config.Cluster().VirtualIPSubnet = "abac:0de0::/64" + config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } @@ -189,7 +190,7 @@ func (s *proxyViaSuite) Test_ProxyViaAllAndMounts() { rq := s.Require() if s.IsIPv6() { ctx = itest.WithConfig(ctx, func(config client.Config) { - config.Cluster().VirtualIPSubnet = "abac:0de0::/64" + config.Routing().VirtualSubnet = netip.MustParsePrefix("abac:0de0::/64") }) } diff --git a/integration_test/testdata/scripts/veth-down.sh b/integration_test/testdata/scripts/veth-down.sh new file mode 100755 index 0000000000..fa83f39732 --- /dev/null +++ b/integration_test/testdata/scripts/veth-down.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash +ip link set vm2 down +ip link set brm down + +for var in "$@" +do + ip addr del "$(echo "$var" | sed -r 's/\.0$/.1/')" dev brm + ip addr del "$(echo "$var" | sed -r 's/\.0$/.2/')" dev vm2 +done + +ip link del brm type bridge +ip link set dev tapm down +ip tuntap del tapm mode tap +ip link set dev vm1 down +ip link del dev vm1 type veth peer name vm2 diff --git a/integration_test/testdata/scripts/veth-up.sh b/integration_test/testdata/scripts/veth-up.sh new file mode 100755 index 0000000000..1f91b7b701 --- /dev/null +++ b/integration_test/testdata/scripts/veth-up.sh @@ -0,0 +1,18 @@ +#!/usr/bin/bash +ip link add dev vm1 type veth peer name vm2 +ip link set dev vm1 up +ip tuntap add tapm mode tap +ip link set dev tapm up +ip link add brm type bridge + +ip link set tapm master brm +ip link set vm1 master brm + +for var in "$@" +do + ip addr add "$(echo "$var" | sed -r 's/\.0$/.1/')" dev brm + ip addr add "$(echo "$var" | sed -r 's/\.0$/.2/')" dev vm2 +done + +ip link set brm up +ip link set vm2 up \ No newline at end of file diff --git a/pkg/client/cli/daemon/request.go b/pkg/client/cli/daemon/request.go index b782aaad7a..6fc0ed7640 100644 --- a/pkg/client/cli/daemon/request.go +++ b/pkg/client/cli/daemon/request.go @@ -53,6 +53,9 @@ type Request struct { // proxyVia holds the string version for the --proxy-via flag values. proxyVia []string + + // vnats holds the string version for the --vnat flag values. + vnats []string } type CobraRequest struct { @@ -82,10 +85,14 @@ func InitRequest(cmd *cobra.Command) *CobraRequest { nwFlags.StringSliceVar(&cr.NeverProxy, "never-proxy", nil, ``+ `Comma separated list of CIDR to never proxy`) + nwFlags.StringSliceVar(&cr.vnats, + "vnat", nil, ``+ + `Use Network Address Translation to create virtual IPs for the given CIDR. CIDR can be substituted for the `+ + `symblic name "service", "pods", "also", or "all".`) nwFlags.StringSliceVar(&cr.proxyVia, "proxy-via", nil, ``+ - `Locally translate cluster DNS responses matching CIDR to virtual IPs that are routed (with reverse `+ - `translation) via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all".`) + `Use Network Address Translation to create virtual IPs for the given CIDR, and route via WORKLOAD. Must be in the`+ + `form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all".`) nwFlags.StringSliceVar(&cr.AllowConflictingSubnets, "allow-conflicting-subnets", nil, ``+ `Comma separated list of CIDR that will be allowed to conflict with local subnets`) @@ -145,6 +152,12 @@ func (cr *CobraRequest) CommitFlags(cmd *cobra.Command) error { if err != nil { return err } + + // A --vnat CIDR is the same as --proxy-via CIDR=local + for _, vnat := range cr.vnats { + cr.proxyVia = append(cr.proxyVia, vnat+"=local") + } + err = cr.setGlobalConnectFlags(cmd) if err != nil { return errcat.User.New(err) diff --git a/pkg/client/config.go b/pkg/client/config.go index 69ce188816..03fc2727a7 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -239,6 +239,14 @@ func ParseConfigYAML(ctx context.Context, path string, data []byte) (Config, err return nil, err } } + if cfg.Routing().VirtualSubnet == defaultVirtualSubnet && cfg.Cluster().OldVirtualIPSubnet != "" { + dlog.Warningf(ctx, "please use routing.VirtualSubnet instead of deprecated deprecated cluster.VirtualIPSubnet") + sn, err := netip.ParsePrefix(cfg.Cluster().OldVirtualIPSubnet) + if err != nil { + return nil, fmt.Errorf("unable to parse deprecated cluster.VirtualIPSubnet: %w", err) + } + cfg.Routing().VirtualSubnet = sn + } return cfg, nil } @@ -268,9 +276,9 @@ func (c *BaseConfig) String() string { return string(y) } -// Watch uses a file system watcher that receives events when the configuration changes +// WatchConfig uses a file system watcher that receives events when the configuration changes // and calls the given function when that happens. -func Watch(c context.Context, onReload func(context.Context) error) error { +func WatchConfig(c context.Context, onReload func(context.Context) error) error { configFile := GetConfigFile(c) watcher, err := fsnotify.NewWatcher() if err != nil { @@ -289,7 +297,14 @@ func Watch(c context.Context, onReload func(context.Context) error) error { // The delay timer will initially sleep forever. It's reset to a very short // delay when the file is modified. delay := time.AfterFunc(time.Duration(math.MaxInt64), func() { - if err := onReload(c); err != nil { + cfg, err := LoadConfig(c) + if err != nil { + dlog.Error(c, err) + } else { + ReplaceConfig(c, cfg) + err = onReload(c) + } + if err != nil { dlog.Error(c, err) } }) @@ -778,7 +793,9 @@ type Cluster struct { ConnectFromRootDaemon bool `json:"connectFromRootDaemon"` ForceSPDY bool `json:"forceSPDY"` AgentPortForward bool `json:"agentPortForward"` - VirtualIPSubnet string `json:"virtualIPSubnet"` + + // deprecated, use Routing.VirtualSubnet + OldVirtualIPSubnet string `json:"virtualIPSubnet"` } // This is used by a different config -- the k8s_config, which needs to be able to tell if it's overridden at a cluster or environment variable level. @@ -789,7 +806,6 @@ var defaultCluster = Cluster{ //nolint:gochecknoglobals // constant DefaultManagerNamespace: defaultDefaultManagerNamespace, ConnectFromRootDaemon: true, AgentPortForward: true, - VirtualIPSubnet: defaultVirtualIPSubnet, } func (cc *Cluster) defaults() DefaultsAware { @@ -819,6 +835,32 @@ func (cc *Cluster) UnmarshalJSONV2(in *jsontext.Decoder, opts json.Options) erro return json.UnmarshalDecode(in, &wp, opts) } +type Routing struct { + Subnets []netip.Prefix `json:"subnets,omitempty"` + AlsoProxy []netip.Prefix `json:"alsoProxySubnets,omitempty"` + NeverProxy []netip.Prefix `json:"neverProxySubnets,omitempty"` + AllowConflicting []netip.Prefix `json:"allowConflictingSubnets,omitempty"` + RecursionBlockDuration time.Duration `json:"recursionBlockDuration,omitempty"` + VirtualSubnet netip.Prefix `json:"virtualSubnet"` + AutoResolveConflicts bool `json:"autoResolveConflicts"` + + // For backward compatibility. + OldAlsoProxy []netip.Prefix `json:"alsoProxy,omitempty"` + OldNeverProxy []netip.Prefix `json:"neverProxy,omitempty"` + OldAllowConflicting []netip.Prefix `json:"allowConflicting,omitempty"` +} + +const defaultAutoResolveConflicts = true + +var defaultRouting = Routing{ //nolint:gochecknoglobals // constant + VirtualSubnet: defaultVirtualSubnet, + AutoResolveConflicts: defaultAutoResolveConflicts, +} + +func (r *Routing) defaults() DefaultsAware { + return &defaultRouting +} + func (r *Routing) merge(o *Routing) { if len(o.AlsoProxy) > 0 { r.AlsoProxy = o.AlsoProxy @@ -841,6 +883,30 @@ func (r *Routing) merge(o *Routing) { if o.RecursionBlockDuration > 0 { r.RecursionBlockDuration = o.RecursionBlockDuration } + if o.VirtualSubnet != defaultVirtualSubnet { + r.VirtualSubnet = o.VirtualSubnet + } + if o.AutoResolveConflicts != defaultAutoResolveConflicts { //nolint:gosimple // keep for the semantic clarity + r.AutoResolveConflicts = o.AutoResolveConflicts + } +} + +// IsZero controls whether this element will be included in marshalled output. +func (r *Routing) IsZero() bool { + return r == nil || isDefault(r) +} + +func (r *Routing) MarshalJSONV2(out *jsontext.Encoder, opts json.Options) error { + return json.MarshalEncode(out, mapWithoutDefaults(r), opts) +} + +func (r *Routing) UnmarshalJSONV2(in *jsontext.Decoder, opts json.Options) error { + // Prevent that the original object is cleared when an empty object is decoded by passing the address + // of the pointer to the object. The unmarshal will then instead clear the pointer (wp becomes nil) and + // leave the underlying object intact. In other words, this code achieves "omitempty" during unmarshal. + type wt Routing + wp := (*wt)(r) + return json.UnmarshalDecode(in, &wp, opts) } func (d *DNS) Equal(o *DNS) bool { @@ -947,7 +1013,7 @@ var defaultConfig = BaseConfig{ //nolint:gochecknoglobals // constant InterceptV: defaultIntercept, ClusterV: defaultCluster, DNSV: defaultDNS, - RoutingV: Routing{}, + RoutingV: defaultRouting, } // GetDefaultBaseConfig returns the default configuration settings. @@ -1003,19 +1069,6 @@ func LoadConfig(c context.Context) (cfg Config, err error) { return cfg, nil } -type Routing struct { - Subnets []netip.Prefix `json:"subnets,omitempty"` - AlsoProxy []netip.Prefix `json:"alsoProxySubnets,omitempty"` - NeverProxy []netip.Prefix `json:"neverProxySubnets,omitempty"` - AllowConflicting []netip.Prefix `json:"allowConflictingSubnets,omitempty"` - RecursionBlockDuration time.Duration `json:"recursionBlockDuration,omitempty"` - - // For backward compatibility. - OldAlsoProxy []netip.Prefix `json:"alsoProxy,omitempty"` - OldNeverProxy []netip.Prefix `json:"neverProxy,omitempty"` - OldAllowConflicting []netip.Prefix `json:"allowConflicting,omitempty"` -} - // RoutingSnake is the same as Routing but with snake_case json/yaml names. type RoutingSnake struct { Subnets []netip.Prefix `json:"subnets"` @@ -1023,6 +1076,8 @@ type RoutingSnake struct { NeverProxy []netip.Prefix `json:"never_proxy_subnets"` AllowConflicting []netip.Prefix `json:"allow_conflicting_subnets"` RecursionBlockDuration time.Duration `json:"recursion_block_duration"` + VirtualSubnet netip.Prefix `json:"virtual_subnet"` + AutoResolveConflicts bool `json:"auto_resolve_conflicts"` } type DNS struct { @@ -1119,10 +1174,11 @@ func DNSFromRPC(s *daemon.DNSConfig) *DNS { func (r *Routing) ToSnake() *RoutingSnake { return &RoutingSnake{ - Subnets: r.Subnets, - AlsoProxy: r.AlsoProxy, - NeverProxy: r.NeverProxy, - AllowConflicting: r.AllowConflicting, + Subnets: r.Subnets, + AlsoProxy: r.AlsoProxy, + NeverProxy: r.NeverProxy, + AllowConflicting: r.AllowConflicting, + AutoResolveConflicts: r.AutoResolveConflicts, } } diff --git a/pkg/client/config_test.go b/pkg/client/config_test.go index 41a03f5212..a7ff5b1a16 100644 --- a/pkg/client/config_test.go +++ b/pkg/client/config_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "net/netip" "os" "path/filepath" "testing" @@ -48,8 +49,8 @@ intercept: appProtocolStrategy: portName defaultPort: 9080 useFtp: true -cluster: - virtualIPSubnet: 192.169.0.0/16 +routing: + virtualSubnet: 192.169.0.0/16 `, } @@ -90,7 +91,7 @@ cluster: assert.Equal(t, 9080, cfg.Intercept().DefaultPort) // from user assert.True(t, cfg.Intercept().UseFtp) // from user assert.Equal(t, cfg.Cluster().DefaultManagerNamespace, "hello") // from sys1 - assert.Equal(t, cfg.Cluster().VirtualIPSubnet, "192.169.0.0/16") // from user + assert.Equal(t, cfg.Routing().VirtualSubnet, netip.MustParsePrefix("192.169.0.0/16")) // from user } func Test_ConfigMarshalYAML(t *testing.T) { diff --git a/pkg/client/config_unix.go b/pkg/client/config_unix.go index 46fc080b38..6a60b331df 100644 --- a/pkg/client/config_unix.go +++ b/pkg/client/config_unix.go @@ -2,8 +2,10 @@ package client -// defaultVirtualIPSubnet A randomly chosen class E subnet. -const defaultVirtualIPSubnet = "246.246.0.0/16" +import "net/netip" + +// defaultVirtualSubnet A randomly chosen class E subnet. +var defaultVirtualSubnet = netip.MustParsePrefix("246.246.0.0/16") //nolint:gochecknoglobals // constant type OSSpecificConfig struct{} diff --git a/pkg/client/config_windows.go b/pkg/client/config_windows.go index 7c6369dc9e..5e758044e9 100644 --- a/pkg/client/config_windows.go +++ b/pkg/client/config_windows.go @@ -1,5 +1,7 @@ package client +import "net/netip" + type OSSpecificConfig struct { Network Network `json:"network,omitzero"` } @@ -21,12 +23,12 @@ type GSCStrategy string const ( defaultDNSWithFallback = true - - // defaultVirtualIPSubnet is an IP that, on windows, is built from 16 class C subnets which were chosen randomly, - // hoping that they don't collide with another subnet. - defaultVirtualIPSubnet = "211.55.48.0/20" ) +// defaultVirtualSubnet is an IP that, on windows, is built from 16 class C subnets which were chosen randomly, +// hoping that they don't collide with another subnet. +var defaultVirtualSubnet = netip.MustParsePrefix("211.55.48.0/20") //nolint:gochecknoglobals // constant + type Network struct { DNSWithFallback bool `json:"dnsWithFallback,omitempty"` } diff --git a/pkg/client/rootd/in_process.go b/pkg/client/rootd/in_process.go index e3d0461e2c..52000abb18 100644 --- a/pkg/client/rootd/in_process.go +++ b/pkg/client/rootd/in_process.go @@ -105,6 +105,11 @@ func (rd *InProcSession) SetLogLevel(context.Context, *manager.LogLevelRequest, return &empty.Empty{}, nil } +func (rd *InProcSession) TranslateEnvIPs(ctx context.Context, in *rpc.Environment, opts ...grpc.CallOption) (*rpc.Environment, error) { + in = rd.translateEnvIPs(ctx, in) + return in, nil +} + func (rd *InProcSession) WaitForNetwork(ctx context.Context, _ *empty.Empty, _ ...grpc.CallOption) (*empty.Empty, error) { if err, ok := <-rd.networkReady(ctx); ok { return &empty.Empty{}, status.Error(codes.Unavailable, err.Error()) diff --git a/pkg/client/rootd/service.go b/pkg/client/rootd/service.go index ee8c3e63c9..c5395e7fe2 100644 --- a/pkg/client/rootd/service.go +++ b/pkg/client/rootd/service.go @@ -217,6 +217,14 @@ func (s *Service) Disconnect(ctx context.Context, _ *emptypb.Empty) (*emptypb.Em return &emptypb.Empty{}, nil } +func (s *Service) TranslateEnvIPs(ctx context.Context, environment *rpc.Environment) (result *rpc.Environment, err error) { + err = s.WithSession(func(ctx context.Context, session *Session) error { + result = session.translateEnvIPs(ctx, environment) + return nil + }) + return result, err +} + func (s *Service) WaitForNetwork(ctx context.Context, e *emptypb.Empty) (*emptypb.Empty, error) { err := s.WithSession(func(ctx context.Context, session *Session) error { if err, ok := <-session.networkReady(ctx); ok { @@ -286,7 +294,7 @@ func (s *Service) SetLogLevel(ctx context.Context, request *manager.LogLevelRequ } func (s *Service) configReload(c context.Context) error { - return client.Watch(c, func(c context.Context) error { + return client.WatchConfig(c, func(c context.Context) error { return client.ReloadDaemonLogLevel(c, true) }) } diff --git a/pkg/client/rootd/session.go b/pkg/client/rootd/session.go index 855200d092..e424b904aa 100644 --- a/pkg/client/rootd/session.go +++ b/pkg/client/rootd/session.go @@ -22,9 +22,6 @@ import ( dns2 "github.com/miekg/dns" "github.com/puzpuzpuz/xsync/v3" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -428,13 +425,13 @@ func (s *Session) clusterLookup(ctx context.Context, q *dns2.Question) (dnsproxy switch rr := rr.(type) { case *dns2.A: var addr netip.Addr - addr, err = s.maybeGetVirtualIP(ctx, netip.AddrFrom4([4]byte(rr.A))) + addr, err = s.GetLocalIP(ctx, netip.AddrFrom4([4]byte(rr.A))) if err == nil { rr.A = addr.AsSlice() } case *dns2.AAAA: var addr netip.Addr - addr, err = s.maybeGetVirtualIP(ctx, netip.AddrFrom16([16]byte(rr.AAAA))) + addr, err = s.GetLocalIP(ctx, netip.AddrFrom16([16]byte(rr.AAAA))) if err == nil { rr.AAAA = addr.AsSlice() } @@ -448,7 +445,7 @@ func (s *Session) clusterLookup(ctx context.Context, q *dns2.Question) (dnsproxy return answer, rCode, err } -func (s *Session) maybeGetVirtualIP(ctx context.Context, destinationIP netip.Addr) (netip.Addr, error) { +func (s *Session) GetLocalIP(ctx context.Context, destinationIP netip.Addr) (netip.Addr, error) { var err error va, ok := s.localTranslationTable.Compute(destinationIP, func(existing netip.Addr, loaded bool) (netip.Addr, bool) { if loaded { @@ -611,27 +608,25 @@ func (s *Session) watchClusterInfo(ctx context.Context) error { } break } - ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "ClusterInfoUpdate") if err = s.readAdditionalRouting(ctx, mgrInfo); err != nil { return err } select { case <-s.vifReady: - if err := s.onClusterInfo(ctx, mgrInfo, span); err != nil { + if err := s.onClusterInfo(ctx, mgrInfo); err != nil { if !errors.Is(err, context.Canceled) { dlog.Error(ctx, err) } return err } default: - if err = s.onFirstClusterInfo(ctx, mgrInfo, span); err != nil { + if err = s.onFirstClusterInfo(ctx, mgrInfo); err != nil { if !errors.Is(err, context.Canceled) { dlog.Error(ctx, err) } return err } } - span.End() } dtime.SleepWithContext(ctx, backoff) backoff *= 2 @@ -667,7 +662,7 @@ func (s *Session) createSubnetForDNSOnly(ctx context.Context, mgrInfo *manager.C } } -func (s *Session) onFirstClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInfo, span trace.Span) (err error) { +func (s *Session) onFirstClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInfo) (err error) { defer func() { if err != nil { s.vifReady <- err @@ -682,14 +677,50 @@ func (s *Session) onFirstClusterInfo(ctx context.Context, mgrInfo *manager.Clust if ctx.Err() != nil { return ctx.Err() } - span.SetAttributes( - attribute.Bool("tel2.proxy-svcs", s.proxyClusterSvcs), - attribute.Bool("tel2.proxy-pods", s.proxyClusterPods), - ) - return s.onClusterInfo(ctx, mgrInfo, span) + return s.onClusterInfo(ctx, mgrInfo) } -func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInfo, span trace.Span) (err error) { +func (s *Session) defaultRouteDNS(ctx context.Context, mgrInfo *manager.ClusterInfo, dnsAddr netip.Addr, subnets []netip.Prefix) (netip.Addr, []netip.Prefix, error) { + // We'll need to synthesize a subnet where we can attach the DNS service when the VIF isn't configured + // from cluster subnets. But not on darwin systems, because there the DNS is controlled by /etc/resolver + // entries appointing the DNS service directly via localhost:. + if s.vipGenerator != nil { + if !s.dnsServerSubnet.IsValid() { + s.createSubnetForDNSOnly(ctx, mgrInfo) + } + dlog.Infof(ctx, "Adding Service subnet %s (for DNS only)", s.dnsServerSubnet) + var wl string + for _, snw := range s.subnetViaWorkloads { + if sn, err := netip.ParsePrefix(snw.Subnet); err == nil && sn.Contains(dnsAddr) { + wl = snw.Workload + if wl == "local" { + wl = "" + } + } + } + s.localTranslationSubnets = append(s.localTranslationSubnets, agentSubnet{ + Prefix: s.dnsServerSubnet, + workload: wl, + }) + var err error + dnsAddr, err = s.GetLocalIP(ctx, dnsAddr) + if err != nil { + return dnsAddr, subnets, err + } + } else { + if !s.dnsServerSubnet.IsValid() { + s.createSubnetForDNSOnly(ctx, mgrInfo) + } + dlog.Infof(ctx, "Adding Service subnet %s (for DNS only)", s.dnsServerSubnet) + subnets = append(subnets, s.dnsServerSubnet) + dnsIP := s.dnsServerSubnet.Addr().AsSlice() + dnsIP[len(dnsIP)-1] = 2 + dnsAddr, _ = netip.AddrFromSlice(dnsIP) + } + return dnsAddr, subnets, nil +} + +func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInfo) (err error) { if s.podDaemon { return nil } @@ -749,7 +780,7 @@ func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInf return fmt.Errorf("invalid traffic-manager pod ip address") } if s.vipGenerator != nil { - dnsAddr, err = s.maybeGetVirtualIP(ctx, dnsAddr) + dnsAddr, err = s.GetLocalIP(ctx, dnsAddr) if err != nil { return err } @@ -762,40 +793,9 @@ func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInf } } if runtime.GOOS != "darwin" && !dnsRouted { - // We'll need to synthesize a subnet where we can attach the DNS service when the VIF isn't configured - // from cluster subnets. But not on darwin systems, because there the DNS is controlled by /etc/resolver - // entries appointing the DNS service directly via localhost:. - if s.vipGenerator != nil { - if !s.dnsServerSubnet.IsValid() { - s.createSubnetForDNSOnly(ctx, mgrInfo) - } - dlog.Infof(ctx, "Adding Service subnet %s (for DNS only)", s.dnsServerSubnet) - var wl string - for _, snw := range s.subnetViaWorkloads { - if sn, err := netip.ParsePrefix(snw.Subnet); err == nil && sn.Contains(dnsAddr) { - wl = snw.Workload - if wl == "local" { - wl = "" - } - } - } - s.localTranslationSubnets = append(s.localTranslationSubnets, agentSubnet{ - Prefix: s.dnsServerSubnet, - workload: wl, - }) - dnsAddr, err = s.maybeGetVirtualIP(ctx, dnsAddr) - if err != nil { - return err - } - } else { - if !s.dnsServerSubnet.IsValid() { - s.createSubnetForDNSOnly(ctx, mgrInfo) - } - dlog.Infof(ctx, "Adding Service subnet %s (for DNS only)", s.dnsServerSubnet) - subnets = append(subnets, s.dnsServerSubnet) - dnsIP := s.dnsServerSubnet.Addr().AsSlice() - dnsIP[len(dnsIP)-1] = 2 - dnsAddr, _ = netip.AddrFromSlice(dnsIP) + dnsAddr, subnets, err = s.defaultRouteDNS(ctx, mgrInfo, dnsAddr, subnets) + if err != nil { + return err } dnsRouted = true } @@ -812,54 +812,53 @@ func (s *Session) onClusterInfo(ctx context.Context, mgrInfo *manager.ClusterInf dlog.Infof(ctx, "Setting cluster DNS to %s", dnsAddr) dlog.Infof(ctx, "Setting cluster domain to %q", d.ClusterDomain) s.dnsServer.SetClusterDNS(d, dnsAddr) - span.SetAttributes( - attribute.Stringer("tel2.cluster-dns", dnsAddr), - attribute.String("tel2.cluster-domain", d.ClusterDomain), - ) } proxy, neverProxy, neverProxyOverrides := computeNeverProxyOverrides(ctx, subnets, s.neverProxySubnets) s.effectiveNeverProxy = neverProxy - - // Fire and forget to send metrics out. - go func() { - scout.Report(ctx, "update_routes", - scout.Entry{Key: "subnets", Value: len(proxy)}, - scout.Entry{Key: "allow_conflicting_subnets", Value: len(s.allowConflictingSubnets)}, - ) - }() if s.tunVif == nil { return nil } rt := s.tunVif.Router rt.UpdateWhitelist(s.allowConflictingSubnets) + + err = rt.ValidateRoutes(ctx, proxy) + if err != nil { + if s.vipGenerator != nil || !client.GetConfig(ctx).Routing().AutoResolveConflicts { + return err + } + // Check each subnet and add a translation for those that conflict. + for _, pp := range proxy { + if routeConflict := rt.ValidateRoutes(ctx, []netip.Prefix{pp}); routeConflict != nil { + dlog.Infof(ctx, "Translating IPs in conflicting subnet %s to the virtual subnet", pp) + s.subnetViaWorkloads = append(s.subnetViaWorkloads, &rpc.SubnetViaWorkload{ + Subnet: pp.String(), + Workload: "local", + }) + } + } + if aErr := s.activateProxyViaWorkloads(ctx); aErr != nil { + dlog.Errorf(ctx, "activateProxyViaWorkloads: %v", aErr) + return err + } + return s.onClusterInfo(ctx, mgrInfo) + } + + dlog.Debugf(ctx, "UpdatinRoutes %s, %s, %s", proxy, s.effectiveNeverProxy, neverProxyOverrides) return rt.UpdateRoutes(ctx, proxy, s.effectiveNeverProxy, neverProxyOverrides) } func computeNeverProxyOverrides(ctx context.Context, subnets, nvp []netip.Prefix) (proxy, neverProxy, neverProxyOverrides []netip.Prefix) { - neverProxy = slices.Clone(nvp) - last := len(neverProxy) - 1 - for i := 0; i <= last; { - nps := neverProxy[i] - found := false + neverProxy = slices.DeleteFunc(slices.Clone(nvp), func(nps netip.Prefix) bool { for _, ds := range subnets { if ds.Overlaps(nps) { - found = true - break + return false } } - if !found { - // This never-proxy is pointless because it's not a subnet that we are routing - dlog.Infof(ctx, "Dropping never-proxy %q because it is not routed", nps) - if last > i { - neverProxy[i] = neverProxy[last] - } - last-- - } else { - i++ - } - } - neverProxy = neverProxy[:last+1] + // This never-proxy is pointless because it's not a subnet that we are routing + dlog.Infof(ctx, "Dropping never-proxy %q because it is not routed", nps) + return true + }) proxy, neverProxyOverrides = subnet.Partition(subnets, func(i int, isn netip.Prefix) bool { for r, rsn := range subnets { @@ -951,7 +950,7 @@ func (s *Session) checkSvcConnectivity(ctx context.Context, info *manager.Cluste // Skip checking the cert because its trust chain is loaded into a secret on the cluster; we'd fail to verify it TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := &http.Client{Transport: tr} + hcl := &http.Client{Transport: tr} tCtx, tCancel := context.WithTimeout(ctx, ct) defer tCancel() url := iputil.JoinHostPort(ip, uint16(port)) @@ -968,7 +967,7 @@ func (s *Session) checkSvcConnectivity(ctx context.Context, info *manager.Cluste } request.Header.Set("Host", info.InjectorSvcHost) dlog.Debugf(ctx, "Performing service connectivity check on %s with Host %s and timeout %s", url, info.InjectorSvcHost, ct) - resp, err := client.Do(request) + resp, err := hcl.Do(request) if err != nil { if ctx.Err() != nil { return false // parent context cancelled @@ -1054,9 +1053,6 @@ func (s *Session) Start(c context.Context, g *dgroup.Group) error { if clusterCfg.AgentPortForward && clusterCfg.ConnectFromRootDaemon { if k8sclient.CanPortForward(c, s.namespace) { s.agentClients = agentpf.NewClients(s.session) - if err := s.activateProxyViaWorkloads(c); err != nil { - return err - } g.Go("agentPods", func(ctx context.Context) error { return s.agentClients.WatchAgentPods(ctx, rmc.RealManagerClient()) }) @@ -1065,6 +1061,9 @@ func (s *Session) Start(c context.Context, g *dgroup.Group) error { } } } + if err := s.activateProxyViaWorkloads(c); err != nil { + return err + } if s.podDaemon { return nil } @@ -1162,17 +1161,17 @@ func (s *Session) activateProxyViaWorkloads(ctx context.Context) error { if sl == 0 { return nil } - vipSubnet, err := netip.ParsePrefix(client.GetConfig(ctx).Cluster().VirtualIPSubnet) - if err != nil { - return fmt.Errorf("unable to parse configuration value cluster.virtualIPSubnet: %w", err) - } + vipSubnet := client.GetConfig(ctx).Routing().VirtualSubnet dlog.Debugf(ctx, "ProxyVIA using subnet %s", vipSubnet) s.vipGenerator = vip.NewGenerator(vipSubnet) s.localTranslationSubnets = make([]agentSubnet, sl) for _, wlName := range s.consolidateProxyViaWorkloads(ctx) { + if s.agentClients == nil { + return errcat.User.Newf("Agent port-forwards are disabled. Client is not permitted to do proxy-via %s", wlName) + } dlog.Debugf(ctx, "Ensuring proxy-via agent in %s", wlName) - _, err = s.managerClient.EnsureAgent(ctx, &manager.EnsureAgentRequest{ + _, err := s.managerClient.EnsureAgent(ctx, &manager.EnsureAgentRequest{ Session: s.session, Name: wlName, }) @@ -1280,6 +1279,29 @@ func (s *Session) SetMappings(ctx context.Context, mappings []*rpc.DNSMapping) { s.dnsServer.SetMappings(mappings) } +func (s *Session) translateEnvIPs(ctx context.Context, environment *rpc.Environment) *rpc.Environment { + vip.TranslateEnvironmentIPs(ctx, environment.Env, s) + return environment +} + +func (s *Session) MapsIPv4() bool { + for _, p := range s.localTranslationSubnets { + if p.Addr().Is4() { + return true + } + } + return false +} + +func (s *Session) MapsIPv6() bool { + for _, p := range s.localTranslationSubnets { + if p.Addr().Is6() { + return true + } + } + return false +} + func (s *Session) waitForAgentIP(ctx context.Context, request *rpc.WaitForAgentIPRequest) (*rpc.WaitForAgentIPResponse, error) { if s.agentClients == nil { return nil, status.Error(codes.Unavailable, "") @@ -1299,7 +1321,7 @@ func (s *Session) waitForAgentIP(ctx context.Context, request *rpc.WaitForAgentI err = status.Error(codes.Internal, err.Error()) } if err == nil { - ip, err = s.maybeGetVirtualIP(ctx, ip) + ip, err = s.GetLocalIP(ctx, ip) } if err != nil { return nil, err diff --git a/pkg/client/rootd/vip/env_nat.go b/pkg/client/rootd/vip/env_nat.go new file mode 100644 index 0000000000..1208761f5a --- /dev/null +++ b/pkg/client/rootd/vip/env_nat.go @@ -0,0 +1,50 @@ +package vip + +import ( + "context" + "net/netip" + "regexp" + "sort" +) + +var ( + ipV4Rx = regexp.MustCompile(`(?:\d{1,3}\.){3}\d{1,3}`) //nolint:gochecknoglobals // constant + ipV6Rx = regexp.MustCompile(`(?:[0-9a-fA-F]{0,4}:){1,7}(?:[0-9a-fA-F]{0,4}%[0-9a-zA-Z]+|(?:\d{1,3}\.){3}\d{1,3}|)`) //nolint:gochecknoglobals // constant +) + +type LocalIPProvider interface { + MapsIPv4() bool + MapsIPv6() bool + GetLocalIP(ctx context.Context, remoteIP netip.Addr) (netip.Addr, error) +} + +func replaceIP(ctx context.Context, provider LocalIPProvider, rx *regexp.Regexp, s string) string { + return rx.ReplaceAllStringFunc(s, func(s string) string { + if ip, err := netip.ParseAddr(s); err == nil { + if rip, err := provider.GetLocalIP(ctx, ip); err == nil { + return rip.String() + } + } + return s + }) +} + +func TranslateEnvironmentIPs(ctx context.Context, env map[string]string, provider LocalIPProvider) { + ks := make([]string, len(env)) + i := 0 + for k := range env { + ks[i] = k + i++ + } + sort.Strings(ks) + if provider.MapsIPv4() { + for _, k := range ks { + env[k] = replaceIP(ctx, provider, ipV4Rx, env[k]) + } + } + if provider.MapsIPv6() { + for _, k := range ks { + env[k] = replaceIP(ctx, provider, ipV6Rx, env[k]) + } + } +} diff --git a/pkg/client/rootd/vip/env_nat_test.go b/pkg/client/rootd/vip/env_nat_test.go new file mode 100644 index 0000000000..6232eb83d4 --- /dev/null +++ b/pkg/client/rootd/vip/env_nat_test.go @@ -0,0 +1,123 @@ +package vip + +import ( + "context" + "net/netip" + "testing" + + "golang.org/x/exp/maps" + + "github.com/datawire/dlib/dlog" +) + +type localIPProviderTest struct { + cidrs []netip.Prefix + generator Generator + mapped map[netip.Addr]netip.Addr +} + +func (l *localIPProviderTest) MapsIPv4() bool { + for _, p := range l.cidrs { + if p.Addr().Is4() { + return true + } + } + return false +} + +func (l *localIPProviderTest) MapsIPv6() bool { + for _, p := range l.cidrs { + if p.Addr().Is6() { + return true + } + } + return false +} + +func (l *localIPProviderTest) GetLocalIP(ctx context.Context, remoteIP netip.Addr) (netip.Addr, error) { + if lip, ok := l.mapped[remoteIP]; ok { + return lip, nil + } + dlog.Infof(ctx, "mapping %s", remoteIP) + for _, p := range l.cidrs { + if p.Contains(remoteIP) { + lip, err := l.generator.Next() + if err != nil { + return remoteIP, err + } + l.mapped[remoteIP] = lip + return lip, nil + } + } + return remoteIP, nil +} + +func Test_translateEnvironmentIPs(t *testing.T) { + tests := []struct { + name string + cidr string + vCidr string + ip string + want string + }{ + { + "IPV4 URI", + "10.110.210.0/24", + "100.156.200.0/24", + "tcp://10.110.210.159:80", + "tcp://100.156.200.1:80", + }, + { + "IPV4", + "10.110.210.0/24", + "100.156.200.0/24", + "10.110.210.159", + "100.156.200.1", + }, + { + "IPV4 list", + "10.110.210.0/24", + "100.156.200.0/24", + `["10.110.210.8", "10.110.210.9", "10.110.210.9", "192.168.1.3"]`, + `["100.156.200.1", "100.156.200.2", "100.156.200.2", "192.168.1.3"]`, + }, + { + "IPV4 in IPV6", + "::ffff:10.110.210.0/96", + "::ffff:100.156.200.0/120", + "::ffff:10.110.210.8", + "::ffff:100.156.200.1", + }, + { + "IPV6 URI", + "::ffff:10.110.210.0/96", + "::ffff:100.156.200.0/120", + "tcp://[::ffff:10.110.210.8]:53", + "tcp://[::ffff:100.156.200.1]:53", + }, + { + "IPV4 leading ndot", + "100.156.200.0/24", + "10.110.210.0/24", + "2.10.110.210.8", + "2.10.110.210.8", + }, + } + + ctx := dlog.NewTestContext(t, false) + for _, tt := range tests { + provider := &localIPProviderTest{ + generator: NewGenerator(netip.MustParsePrefix(tt.vCidr)), + mapped: make(map[netip.Addr]netip.Addr), + cidrs: []netip.Prefix{netip.MustParsePrefix(tt.cidr)}, + } + t.Run(tt.name, func(t *testing.T) { + env := map[string]string{"key": tt.ip} + want := map[string]string{"key": tt.want} + TranslateEnvironmentIPs(ctx, env, provider) + if !maps.Equal(env, want) { + t.Errorf("TranslateEnvironmentIPs() = %v, want %v", env, want) + } + }) + } +} diff --git a/pkg/client/userd/daemon/service.go b/pkg/client/userd/daemon/service.go index d7945e09fb..2bb69a83fc 100644 --- a/pkg/client/userd/daemon/service.go +++ b/pkg/client/userd/daemon/service.go @@ -205,7 +205,7 @@ func (s *service) configReload(c context.Context) error { if err := os.MkdirAll(filepath.Dir(client.GetConfigFile(c)), 0o755); err != nil { return err } - return client.Watch(c, func(ctx context.Context) error { + return client.WatchConfig(c, func(ctx context.Context) error { s.sessionLock.RLock() defer s.sessionLock.RUnlock() if s.session == nil { diff --git a/pkg/client/userd/k8s/k8s_cluster.go b/pkg/client/userd/k8s/k8s_cluster.go index 4e77ec5c05..a4be621286 100644 --- a/pkg/client/userd/k8s/k8s_cluster.go +++ b/pkg/client/userd/k8s/k8s_cluster.go @@ -264,7 +264,7 @@ func (kc *Cluster) determineTrafficManagerNamespace(c context.Context) (string, // GetCurrentNamespaces returns the names of the namespaces that this client // is mapping. If the forClientAccess is true, then the namespaces are restricted // to those where an intercept can take place, i.e. the namespaces where this -// client can Watch and get services and deployments. +// client can WatchConfig and get services and deployments. func (kc *Cluster) GetCurrentNamespaces(forClientAccess bool) []string { kc.nsLock.Lock() nss := make([]string, 0, len(kc.currentMappedNamespaces)) diff --git a/pkg/client/userd/trafficmgr/agents.go b/pkg/client/userd/trafficmgr/agents.go index a5765e08a6..7282501636 100644 --- a/pkg/client/userd/trafficmgr/agents.go +++ b/pkg/client/userd/trafficmgr/agents.go @@ -7,6 +7,7 @@ import ( "io" "slices" + "github.com/datawire/dlib/dlog" "github.com/telepresenceio/telepresence/rpc/v2/manager" ) @@ -54,7 +55,12 @@ func (s *session) handleAgentSnapshot(ctx context.Context, infos []*manager.Agen if len(ais) > 0 { if slices.IndexFunc(ais, func(cai *manager.AgentInfo) bool { return cai.PodName == ig.PodName }) < 0 { // The pod selected for the ingest is no longer active, so replace it. - ig.AgentInfo = ais[0] + ai := ais[0] + err := s.translateContainerEnv(ctx, ai, ig.container) + if err != nil { + dlog.Errorf(ctx, "failed to translate container env: %v", err) + } + ig.AgentInfo = ai } s.ingestTracker.start(ig.podAccess(s.rootDaemon)) } diff --git a/pkg/client/userd/trafficmgr/ingest.go b/pkg/client/userd/trafficmgr/ingest.go index 305f22ba24..14cb106add 100644 --- a/pkg/client/userd/trafficmgr/ingest.go +++ b/pkg/client/userd/trafficmgr/ingest.go @@ -154,6 +154,11 @@ func (s *session) Ingest(ctx context.Context, rq *rpc.IngestRequest) (ir *rpc.In return nil, fmt.Errorf("workload %s has no container named %s", ik.workload, ik.container) } + err = s.translateContainerEnv(ctx, ai, ik.container) + if err != nil { + return nil, err + } + ig, loaded := s.currentIngests.LoadOrCompute(ik, func() *ingest { ctx, cancel := context.WithCancel(ctx) cancelIngest := func() { @@ -178,6 +183,19 @@ func (s *session) Ingest(ctx context.Context, rq *rpc.IngestRequest) (ir *rpc.In return ig.response(), nil } +func (s *session) translateContainerEnv(ctx context.Context, ai *manager.AgentInfo, container string) error { + cn, ok := ai.Containers[container] + if !ok { + return fmt.Errorf("workload %s has no container named %s", ai.Name, container) + } + env, err := s.rootDaemon.TranslateEnvIPs(ctx, &daemon.Environment{Env: cn.Environment}) + if err != nil { + return err + } + cn.Environment = env.Env + return nil +} + func (s *session) getCurrentIngests() []*rpc.IngestInfo { ingests := make([]*rpc.IngestInfo, 0, s.currentIngests.Size()) s.currentIngests.Range(func(key ingestKey, ig *ingest) bool { diff --git a/pkg/client/userd/trafficmgr/intercept.go b/pkg/client/userd/trafficmgr/intercept.go index 42de65c7a0..18c88e8111 100644 --- a/pkg/client/userd/trafficmgr/intercept.go +++ b/pkg/client/userd/trafficmgr/intercept.go @@ -517,9 +517,15 @@ func (s *session) AddIntercept(c context.Context, ir *rpc.CreateInterceptRequest return InterceptError(common.InterceptError_FAILED_TO_ESTABLISH, client.CheckTimeout(c, c.Err())) case <-wr.mountsDone: } + if er := self.InterceptEpilog(c, ir, result); er != nil { return er } + env, err := s.rootDaemon.TranslateEnvIPs(c, &daemon.Environment{Env: result.InterceptInfo.Environment}) + if err != nil { + return InterceptError(common.InterceptError_INTERNAL, client.CheckTimeout(c, err)) + } + result.InterceptInfo.Environment = env.Env success = true // Prevent removal in deferred function return result } diff --git a/pkg/vif/router.go b/pkg/vif/router.go index 857dc7c726..bbe84a355a 100644 --- a/pkg/vif/router.go +++ b/pkg/vif/router.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/netip" + "slices" "github.com/datawire/dlib/dlog" "github.com/telepresenceio/telepresence/v2/pkg/errcat" @@ -42,22 +43,24 @@ func (rt *Router) ValidateRoutes(ctx context.Context, routes []netip.Prefix) err if err != nil { return err } - _, nonWhitelisted := subnet.Partition(routes, func(_ int, r netip.Prefix) bool { + + nonWhitelisted := slices.DeleteFunc(slices.Clone(routes), func(r netip.Prefix) bool { for _, w := range rt.whitelistedSubnets { if subnet.Covers(w, r) { - // This is a whitelisted subnet, so we'll overlap it if needed return true } } for _, er := range table { - // Route is already in the routing table. - if r == er.RoutedNet { + if r == er.RoutedNet && er.Interface.Name == rt.device.Name() { + // Route is already in the routing table. return true } } return false }) - // Slightly awkward nested loops, since they can both continue (i.e., there are probably wasted iterations), but it's okay, there's not going to be hundreds of routes. + + // Slightly awkward nested loops, since they can both continue (i.e., there are probably wasted iterations), but it's + // okay, there's not going to be hundreds of routes. // In any case, we really wanna run over the table as the outer loop, since it's bigger. for _, tr := range table { dlog.Tracef(ctx, "checking for overlap with route %q", tr) @@ -79,11 +82,6 @@ func (rt *Router) ValidateRoutes(ctx context.Context, routes []netip.Prefix) err } func (rt *Router) UpdateRoutes(ctx context.Context, pleaseProxy, dontProxy, dontProxyOverrides []netip.Prefix) error { - // Don't never-proxy subnets that aren't routed - if err := rt.ValidateRoutes(ctx, pleaseProxy); err != nil { - return err - } - // Remove all current static routes so that they don't affect the routes for subnets // that we're about to add. rt.dropStaticOverrides(ctx) @@ -100,13 +98,13 @@ func (rt *Router) UpdateRoutes(ctx context.Context, pleaseProxy, dontProxy, dont }) // Remove already routed subnets from the pleaseProxy list - added, _ := subnet.Partition(pleaseProxy, func(_ int, sn netip.Prefix) bool { + added := slices.DeleteFunc(pleaseProxy, func(sn netip.Prefix) bool { for _, d := range rt.routedSubnets { if sn == d { - return false + return true } } - return true + return false }) // Add pleaseProxy subnets to the currently routed subnets diff --git a/pkg/vif/testdata/router/main.go b/pkg/vif/testdata/router/main.go index 0fd987c469..fd44241b92 100644 --- a/pkg/vif/testdata/router/main.go +++ b/pkg/vif/testdata/router/main.go @@ -81,6 +81,11 @@ func main() { } } dev.Router.UpdateWhitelist(whitelist) + err = dev.Router.ValidateRoutes(ctx, yesRoutes) + if err != nil { + return + } + err = dev.Router.UpdateRoutes(ctx, yesRoutes, noRoutes, nil) if err != nil { return diff --git a/rpc/daemon/daemon.pb.go b/rpc/daemon/daemon.pb.go index 5ccb55bb0c..52db7cfe51 100644 --- a/rpc/daemon/daemon.pb.go +++ b/rpc/daemon/daemon.pb.go @@ -654,6 +654,53 @@ func (x *WaitForAgentIPResponse) GetLocalIp() []byte { return nil } +type Environment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Env map[string]string `protobuf:"bytes,1,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *Environment) Reset() { + *x = Environment{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_daemon_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Environment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Environment) ProtoMessage() {} + +func (x *Environment) ProtoReflect() protoreflect.Message { + mi := &file_daemon_daemon_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Environment.ProtoReflect.Descriptor instead. +func (*Environment) Descriptor() ([]byte, []int) { + return file_daemon_daemon_proto_rawDescGZIP(), []int{10} +} + +func (x *Environment) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + var File_daemon_daemon_proto protoreflect.FileDescriptor var file_daemon_daemon_proto_rawDesc = []byte{ @@ -757,69 +804,83 @@ var file_daemon_daemon_proto_rawDesc = []byte{ 0x6f, 0x75, 0x74, 0x22, 0x33, 0x0a, 0x16, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x07, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x70, 0x32, 0xa0, 0x07, 0x0a, 0x06, 0x44, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, + 0x07, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x70, 0x22, 0x82, 0x01, 0x0a, 0x0b, 0x45, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, + 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x03, 0x65, 0x6e, 0x76, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xf7, 0x07, + 0x0a, 0x06, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x43, 0x0a, + 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x36, 0x0a, 0x04, 0x51, 0x75, 0x69, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x50, 0x0a, 0x07, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, + 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3c, 0x0a, 0x0a, + 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4e, 0x0a, 0x10, 0x47, 0x65, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x43, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x36, 0x0a, - 0x04, 0x51, 0x75, 0x69, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x50, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x12, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, - 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3c, 0x0a, 0x0a, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, + 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4d, 0x0a, 0x15, 0x53, 0x65, + 0x74, 0x44, 0x4e, 0x53, 0x54, 0x6f, 0x70, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, + 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x54, 0x0a, 0x0e, 0x53, 0x65, 0x74, + 0x44, 0x4e, 0x53, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x2a, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x54, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4e, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4d, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x54, - 0x6f, 0x70, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x16, 0x2e, 0x67, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x25, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, + 0x6e, 0x63, 0x65, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x55, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x76, 0x49, 0x50, 0x73, 0x12, 0x20, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, + 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x40, 0x0a, 0x0e, 0x57, 0x61, + 0x69, 0x74, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x54, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x45, 0x78, - 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, - 0x44, 0x4e, 0x53, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x54, 0x0a, 0x0e, 0x53, 0x65, - 0x74, 0x44, 0x4e, 0x53, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2a, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x4c, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, - 0x25, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x40, - 0x0a, 0x0e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x69, 0x0a, 0x0e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x49, 0x50, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, - 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x69, 0x0a, 0x0e, + 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x50, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x49, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, - 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x69, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x49, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x50, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, + 0x63, 0x65, 0x69, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, + 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -834,7 +895,7 @@ func file_daemon_daemon_proto_rawDescGZIP() []byte { return file_daemon_daemon_proto_rawDescData } -var file_daemon_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_daemon_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_daemon_daemon_proto_goTypes = []any{ (*DaemonStatus)(nil), // 0: telepresence.daemon.DaemonStatus (*Domains)(nil), // 1: telepresence.daemon.Domains @@ -846,52 +907,57 @@ var file_daemon_daemon_proto_goTypes = []any{ (*SetDNSMappingsRequest)(nil), // 7: telepresence.daemon.SetDNSMappingsRequest (*WaitForAgentIPRequest)(nil), // 8: telepresence.daemon.WaitForAgentIPRequest (*WaitForAgentIPResponse)(nil), // 9: telepresence.daemon.WaitForAgentIPResponse - nil, // 10: telepresence.daemon.NetworkConfig.KubeFlagsEntry - (*common.VersionInfo)(nil), // 11: telepresence.common.VersionInfo - (*durationpb.Duration)(nil), // 12: google.protobuf.Duration - (*manager.SessionInfo)(nil), // 13: telepresence.manager.SessionInfo - (*emptypb.Empty)(nil), // 14: google.protobuf.Empty - (*manager.LogLevelRequest)(nil), // 15: telepresence.manager.LogLevelRequest + (*Environment)(nil), // 10: telepresence.daemon.Environment + nil, // 11: telepresence.daemon.NetworkConfig.KubeFlagsEntry + nil, // 12: telepresence.daemon.Environment.EnvEntry + (*common.VersionInfo)(nil), // 13: telepresence.common.VersionInfo + (*durationpb.Duration)(nil), // 14: google.protobuf.Duration + (*manager.SessionInfo)(nil), // 15: telepresence.manager.SessionInfo + (*emptypb.Empty)(nil), // 16: google.protobuf.Empty + (*manager.LogLevelRequest)(nil), // 17: telepresence.manager.LogLevelRequest } var file_daemon_daemon_proto_depIdxs = []int32{ 5, // 0: telepresence.daemon.DaemonStatus.outbound_config:type_name -> telepresence.daemon.NetworkConfig - 11, // 1: telepresence.daemon.DaemonStatus.version:type_name -> telepresence.common.VersionInfo + 13, // 1: telepresence.daemon.DaemonStatus.version:type_name -> telepresence.common.VersionInfo 2, // 2: telepresence.daemon.DNSConfig.mappings:type_name -> telepresence.daemon.DNSMapping - 12, // 3: telepresence.daemon.DNSConfig.lookup_timeout:type_name -> google.protobuf.Duration - 13, // 4: telepresence.daemon.NetworkConfig.session:type_name -> telepresence.manager.SessionInfo + 14, // 3: telepresence.daemon.DNSConfig.lookup_timeout:type_name -> google.protobuf.Duration + 15, // 4: telepresence.daemon.NetworkConfig.session:type_name -> telepresence.manager.SessionInfo 4, // 5: telepresence.daemon.NetworkConfig.subnet_via_workloads:type_name -> telepresence.daemon.SubnetViaWorkload - 10, // 6: telepresence.daemon.NetworkConfig.kube_flags:type_name -> telepresence.daemon.NetworkConfig.KubeFlagsEntry + 11, // 6: telepresence.daemon.NetworkConfig.kube_flags:type_name -> telepresence.daemon.NetworkConfig.KubeFlagsEntry 2, // 7: telepresence.daemon.SetDNSMappingsRequest.mappings:type_name -> telepresence.daemon.DNSMapping - 12, // 8: telepresence.daemon.WaitForAgentIPRequest.timeout:type_name -> google.protobuf.Duration - 14, // 9: telepresence.daemon.Daemon.Version:input_type -> google.protobuf.Empty - 14, // 10: telepresence.daemon.Daemon.Status:input_type -> google.protobuf.Empty - 14, // 11: telepresence.daemon.Daemon.Quit:input_type -> google.protobuf.Empty - 5, // 12: telepresence.daemon.Daemon.Connect:input_type -> telepresence.daemon.NetworkConfig - 14, // 13: telepresence.daemon.Daemon.Disconnect:input_type -> google.protobuf.Empty - 14, // 14: telepresence.daemon.Daemon.GetNetworkConfig:input_type -> google.protobuf.Empty - 1, // 15: telepresence.daemon.Daemon.SetDNSTopLevelDomains:input_type -> telepresence.daemon.Domains - 6, // 16: telepresence.daemon.Daemon.SetDNSExcludes:input_type -> telepresence.daemon.SetDNSExcludesRequest - 7, // 17: telepresence.daemon.Daemon.SetDNSMappings:input_type -> telepresence.daemon.SetDNSMappingsRequest - 15, // 18: telepresence.daemon.Daemon.SetLogLevel:input_type -> telepresence.manager.LogLevelRequest - 14, // 19: telepresence.daemon.Daemon.WaitForNetwork:input_type -> google.protobuf.Empty - 8, // 20: telepresence.daemon.Daemon.WaitForAgentIP:input_type -> telepresence.daemon.WaitForAgentIPRequest - 11, // 21: telepresence.daemon.Daemon.Version:output_type -> telepresence.common.VersionInfo - 0, // 22: telepresence.daemon.Daemon.Status:output_type -> telepresence.daemon.DaemonStatus - 14, // 23: telepresence.daemon.Daemon.Quit:output_type -> google.protobuf.Empty - 0, // 24: telepresence.daemon.Daemon.Connect:output_type -> telepresence.daemon.DaemonStatus - 14, // 25: telepresence.daemon.Daemon.Disconnect:output_type -> google.protobuf.Empty - 5, // 26: telepresence.daemon.Daemon.GetNetworkConfig:output_type -> telepresence.daemon.NetworkConfig - 14, // 27: telepresence.daemon.Daemon.SetDNSTopLevelDomains:output_type -> google.protobuf.Empty - 14, // 28: telepresence.daemon.Daemon.SetDNSExcludes:output_type -> google.protobuf.Empty - 14, // 29: telepresence.daemon.Daemon.SetDNSMappings:output_type -> google.protobuf.Empty - 14, // 30: telepresence.daemon.Daemon.SetLogLevel:output_type -> google.protobuf.Empty - 14, // 31: telepresence.daemon.Daemon.WaitForNetwork:output_type -> google.protobuf.Empty - 9, // 32: telepresence.daemon.Daemon.WaitForAgentIP:output_type -> telepresence.daemon.WaitForAgentIPResponse - 21, // [21:33] is the sub-list for method output_type - 9, // [9:21] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 14, // 8: telepresence.daemon.WaitForAgentIPRequest.timeout:type_name -> google.protobuf.Duration + 12, // 9: telepresence.daemon.Environment.env:type_name -> telepresence.daemon.Environment.EnvEntry + 16, // 10: telepresence.daemon.Daemon.Version:input_type -> google.protobuf.Empty + 16, // 11: telepresence.daemon.Daemon.Status:input_type -> google.protobuf.Empty + 16, // 12: telepresence.daemon.Daemon.Quit:input_type -> google.protobuf.Empty + 5, // 13: telepresence.daemon.Daemon.Connect:input_type -> telepresence.daemon.NetworkConfig + 16, // 14: telepresence.daemon.Daemon.Disconnect:input_type -> google.protobuf.Empty + 16, // 15: telepresence.daemon.Daemon.GetNetworkConfig:input_type -> google.protobuf.Empty + 1, // 16: telepresence.daemon.Daemon.SetDNSTopLevelDomains:input_type -> telepresence.daemon.Domains + 6, // 17: telepresence.daemon.Daemon.SetDNSExcludes:input_type -> telepresence.daemon.SetDNSExcludesRequest + 7, // 18: telepresence.daemon.Daemon.SetDNSMappings:input_type -> telepresence.daemon.SetDNSMappingsRequest + 17, // 19: telepresence.daemon.Daemon.SetLogLevel:input_type -> telepresence.manager.LogLevelRequest + 10, // 20: telepresence.daemon.Daemon.TranslateEnvIPs:input_type -> telepresence.daemon.Environment + 16, // 21: telepresence.daemon.Daemon.WaitForNetwork:input_type -> google.protobuf.Empty + 8, // 22: telepresence.daemon.Daemon.WaitForAgentIP:input_type -> telepresence.daemon.WaitForAgentIPRequest + 13, // 23: telepresence.daemon.Daemon.Version:output_type -> telepresence.common.VersionInfo + 0, // 24: telepresence.daemon.Daemon.Status:output_type -> telepresence.daemon.DaemonStatus + 16, // 25: telepresence.daemon.Daemon.Quit:output_type -> google.protobuf.Empty + 0, // 26: telepresence.daemon.Daemon.Connect:output_type -> telepresence.daemon.DaemonStatus + 16, // 27: telepresence.daemon.Daemon.Disconnect:output_type -> google.protobuf.Empty + 5, // 28: telepresence.daemon.Daemon.GetNetworkConfig:output_type -> telepresence.daemon.NetworkConfig + 16, // 29: telepresence.daemon.Daemon.SetDNSTopLevelDomains:output_type -> google.protobuf.Empty + 16, // 30: telepresence.daemon.Daemon.SetDNSExcludes:output_type -> google.protobuf.Empty + 16, // 31: telepresence.daemon.Daemon.SetDNSMappings:output_type -> google.protobuf.Empty + 16, // 32: telepresence.daemon.Daemon.SetLogLevel:output_type -> google.protobuf.Empty + 10, // 33: telepresence.daemon.Daemon.TranslateEnvIPs:output_type -> telepresence.daemon.Environment + 16, // 34: telepresence.daemon.Daemon.WaitForNetwork:output_type -> google.protobuf.Empty + 9, // 35: telepresence.daemon.Daemon.WaitForAgentIP:output_type -> telepresence.daemon.WaitForAgentIPResponse + 23, // [23:36] is the sub-list for method output_type + 10, // [10:23] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_daemon_daemon_proto_init() } @@ -1020,6 +1086,18 @@ func file_daemon_daemon_proto_init() { return nil } } + file_daemon_daemon_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*Environment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_daemon_daemon_proto_msgTypes[5].OneofWrappers = []any{} type x struct{} @@ -1028,7 +1106,7 @@ func file_daemon_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_daemon_proto_rawDesc, NumEnums: 0, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/rpc/daemon/daemon.proto b/rpc/daemon/daemon.proto index 0e53b34c9e..40cb82c5dd 100644 --- a/rpc/daemon/daemon.proto +++ b/rpc/daemon/daemon.proto @@ -41,6 +41,9 @@ service Daemon { // SetLogLevel will temporarily set the log-level for the daemon for a duration that is determined b the request. rpc SetLogLevel(manager.LogLevelRequest) returns (google.protobuf.Empty); + // TranslateEnvIPs translates remote IPs found in the environment to local IPs + rpc TranslateEnvIPs(Environment) returns (Environment); + // WaitForNetwork waits for the network of the currently connected session to become ready. rpc WaitForNetwork(google.protobuf.Empty) returns (google.protobuf.Empty); @@ -145,3 +148,7 @@ message WaitForAgentIPResponse { // The local IP of the agent (might be virtual) bytes local_ip = 1; } + +message Environment { + map env = 1; +} \ No newline at end of file diff --git a/rpc/daemon/daemon_grpc.pb.go b/rpc/daemon/daemon_grpc.pb.go index 3c7b13bdbd..40bdcdf380 100644 --- a/rpc/daemon/daemon_grpc.pb.go +++ b/rpc/daemon/daemon_grpc.pb.go @@ -32,6 +32,7 @@ const ( Daemon_SetDNSExcludes_FullMethodName = "/telepresence.daemon.Daemon/SetDNSExcludes" Daemon_SetDNSMappings_FullMethodName = "/telepresence.daemon.Daemon/SetDNSMappings" Daemon_SetLogLevel_FullMethodName = "/telepresence.daemon.Daemon/SetLogLevel" + Daemon_TranslateEnvIPs_FullMethodName = "/telepresence.daemon.Daemon/TranslateEnvIPs" Daemon_WaitForNetwork_FullMethodName = "/telepresence.daemon.Daemon/WaitForNetwork" Daemon_WaitForAgentIP_FullMethodName = "/telepresence.daemon.Daemon/WaitForAgentIP" ) @@ -63,6 +64,8 @@ type DaemonClient interface { SetDNSMappings(ctx context.Context, in *SetDNSMappingsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // SetLogLevel will temporarily set the log-level for the daemon for a duration that is determined b the request. SetLogLevel(ctx context.Context, in *manager.LogLevelRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // TranslateEnvIPs translates remote IPs found in the environment to local IPs + TranslateEnvIPs(ctx context.Context, in *Environment, opts ...grpc.CallOption) (*Environment, error) // WaitForNetwork waits for the network of the currently connected session to become ready. WaitForNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) // WaitForAgentIP waits for the network of an intercepted agent to become ready. @@ -177,6 +180,16 @@ func (c *daemonClient) SetLogLevel(ctx context.Context, in *manager.LogLevelRequ return out, nil } +func (c *daemonClient) TranslateEnvIPs(ctx context.Context, in *Environment, opts ...grpc.CallOption) (*Environment, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Environment) + err := c.cc.Invoke(ctx, Daemon_TranslateEnvIPs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *daemonClient) WaitForNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -224,6 +237,8 @@ type DaemonServer interface { SetDNSMappings(context.Context, *SetDNSMappingsRequest) (*emptypb.Empty, error) // SetLogLevel will temporarily set the log-level for the daemon for a duration that is determined b the request. SetLogLevel(context.Context, *manager.LogLevelRequest) (*emptypb.Empty, error) + // TranslateEnvIPs translates remote IPs found in the environment to local IPs + TranslateEnvIPs(context.Context, *Environment) (*Environment, error) // WaitForNetwork waits for the network of the currently connected session to become ready. WaitForNetwork(context.Context, *emptypb.Empty) (*emptypb.Empty, error) // WaitForAgentIP waits for the network of an intercepted agent to become ready. @@ -265,6 +280,9 @@ func (UnimplementedDaemonServer) SetDNSMappings(context.Context, *SetDNSMappings func (UnimplementedDaemonServer) SetLogLevel(context.Context, *manager.LogLevelRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetLogLevel not implemented") } +func (UnimplementedDaemonServer) TranslateEnvIPs(context.Context, *Environment) (*Environment, error) { + return nil, status.Errorf(codes.Unimplemented, "method TranslateEnvIPs not implemented") +} func (UnimplementedDaemonServer) WaitForNetwork(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method WaitForNetwork not implemented") } @@ -464,6 +482,24 @@ func _Daemon_SetLogLevel_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Daemon_TranslateEnvIPs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Environment) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServer).TranslateEnvIPs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Daemon_TranslateEnvIPs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServer).TranslateEnvIPs(ctx, req.(*Environment)) + } + return interceptor(ctx, in, info, handler) +} + func _Daemon_WaitForNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -547,6 +583,10 @@ var Daemon_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetLogLevel", Handler: _Daemon_SetLogLevel_Handler, }, + { + MethodName: "TranslateEnvIPs", + Handler: _Daemon_TranslateEnvIPs_Handler, + }, { MethodName: "WaitForNetwork", Handler: _Daemon_WaitForNetwork_Handler,