Skip to content
Open
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
74 changes: 74 additions & 0 deletions internal/atunnel/original_dst_format_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build linux

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package atunnel

import (
"encoding/binary"
"net"
"testing"
)

// networkOrderPort produces the raw field value the kernel leaves in
// RawSockaddrInet4.Port and RawSockaddrInet6.Port: a uint16 whose in-memory
// bytes are the port in network order, which on a little-endian host is not
// the port's numeric value.
func networkOrderPort(port uint16) uint16 {
return binary.NativeEndian.Uint16(binary.BigEndian.AppendUint16(nil, port))
}

func TestFormatOriginalDestination(t *testing.T) {
tests := []struct {
name string
ip []byte
port uint16
want string
wantErr bool
}{
{name: "IPv4", ip: []byte{198, 18, 0, 1}, port: 443, want: "198.18.0.1:443"},
{
name: "IPv6 is bracketed",
ip: net.ParseIP("fd00:198:18::1").To16(),
port: 443,
want: "[fd00:198:18::1]:443",
},
{
name: "v4-mapped IPv6 renders as IPv4",
ip: net.ParseIP("::ffff:198.18.0.1").To16(),
port: 8080,
want: "198.18.0.1:8080",
},
{name: "high port is not sign-extended", ip: []byte{198, 18, 0, 1}, port: 65535, want: "198.18.0.1:65535"},
{name: "port zero is rejected", ip: []byte{198, 18, 0, 1}, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := formatOriginalDestination(test.ip, networkOrderPort(test.port))
if test.wantErr {
if err == nil {
t.Fatalf("formatOriginalDestination() = %q, want an error", got)
}
return
}
if err != nil {
t.Fatalf("formatOriginalDestination() error = %v", err)
}
if got != test.want {
t.Errorf("formatOriginalDestination() = %q, want %q", got, test.want)
}
})
}
}
82 changes: 62 additions & 20 deletions internal/atunnel/original_dst_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import (
"golang.org/x/sys/unix"
)

// 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.
// IP6T_SO_ORIGINAL_DST is not generated by golang.org/x/sys/unix. It is
// defined as 80 in linux/netfilter_ipv6/ip6_tables.h.
const ip6tSOOriginalDst = 80

// TCPOriginalDestination reads the IPv4 or IPv6 destination preserved by a
// Linux REDIRECT rule.
func TCPOriginalDestination(conn net.Conn) (string, error) {
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
Expand All @@ -39,34 +41,74 @@ func TCPOriginalDestination(conn net.Conn) (string, error) {
if err != nil {
return "", fmt.Errorf("atunnel: acquiring TCP syscall connection: %w", err)
}
// The IPv6 option is only meaningful on an AF_INET6 socket: on AF_INET the
// kernel returns EOPNOTSUPP, which would mask the real IPv4 error. A
// v4-mapped local address still means an IPv4 flow, so To4 is the test.
local, ok := tcpConn.LocalAddr().(*net.TCPAddr)
if !ok {
return "", fmt.Errorf("atunnel: original destination requires a TCP local address, got %T", tcpConn.LocalAddr())
}
isIPv6 := local.IP.To4() == nil

var addr unix.RawSockaddrInet4
var sockoptErr error
var destination string
if err := rawConn.Control(func(fd uintptr) {
size := uint32(unsafe.Sizeof(addr))
_, _, errno := unix.Syscall6(
unix.SYS_GETSOCKOPT,
fd,
unix.SOL_IP,
unix.SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)),
uintptr(unsafe.Pointer(&size)),
0,
)
if errno != 0 {
sockoptErr = errno
if isIPv6 {
destination, sockoptErr = originalIPv6Destination(fd)
return
}
destination, sockoptErr = originalIPv4Destination(fd)
}); err != nil {
return "", fmt.Errorf("atunnel: accessing TCP socket: %w", err)
}
if sockoptErr != nil {
return "", fmt.Errorf("atunnel: reading original TCP destination: %w", sockoptErr)
family := "IPv4"
if isIPv6 {
family = "IPv6"
}
return "", fmt.Errorf("atunnel: reading original %s TCP destination: %w", family, sockoptErr)
}
return destination, nil
}

func originalIPv4Destination(fd uintptr) (string, error) {
var addr unix.RawSockaddrInet4
if errno := getOriginalDestination(fd, unix.SOL_IP, unix.SO_ORIGINAL_DST, unsafe.Pointer(&addr), unsafe.Sizeof(addr)); errno != 0 {
return "", errno
}
return formatOriginalDestination(addr.Addr[:], addr.Port)
}

func originalIPv6Destination(fd uintptr) (string, error) {
var addr unix.RawSockaddrInet6
if errno := getOriginalDestination(fd, unix.SOL_IPV6, ip6tSOOriginalDst, unsafe.Pointer(&addr), unsafe.Sizeof(addr)); errno != 0 {
return "", errno
}
return formatOriginalDestination(addr.Addr[:], addr.Port)
}

func getOriginalDestination(fd uintptr, level, option int, addr unsafe.Pointer, addrSize uintptr) unix.Errno {
// getsockopt treats size as both input and output. Callers provide the exact
// size of RawSockaddrInet4 (16 bytes) or RawSockaddrInet6 (28 bytes), which
// the kernel validates before writing the original destination into addr.
size := uint32(addrSize)
_, _, errno := unix.Syscall6(
unix.SYS_GETSOCKOPT,
fd,
uintptr(level),
uintptr(option),
uintptr(addr),
uintptr(unsafe.Pointer(&size)),
0,
)
return errno
}

portBytes := (*[2]byte)(unsafe.Pointer(&addr.Port))
func formatOriginalDestination(ip []byte, rawPort uint16) (string, error) {
portBytes := (*[2]byte)(unsafe.Pointer(&rawPort))
port := binary.BigEndian.Uint16(portBytes[:])
if port == 0 {
return "", fmt.Errorf("atunnel: original TCP destination has port zero")
}
return net.JoinHostPort(net.IP(addr.Addr[:]).String(), strconv.Itoa(int(port))), nil
return net.JoinHostPort(net.IP(ip).String(), strconv.Itoa(int(port))), nil
}
Loading