From 85a4725e387cf299f96ac7bcfe1bf946dca21150 Mon Sep 17 00:00:00 2001 From: David Hadas Date: Sat, 21 Sep 2024 13:42:41 -0500 Subject: [PATCH] SecureComms: Add support for inbound namespace Support opening inbound ports in configrable network namespaces. This allows opening an inbound port inside the pod network namespace of the podvm. The traffic will be forwarded via the SecureComms ssh channel to the outbound at the worker node. Signed-off-by: David Hadas --- src/cloud-api-adaptor/docs/SecureComms.md | 6 ++- .../pkg/securecomms/sshproxy/sshproxy.go | 47 +++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/cloud-api-adaptor/docs/SecureComms.md b/src/cloud-api-adaptor/docs/SecureComms.md index 46ac8c4176..c284410c87 100644 --- a/src/cloud-api-adaptor/docs/SecureComms.md +++ b/src/cloud-api-adaptor/docs/SecureComms.md @@ -79,7 +79,7 @@ You may also include additional Inbounds and Outbounds configurations to the Ada You may also set the KBS address using the `SECURE_COMMS_KBS_ADDR` config point. -### Adding named tunnels to the SSH channel +## Adding named tunnels to the SSH channel Named tunnels can be added to the SSH channel. Adding a named tunnel requires adding an Inbound at one of the SSH channel peers and an Outbound at the other SSH channel peer. The Inbound and Outbound both carry the name of the tunnel being created. |---------Tunnel----------| @@ -88,9 +88,11 @@ Client->Inbound----------->Outbound->Server Inbounds and Outbounds take the form of a comma separated inbound/outbound tags such that Inbounds are formed as "InboundTag1,InboundTag2,InboundTag3,..." and Outbounds are formed as "OutboundTag1,OutboundTag2,outboundTag3,..." -Each Inbound tag is structured as `Phase:Name:Port` where: + +Each Inbound tag is structured as `Phase:Name:Namespace:Port` or `Phase:Name:Port` where: - Phase can be 'KUBERNETES_PHASE' to represent an outbound available during the Kubernetes phase, 'ATTESTATION_PHASE' to represent an outbound available during the Attestation phase, or 'BOTH_PHASES' to represent an outbound available during both phases. - Name is the name of the tunnel +- Namespace (if available) is a linux network namespace where the local service should be available. - Port is the local service port being opened to serve as ingress of the tunnel. Each outbound tag is structured as `Phase:Name:Host:Port` or `Phase:Name:Port` where: diff --git a/src/cloud-api-adaptor/pkg/securecomms/sshproxy/sshproxy.go b/src/cloud-api-adaptor/pkg/securecomms/sshproxy/sshproxy.go index 972eac8c9e..1300549e30 100644 --- a/src/cloud-api-adaptor/pkg/securecomms/sshproxy/sshproxy.go +++ b/src/cloud-api-adaptor/pkg/securecomms/sshproxy/sshproxy.go @@ -9,11 +9,13 @@ import ( "net/http" "net/http/httputil" "net/url" + "runtime" "strconv" "strings" "sync" "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/securecomms/sshutil" + "github.com/vishvananda/netns" "golang.org/x/crypto/ssh" ) @@ -102,11 +104,11 @@ func (inbounds *Inbounds) AddTags(tags []string, inboundPorts map[string]string, if tag == "" { continue } - inPort, _, name, phase, err := ParseTag(tag) + inPort, namespace, name, phase, err := ParseTag(tag) if err != nil { return fmt.Errorf("failed to parse inbound tag %s: %v", tag, err) } - retPort, err := inbounds.Add(inPort, name, phase, wg) + retPort, err := inbounds.Add(namespace, inPort, name, phase, wg) if err != nil { return fmt.Errorf("failed to add inbound: %v", err) } @@ -117,12 +119,49 @@ func (inbounds *Inbounds) AddTags(tags []string, inboundPorts map[string]string, return nil } -func (inbounds *Inbounds) Add(inPort int, name, phase string, wg *sync.WaitGroup) (string, error) { +func (inbounds *Inbounds) listen(namespace string, tcpAddr *net.TCPAddr, name string) (tcpListener *net.TCPListener, err error) { + if namespace != "" { + var podns, myns netns.NsHandle + var err error + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if podns, err = netns.GetFromName(namespace); err != nil { + logger.Printf("Inbound listen get podns '%s': %v", namespace, err) + return nil, fmt.Errorf("inbound %s failed to get other ns '%s': %w", name, namespace, err) + } + defer podns.Close() + if myns, err = netns.Get(); err != nil { + logger.Printf("Inbound listen get myns: %v", err) + return nil, fmt.Errorf("inbound %s failed to get my ns '%s': %w", name, namespace, err) + } + defer myns.Close() + + // switch ns + if err := netns.Set(podns); err != nil { + logger.Printf("Inbound listen setns '%s': %v", namespace, err) + return nil, fmt.Errorf("inbound %s failed to setns '%s': %w", name, namespace, err) + } + defer func() { + if err := netns.Set(myns); err != nil { + logger.Printf("Inbound listen revert setns: %v", err) + } else { + logger.Printf("Inbound %s listen reverted setns", name) + } + }() + + logger.Printf("Inbound %s switch ns %s success", name, namespace) + } + return net.ListenTCP("tcp", tcpAddr) +} + +func (inbounds *Inbounds) Add(namespace string, inPort int, name, phase string, wg *sync.WaitGroup) (string, error) { tcpAddr := &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: inPort, } - tcpListener, err := net.ListenTCP("tcp", tcpAddr) + tcpListener, err := inbounds.listen(namespace, tcpAddr, name) if err != nil { return "", fmt.Errorf("inbound failed to listen to host: %s port '%d' - err: %v", name, inPort, err) }