iptables: support SO_ORIGINAL_DST

Envoy (#170) uses this to get the original destination of redirected
packets.
This commit is contained in:
Kevin Krakauer
2020-07-31 10:47:26 -07:00
parent 68a7da9549
commit 2a7b2a61e3
13 changed files with 344 additions and 13 deletions
+7 -1
View File
@@ -59,7 +59,7 @@ var VerdictStrings = map[int32]string{
NF_RETURN: "RETURN",
}
// Socket options. These correspond to values in
// Socket options for SOL_SOCKET. These correspond to values in
// include/uapi/linux/netfilter_ipv4/ip_tables.h.
const (
IPT_BASE_CTL = 64
@@ -74,6 +74,12 @@ const (
IPT_SO_GET_MAX = IPT_SO_GET_REVISION_TARGET
)
// Socket option for SOL_IP. This corresponds to the value in
// include/uapi/linux/netfilter_ipv4.h.
const (
SO_ORIGINAL_DST = 80
)
// Name lengths. These correspond to values in
// include/uapi/linux/netfilter/x_tables.h.
const (
+3 -1
View File
@@ -239,11 +239,13 @@ const SockAddrMax = 128
type InetAddr [4]byte
// SockAddrInet is struct sockaddr_in, from uapi/linux/in.h.
//
// +marshal
type SockAddrInet struct {
Family uint16
Port uint16
Addr InetAddr
Zero [8]uint8 // pad to sizeof(struct sockaddr).
_ [8]uint8 // pad to sizeof(struct sockaddr).
}
// InetMulticastRequest is struct ip_mreq, from uapi/linux/in.h.
+17
View File
@@ -1490,6 +1490,10 @@ func getSockOptIPv6(t *kernel.Task, ep commonEndpoint, name, outLen int) (marsha
vP := primitive.Int32(boolToInt32(v))
return &vP, nil
case linux.SO_ORIGINAL_DST:
// TODO(gvisor.dev/issue/170): ip6tables.
return nil, syserr.ErrInvalidArgument
default:
emitUnimplementedEventIPv6(t, name)
}
@@ -1600,6 +1604,19 @@ func getSockOptIP(t *kernel.Task, ep commonEndpoint, name, outLen int, family in
vP := primitive.Int32(boolToInt32(v))
return &vP, nil
case linux.SO_ORIGINAL_DST:
if outLen < int(binary.Size(linux.SockAddrInet{})) {
return nil, syserr.ErrInvalidArgument
}
var v tcpip.OriginalDestinationOption
if err := ep.GetSockOpt(&v); err != nil {
return nil, syserr.TranslateNetstackError(err)
}
a, _ := ConvertAddress(linux.AF_INET, tcpip.FullAddress(v))
return a.(*linux.SockAddrInet), nil
default:
emitUnimplementedEventIP(t, name)
}
+1
View File
@@ -521,6 +521,7 @@ var sockOptNames = map[uint64]abi.ValueSet{
linux.IP_ROUTER_ALERT: "IP_ROUTER_ALERT",
linux.IP_PKTOPTIONS: "IP_PKTOPTIONS",
linux.IP_MTU: "IP_MTU",
linux.SO_ORIGINAL_DST: "SO_ORIGINAL_DST",
},
linux.SOL_SOCKET: {
linux.SO_ERROR: "SO_ERROR",
+26
View File
@@ -240,7 +240,10 @@ func (ct *ConnTrack) connFor(pkt *PacketBuffer) (*conn, direction) {
if err != nil {
return nil, dirOriginal
}
return ct.connForTID(tid)
}
func (ct *ConnTrack) connForTID(tid tupleID) (*conn, direction) {
bucket := ct.bucket(tid)
now := time.Now()
@@ -604,3 +607,26 @@ func (ct *ConnTrack) reapTupleLocked(tuple *tuple, bucket int, now time.Time) bo
return true
}
func (ct *ConnTrack) originalDst(epID TransportEndpointID) (tcpip.Address, uint16, *tcpip.Error) {
// Lookup the connection. The reply's original destination
// describes the original address.
tid := tupleID{
srcAddr: epID.LocalAddress,
srcPort: epID.LocalPort,
dstAddr: epID.RemoteAddress,
dstPort: epID.RemotePort,
transProto: header.TCPProtocolNumber,
netProto: header.IPv4ProtocolNumber,
}
conn, _ := ct.connForTID(tid)
if conn == nil {
// Not a tracked connection.
return "", 0, tcpip.ErrNotConnected
} else if conn.manip == manipNone {
// Unmanipulated connection.
return "", 0, tcpip.ErrInvalidOptionValue
}
return conn.original.dstAddr, conn.original.dstPort, nil
}
+7 -4
View File
@@ -218,19 +218,16 @@ func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, addr
// Many users never configure iptables. Spare them the cost of rule
// traversal if rules have never been set.
it.mu.RLock()
defer it.mu.RUnlock()
if !it.modified {
it.mu.RUnlock()
return true
}
it.mu.RUnlock()
// Packets are manipulated only if connection and matching
// NAT rule exists.
shouldTrack := it.connections.handlePacket(pkt, hook, gso, r)
// Go through each table containing the hook.
it.mu.RLock()
defer it.mu.RUnlock()
priorities := it.priorities[hook]
for _, tableID := range priorities {
// If handlePacket already NATed the packet, we don't need to
@@ -418,3 +415,9 @@ func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx
// All the matchers matched, so run the target.
return rule.Target.Action(pkt, &it.connections, hook, gso, r, address)
}
// OriginalDst returns the original destination of redirected connections. It
// returns an error if the connection doesn't exist or isn't redirected.
func (it *IPTables) OriginalDst(epID TransportEndpointID) (tcpip.Address, uint16, *tcpip.Error) {
return it.connections.originalDst(epID)
}
+4
View File
@@ -954,6 +954,10 @@ type DefaultTTLOption uint8
// classic BPF filter on a given endpoint.
type SocketDetachFilterOption int
// OriginalDestinationOption is used to get the original destination address
// and port of a redirected packet.
type OriginalDestinationOption FullAddress
// IPPacketInfo is the message structure for IP_PKTINFO.
//
// +stateify savable
+11
View File
@@ -2017,6 +2017,17 @@ func (e *endpoint) GetSockOpt(opt interface{}) *tcpip.Error {
*o = tcpip.TCPDeferAcceptOption(e.deferAccept)
e.UnlockUser()
case *tcpip.OriginalDestinationOption:
ipt := e.stack.IPTables()
addr, port, err := ipt.OriginalDst(e.ID)
if err != nil {
return err
}
*o = tcpip.OriginalDestinationOption{
Addr: addr,
Port: port,
}
default:
return tcpip.ErrUnknownProtocolOption
}
+1
View File
@@ -9,6 +9,7 @@ go_library(
"filter_input.go",
"filter_output.go",
"iptables.go",
"iptables_unsafe.go",
"iptables_util.go",
"nat.go",
],
+8
View File
@@ -371,3 +371,11 @@ func TestFilterAddrs(t *testing.T) {
}
}
}
func TestNATPreOriginalDst(t *testing.T) {
singleTest(t, NATPreOriginalDst{})
}
func TestNATOutOriginalDst(t *testing.T) {
singleTest(t, NATOutOriginalDst{})
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2020 The gVisor Authors.
//
// 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 iptables
import (
"fmt"
"syscall"
"unsafe"
)
type originalDstError struct {
errno syscall.Errno
}
func (e originalDstError) Error() string {
return fmt.Sprintf("errno (%d) when calling getsockopt(SO_ORIGINAL_DST): %v", int(e.errno), e.errno.Error())
}
// SO_ORIGINAL_DST gets the original destination of a redirected packet via
// getsockopt.
const SO_ORIGINAL_DST = 80
func originalDestination4(connfd int) (syscall.RawSockaddrInet4, error) {
var addr syscall.RawSockaddrInet4
var addrLen uint32 = syscall.SizeofSockaddrInet4
if errno := originalDestination(connfd, syscall.SOL_IP, unsafe.Pointer(&addr), &addrLen); errno != 0 {
return syscall.RawSockaddrInet4{}, originalDstError{errno}
}
return addr, nil
}
func originalDestination6(connfd int) (syscall.RawSockaddrInet6, error) {
var addr syscall.RawSockaddrInet6
var addrLen uint32 = syscall.SizeofSockaddrInet6
if errno := originalDestination(connfd, syscall.SOL_IPV6, unsafe.Pointer(&addr), &addrLen); errno != 0 {
return syscall.RawSockaddrInet6{}, originalDstError{errno}
}
return addr, nil
}
func originalDestination(connfd int, level uintptr, optval unsafe.Pointer, optlen *uint32) syscall.Errno {
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
uintptr(connfd),
level,
SO_ORIGINAL_DST,
uintptr(optval),
uintptr(unsafe.Pointer(optlen)),
0)
return errno
}
+47 -4
View File
@@ -15,6 +15,8 @@
package iptables
import (
"encoding/binary"
"errors"
"fmt"
"net"
"os/exec"
@@ -218,17 +220,58 @@ func filterAddrs(addrs []string, ipv6 bool) []string {
// getInterfaceName returns the name of the interface other than loopback.
func getInterfaceName() (string, bool) {
var ifname string
iface, ok := getNonLoopbackInterface()
if !ok {
return "", false
}
return iface.Name, true
}
func getInterfaceAddrs(ipv6 bool) ([]net.IP, error) {
iface, ok := getNonLoopbackInterface()
if !ok {
return nil, errors.New("no non-loopback interface found")
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
// Get only IPv4 or IPv6 addresses.
ips := make([]net.IP, 0, len(addrs))
for _, addr := range addrs {
parts := strings.Split(addr.String(), "/")
var ip net.IP
// To16() returns IPv4 addresses as IPv4-mapped IPv6 addresses.
// So we check whether To4() returns nil to test whether the
// address is v4 or v6.
if v4 := net.ParseIP(parts[0]).To4(); ipv6 && v4 == nil {
ip = net.ParseIP(parts[0]).To16()
} else {
ip = v4
}
if ip != nil {
ips = append(ips, ip)
}
}
return ips, nil
}
func getNonLoopbackInterface() (net.Interface, bool) {
if interfaces, err := net.Interfaces(); err == nil {
for _, intf := range interfaces {
if intf.Name != "lo" {
ifname = intf.Name
break
return intf, true
}
}
}
return net.Interface{}, false
}
return ifname, ifname != ""
func htons(x uint16) uint16 {
buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, x)
return binary.LittleEndian.Uint16(buf)
}
func localIP(ipv6 bool) string {
+149 -3
View File
@@ -18,12 +18,11 @@ import (
"errors"
"fmt"
"net"
"syscall"
"time"
)
const (
redirectPort = 42
)
const redirectPort = 42
func init() {
RegisterTestCase(NATPreRedirectUDPPort{})
@@ -42,6 +41,8 @@ func init() {
RegisterTestCase(NATOutRedirectInvert{})
RegisterTestCase(NATRedirectRequiresProtocol{})
RegisterTestCase(NATLoopbackSkipsPrerouting{})
RegisterTestCase(NATPreOriginalDst{})
RegisterTestCase(NATOutOriginalDst{})
}
// NATPreRedirectUDPPort tests that packets are redirected to different port.
@@ -471,6 +472,151 @@ func (NATLoopbackSkipsPrerouting) LocalAction(ip net.IP, ipv6 bool) error {
return nil
}
// NATPreOriginalDst tests that SO_ORIGINAL_DST returns the pre-NAT destination
// of PREROUTING NATted packets.
type NATPreOriginalDst struct{}
// Name implements TestCase.Name.
func (NATPreOriginalDst) Name() string {
return "NATPreOriginalDst"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATPreOriginalDst) ContainerAction(ip net.IP, ipv6 bool) error {
// Redirect incoming TCP connections to acceptPort.
if err := natTable(ipv6, "-A", "PREROUTING",
"-p", "tcp",
"--destination-port", fmt.Sprintf("%d", dropPort),
"-j", "REDIRECT", "--to-port", fmt.Sprintf("%d", acceptPort)); err != nil {
return err
}
addrs, err := getInterfaceAddrs(ipv6)
if err != nil {
return err
}
return listenForRedirectedConn(ipv6, addrs)
}
// LocalAction implements TestCase.LocalAction.
func (NATPreOriginalDst) LocalAction(ip net.IP, ipv6 bool) error {
return connectTCP(ip, dropPort, sendloopDuration)
}
// NATOutOriginalDst tests that SO_ORIGINAL_DST returns the pre-NAT destination
// of OUTBOUND NATted packets.
type NATOutOriginalDst struct{}
// Name implements TestCase.Name.
func (NATOutOriginalDst) Name() string {
return "NATOutOriginalDst"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATOutOriginalDst) ContainerAction(ip net.IP, ipv6 bool) error {
// Redirect incoming TCP connections to acceptPort.
if err := natTable(ipv6, "-A", "OUTPUT", "-p", "tcp", "-j", "REDIRECT", "--to-port", fmt.Sprintf("%d", acceptPort)); err != nil {
return err
}
connCh := make(chan error)
go func() {
connCh <- connectTCP(ip, dropPort, sendloopDuration)
}()
if err := listenForRedirectedConn(ipv6, []net.IP{ip}); err != nil {
return err
}
return <-connCh
}
// LocalAction implements TestCase.LocalAction.
func (NATOutOriginalDst) LocalAction(ip net.IP, ipv6 bool) error {
// No-op.
return nil
}
func listenForRedirectedConn(ipv6 bool, originalDsts []net.IP) error {
// The net package doesn't give guarantee access to the connection's
// underlying FD, and thus we cannot call getsockopt. We have to use
// traditional syscalls for SO_ORIGINAL_DST.
// Create the listening socket, bind, listen, and accept.
family := syscall.AF_INET
if ipv6 {
family = syscall.AF_INET6
}
sockfd, err := syscall.Socket(family, syscall.SOCK_STREAM, 0)
if err != nil {
return err
}
defer syscall.Close(sockfd)
var bindAddr syscall.Sockaddr
if ipv6 {
bindAddr = &syscall.SockaddrInet6{
Port: acceptPort,
Addr: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // in6addr_any
}
} else {
bindAddr = &syscall.SockaddrInet4{
Port: acceptPort,
Addr: [4]byte{0, 0, 0, 0}, // INADDR_ANY
}
}
if err := syscall.Bind(sockfd, bindAddr); err != nil {
return err
}
if err := syscall.Listen(sockfd, 1); err != nil {
return err
}
connfd, _, err := syscall.Accept(sockfd)
if err != nil {
return err
}
defer syscall.Close(connfd)
// Verify that, despite listening on acceptPort, SO_ORIGINAL_DST
// indicates the packet was sent to originalDst:dropPort.
if ipv6 {
got, err := originalDestination6(connfd)
if err != nil {
return err
}
// The original destination could be any of our IPs.
for _, dst := range originalDsts {
want := syscall.RawSockaddrInet6{
Family: syscall.AF_INET6,
Port: htons(dropPort),
}
copy(want.Addr[:], dst.To16())
if got == want {
return nil
}
}
return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts)
} else {
got, err := originalDestination4(connfd)
if err != nil {
return err
}
// The original destination could be any of our IPs.
for _, dst := range originalDsts {
want := syscall.RawSockaddrInet4{
Family: syscall.AF_INET,
Port: htons(dropPort),
}
copy(want.Addr[:], dst.To4())
if got == want {
return nil
}
}
return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts)
}
}
// loopbackTests runs an iptables rule and ensures that packets sent to
// dest:dropPort are received by localhost:acceptPort.
func loopbackTest(ipv6 bool, dest net.IP, args ...string) error {