Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions cmd/ateom-gvisor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ var (
podUID = pflag.String("pod-uid", "", "The UID of the current pod")

// TODO(liorlieberman) have a sub package for all atunnel releated things like that
atunnelListenAddress = pflag.String("atunnel-listen-address", "0.0.0.0:443", "Address for actor ingress HTTPS")
atunnelConnectListenAddress = pflag.String("atunnel-connect-listen-address", "0.0.0.0:444", "Address for actor ingress mTLS CONNECT")
//
// Ingress is dual-stack; egress is pinned to IPv4 by atunnel.ListenEgressIPv4.
atunnelListenAddress = pflag.String("atunnel-listen-address", ":443", "Address for actor ingress HTTPS")
atunnelConnectListenAddress = pflag.String("atunnel-connect-listen-address", ":444", "Address for actor ingress mTLS CONNECT")
workerCredentialBundle = pflag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Worker Pod credential bundle used by atunnel for inbound serving and outbound mTLS")
podIdentityTrustBundle = pflag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "Pod identity trust bundle used for router clients and the node-local atelet")
atunnelClientIdentity = pflag.String("atunnel-client-identity", "spiffe://cluster.local/ns/ate-system/sa/atenet-router", "SPIFFE identity allowed to call actor ingress HTTPS")
Expand Down Expand Up @@ -277,7 +279,7 @@ func runAtunnel(ctx context.Context, upstream *url.URL) (*atunnel.Server, *atunn
if err != nil {
return nil, nil, 0, fmt.Errorf("while configuring atunnel egress: %w", err)
}
egressListener, err := net.Listen("tcp", *atunnelEgressListenAddress)
egressListener, err := atunnel.ListenEgressIPv4(*atunnelEgressListenAddress)
if err != nil {
return nil, nil, 0, fmt.Errorf("while opening atunnel egress listener: %w", err)
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/ateom-microvm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ var (
otlpRelaySocket = flag.String("otlp-relay-socket", ateompath.AteletOTLPSocketPath(),
"Unix socket of atelet's OTLP relay to export telemetry through, keeping it off the pod network. Empty, or absent at startup, exports directly to OTEL_EXPORTER_OTLP_ENDPOINT instead.")

atunnelListenAddress = flag.String("atunnel-listen-address", "0.0.0.0:443", "Address for actor ingress HTTPS")
atunnelConnectListenAddress = flag.String("atunnel-connect-listen-address", "0.0.0.0:444", "Address for actor ingress mTLS CONNECT")
// Ingress is dual-stack; egress is pinned to IPv4 by atunnel.ListenEgressIPv4.
atunnelListenAddress = flag.String("atunnel-listen-address", ":443", "Address for actor ingress HTTPS")
atunnelConnectListenAddress = flag.String("atunnel-connect-listen-address", ":444", "Address for actor ingress mTLS CONNECT")
workerCredentialBundle = flag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Worker Pod credential bundle used by atunnel for inbound serving and outbound mTLS")
podIdentityTrustBundle = flag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "Pod identity trust bundle used for router clients and the node-local atelet")
atunnelClientIdentity = flag.String("atunnel-client-identity", "spiffe://cluster.local/ns/ate-system/sa/atenet-router", "SPIFFE identity allowed to call actor ingress HTTPS")
Expand Down Expand Up @@ -236,7 +237,7 @@ func do(ctx context.Context) error {
if err != nil {
return fmt.Errorf("while configuring atunnel egress: %w", err)
}
egressListener, err := net.Listen("tcp", *atunnelEgressListenAddress)
egressListener, err := atunnel.ListenEgressIPv4(*atunnelEgressListenAddress)
if err != nil {
return fmt.Errorf("while opening atunnel egress listener: %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions internal/ateomnet/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,10 @@ func InstallActorNftablesRules(egressPort uint16) error {
// rules in an ateom-owned table makes cleanup simple and avoids mutating
// Kubernetes or CNI-managed chains directly.
//
// TODO: Add IPv6 veth addressing, forwarding, and nftables rules once actor
// networking supports dual-stack pods. The current actor network is IPv4-only.
// TODO(#945): Add IPv6 veth addressing, forwarding, and nftables rules once
// actor networking supports dual-stack pods. The current actor network is
// IPv4-only, which is why atunnel opens the listener these rules redirect to
// with ListenEgressIPv4.
//
// The rules do three things:
//
Expand Down
16 changes: 16 additions & 0 deletions internal/atunnel/egress.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ type actorCertificateSource interface {
// connection originally targeted.
type OriginalDestination func(net.Conn) (string, error)

// ListenEgressIPv4 opens the transparent egress listener on address. The family
// is in the name because it cannot be left to the address: Go treats every
// unspecified address as a wildcard, so net.Listen("tcp", "0.0.0.0:15001") binds
// "::" dual-stack and accepts IPv6 too. TCPOriginalDestination reads only the
// IPv4 SOL_IP/SO_ORIGINAL_DST, so such a connection is accepted and then fails.
//
// TODO(#945): use "tcp" once actor networking is dual-stack; the IPv6
// original-destination lookup is #686.
func ListenEgressIPv4(address string) (net.Listener, error) {
listener, err := net.Listen("tcp4", address)
if err != nil {
return nil, fmt.Errorf("atunnel: egress listener is IPv4-only: %w", err)
}
return listener, nil
}

// Egress proxies actor TCP connections through an egress CONNECT dialer. It is
// long-lived across actor activations, but only carries traffic while an actor
// is assigned to its worker.
Expand Down
48 changes: 48 additions & 0 deletions internal/atunnel/egress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io"
"net"
"net/http"
"strconv"
"strings"
"sync/atomic"
"testing"
Expand All @@ -31,6 +32,53 @@ import (
"google.golang.org/grpc/status"
)

// TestListenEgressIPv4Only pins the egress listener to IPv4, because
// TCPOriginalDestination can only recover an IPv4 original destination. The
// wildcard address does not carry that constraint. Every listener here uses the
// same address, "0.0.0.0:0", so the network string is the only variable.
func TestListenEgressIPv4Only(t *testing.T) {
// The negative control comes first because it decides whether the rest means
// anything: "tcp" and "tcp4" differ only on a host that binds "tcp"
// dual-stack, and only there can this test observe the bug it guards.
control, err := net.Listen("tcp", "0.0.0.0:0")
if err != nil {
t.Fatalf("net.Listen(%q, \"0.0.0.0:0\") = %v", "tcp", err)
}
t.Cleanup(func() { control.Close() })
if controlAddr := control.Addr().(*net.TCPAddr); controlAddr.IP.To4() != nil {
t.Skipf("net.Listen(%q, \"0.0.0.0:0\") bound IPv4 %v, so this host cannot "+
"distinguish it from %q", "tcp", controlAddr, "tcp4")
}

listener, err := ListenEgressIPv4("0.0.0.0:0")
if err != nil {
t.Fatalf("ListenEgressIPv4() = %v", err)
}
t.Cleanup(func() { listener.Close() })

addr, ok := listener.Addr().(*net.TCPAddr)
if !ok {
t.Fatalf("ListenEgressIPv4() bound %T, want *net.TCPAddr", listener.Addr())
}
if addr.IP.To4() == nil {
t.Fatalf("ListenEgressIPv4() bound %v, want an IPv4 address; net.Listen(%q, ...) "+
"bound %v on the same address, and an IPv6 client would be accepted and then "+
"fail TCPOriginalDestination", addr, "tcp", control.Addr())
}

// What the pin is for: there is no IPv6 socket, so an IPv6 client is refused
// at connect rather than accepted and then failed.
t.Run("an IPv6 client is refused", func(t *testing.T) {
port := strconv.Itoa(addr.Port)
conn, err := net.DialTimeout("tcp6", net.JoinHostPort("::1", port), 5*time.Second)
if err == nil {
conn.Close()
t.Errorf("net.Dial(%q, \"[::1]:%s\") succeeded, want refused; the egress listener "+
"is accepting IPv6 it cannot resolve an original destination for", "tcp6", port)
}
})
}

func TestEgressActivationFailsClosed(t *testing.T) {
egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil })
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/atunnel/original_dst_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import (
)

// TCPOriginalDestination reads the IPv4 destination preserved by a Linux
// REDIRECT rule. Actor networking is currently IPv4-only.
// TODO(liorlieberman) add the IPv6 IP6T_SO_ORIGINAL_DST variant
// when actor veth setup gains dual-stack support.
// REDIRECT rule. Actor networking is currently IPv4-only; see ListenEgressIPv4.
// TODO(#686): add the IPv6 IP6T_SO_ORIGINAL_DST variant when actor veth setup
// gains dual-stack support.
func TCPOriginalDestination(conn net.Conn) (string, error) {
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
Expand Down
Loading