ip6tables: redirect support

Adds support for the IPv6-compatible redirect target. Redirection is a limited
form of DNAT, where the destination is always the localhost.

Updates #3549.

PiperOrigin-RevId: 334698344
This commit is contained in:
Kevin Krakauer 2020-09-30 16:02:46 -07:00 committed by gVisor bot
parent 7f9e13053e
commit 6f8d64f422
11 changed files with 208 additions and 69 deletions

View File

@ -321,3 +321,16 @@ const (
// Enable all flags.
IP6T_INV_MASK = 0x7F
)
// NFNATRange corresponds to struct nf_nat_range in
// include/uapi/linux/netfilter/nf_nat.h.
type NFNATRange struct {
Flags uint32
MinAddr Inet6Addr
MaxAddr Inet6Addr
MinProto uint16 // Network byte order.
MaxProto uint16 // Network byte order.
}
// SizeOfNFNATRange is the size of NFNATRange.
const SizeOfNFNATRange = 40

View File

@ -147,10 +147,6 @@ func SetEntries(stk *stack.Stack, optVal []byte, ipv6 bool) *syserr.Error {
case stack.FilterTable:
table = stack.EmptyFilterTable()
case stack.NATTable:
if ipv6 {
nflog("IPv6 redirection not yet supported (gvisor.dev/issue/3549)")
return syserr.ErrInvalidArgument
}
table = stack.EmptyNATTable()
default:
nflog("we don't yet support writing to the %q table (gvisor.dev/issue/170)", replace.Name.String())

View File

@ -47,6 +47,9 @@ func init() {
registerTargetMaker(&redirectTargetMaker{
NetworkProtocol: header.IPv4ProtocolNumber,
})
registerTargetMaker(&nfNATTargetMaker{
NetworkProtocol: header.IPv6ProtocolNumber,
})
}
type standardTargetMaker struct {
@ -250,6 +253,86 @@ func (*redirectTargetMaker) unmarshal(buf []byte, filter stack.IPHeaderFilter) (
return &target, nil
}
type nfNATTarget struct {
Target linux.XTEntryTarget
Range linux.NFNATRange
}
const nfNATMarhsalledSize = linux.SizeOfXTEntryTarget + linux.SizeOfNFNATRange
type nfNATTargetMaker struct {
NetworkProtocol tcpip.NetworkProtocolNumber
}
func (rm *nfNATTargetMaker) id() stack.TargetID {
return stack.TargetID{
Name: stack.RedirectTargetName,
NetworkProtocol: rm.NetworkProtocol,
}
}
func (*nfNATTargetMaker) marshal(target stack.Target) []byte {
rt := target.(*stack.RedirectTarget)
nt := nfNATTarget{
Target: linux.XTEntryTarget{
TargetSize: nfNATMarhsalledSize,
},
Range: linux.NFNATRange{
Flags: linux.NF_NAT_RANGE_PROTO_SPECIFIED,
},
}
copy(nt.Target.Name[:], stack.RedirectTargetName)
copy(nt.Range.MinAddr[:], rt.Addr)
copy(nt.Range.MaxAddr[:], rt.Addr)
nt.Range.MinProto = htons(rt.Port)
nt.Range.MaxProto = nt.Range.MinProto
ret := make([]byte, 0, nfNATMarhsalledSize)
return binary.Marshal(ret, usermem.ByteOrder, nt)
}
func (*nfNATTargetMaker) unmarshal(buf []byte, filter stack.IPHeaderFilter) (stack.Target, *syserr.Error) {
if size := nfNATMarhsalledSize; len(buf) < size {
nflog("nfNATTargetMaker: buf has insufficient size (%d) for nfNAT target (%d)", len(buf), size)
return nil, syserr.ErrInvalidArgument
}
if p := filter.Protocol; p != header.TCPProtocolNumber && p != header.UDPProtocolNumber {
nflog("nfNATTargetMaker: bad proto %d", p)
return nil, syserr.ErrInvalidArgument
}
var natRange linux.NFNATRange
buf = buf[linux.SizeOfXTEntryTarget:nfNATMarhsalledSize]
binary.Unmarshal(buf, usermem.ByteOrder, &natRange)
// We don't support port or address ranges.
if natRange.MinAddr != natRange.MaxAddr {
nflog("nfNATTargetMaker: MinAddr and MaxAddr are different")
return nil, syserr.ErrInvalidArgument
}
if natRange.MinProto != natRange.MaxProto {
nflog("nfNATTargetMaker: MinProto and MaxProto are different")
return nil, syserr.ErrInvalidArgument
}
// TODO(gvisor.dev/issue/3549): Check for other flags.
// For now, redirect target only supports destination change.
if natRange.Flags != linux.NF_NAT_RANGE_PROTO_SPECIFIED {
nflog("nfNATTargetMaker: invalid range flags %d", natRange.Flags)
return nil, syserr.ErrInvalidArgument
}
target := stack.RedirectTarget{
NetworkProtocol: filter.NetworkProtocol(),
Addr: tcpip.Address(natRange.MinAddr[:]),
Port: ntohs(natRange.MinProto),
}
return &target, nil
}
// translateToStandardTarget translates from the value in a
// linux.XTStandardTarget to an stack.Verdict.
func translateToStandardTarget(val int32, netProto tcpip.NetworkProtocolNumber) (stack.Target, *syserr.Error) {

View File

@ -1512,8 +1512,17 @@ func getSockOptIPv6(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name
return &vP, nil
case linux.IP6T_ORIGINAL_DST:
// TODO(gvisor.dev/issue/170): ip6tables.
return nil, syserr.ErrInvalidArgument
if outLen < int(binary.Size(linux.SockAddrInet6{})) {
return nil, syserr.ErrInvalidArgument
}
var v tcpip.OriginalDestinationOption
if err := ep.GetSockOpt(&v); err != nil {
return nil, syserr.TranslateNetstackError(err)
}
a, _ := ConvertAddress(linux.AF_INET6, tcpip.FullAddress(v))
return a.(*linux.SockAddrInet6), nil
case linux.IP6T_SO_GET_INFO:
if outLen < linux.SizeOfIPTGetinfo {
@ -1555,6 +1564,26 @@ func getSockOptIPv6(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name
}
return &entries, nil
case linux.IP6T_SO_GET_REVISION_TARGET:
if outLen < linux.SizeOfXTGetRevision {
return nil, syserr.ErrInvalidArgument
}
// Only valid for raw IPv6 sockets.
if family, skType, _ := s.Type(); family != linux.AF_INET6 || skType != linux.SOCK_RAW {
return nil, syserr.ErrProtocolNotAvailable
}
stack := inet.StackFromContext(t)
if stack == nil {
return nil, syserr.ErrNoDevice
}
ret, err := netfilter.TargetRevision(t, outPtr, header.IPv6ProtocolNumber)
if err != nil {
return nil, err
}
return &ret, nil
default:
emitUnimplementedEventIPv6(t, name)
}

View File

@ -196,13 +196,14 @@ type bucket struct {
// packetToTupleID converts packet to a tuple ID. It fails when pkt lacks a valid
// TCP header.
//
// Preconditions: pkt.NetworkHeader() is valid.
func packetToTupleID(pkt *PacketBuffer) (tupleID, *tcpip.Error) {
// TODO(gvisor.dev/issue/170): Need to support for other
// protocols as well.
netHeader := header.IPv4(pkt.NetworkHeader().View())
if len(netHeader) < header.IPv4MinimumSize || netHeader.TransportProtocol() != header.TCPProtocolNumber {
netHeader := pkt.Network()
if netHeader.TransportProtocol() != header.TCPProtocolNumber {
return tupleID{}, tcpip.ErrUnknownProtocol
}
tcpHeader := header.TCP(pkt.TransportHeader().View())
if len(tcpHeader) < header.TCPMinimumSize {
return tupleID{}, tcpip.ErrUnknownProtocol
@ -214,7 +215,7 @@ func packetToTupleID(pkt *PacketBuffer) (tupleID, *tcpip.Error) {
dstAddr: netHeader.DestinationAddress(),
dstPort: tcpHeader.DestinationPort(),
transProto: netHeader.TransportProtocol(),
netProto: header.IPv4ProtocolNumber,
netProto: pkt.NetworkProtocolNumber,
}, nil
}
@ -344,7 +345,7 @@ func handlePacketPrerouting(pkt *PacketBuffer, conn *conn, dir direction) {
return
}
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader := pkt.Network()
tcpHeader := header.TCP(pkt.TransportHeader().View())
// For prerouting redirection, packets going in the original direction
@ -366,8 +367,12 @@ func handlePacketPrerouting(pkt *PacketBuffer, conn *conn, dir direction) {
// support cases when they are validated, e.g. when we can't offload
// receive checksumming.
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
// After modification, IPv4 packets need a valid checksum.
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
}
// handlePacketOutput manipulates ports for packets in Output hook.
@ -377,7 +382,7 @@ func handlePacketOutput(pkt *PacketBuffer, conn *conn, gso *GSO, r *Route, dir d
return
}
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader := pkt.Network()
tcpHeader := header.TCP(pkt.TransportHeader().View())
// For output redirection, packets going in the original direction
@ -396,7 +401,7 @@ func handlePacketOutput(pkt *PacketBuffer, conn *conn, gso *GSO, r *Route, dir d
// Calculate the TCP checksum and set it.
tcpHeader.SetChecksum(0)
length := uint16(pkt.Size()) - uint16(netHeader.HeaderLength())
length := uint16(pkt.Size()) - uint16(len(pkt.NetworkHeader().View()))
xsum := r.PseudoHeaderChecksum(header.TCPProtocolNumber, length)
if gso != nil && gso.NeedsCsum {
tcpHeader.SetChecksum(xsum)
@ -405,8 +410,11 @@ func handlePacketOutput(pkt *PacketBuffer, conn *conn, gso *GSO, r *Route, dir d
tcpHeader.SetChecksum(^tcpHeader.CalculateChecksum(xsum))
}
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
}
// handlePacket will manipulate the port and address of the packet if the
@ -422,7 +430,7 @@ func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, gso *GSO, r *Rou
}
// TODO(gvisor.dev/issue/170): Support other transport protocols.
if nh := pkt.NetworkHeader().View(); nh.IsEmpty() || header.IPv4(nh).TransportProtocol() != header.TCPProtocolNumber {
if pkt.Network().TransportProtocol() != header.TCPProtocolNumber {
return false
}
@ -473,7 +481,7 @@ func (ct *ConnTrack) maybeInsertNoop(pkt *PacketBuffer, hook Hook) {
}
// We only track TCP connections.
if nh := pkt.NetworkHeader().View(); nh.IsEmpty() || header.IPv4(nh).TransportProtocol() != header.TCPProtocolNumber {
if pkt.Network().TransportProtocol() != header.TCPProtocolNumber {
return
}
@ -609,7 +617,7 @@ 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) {
func (ct *ConnTrack) originalDst(epID TransportEndpointID, netProto tcpip.NetworkProtocolNumber) (tcpip.Address, uint16, *tcpip.Error) {
// Lookup the connection. The reply's original destination
// describes the original address.
tid := tupleID{
@ -618,7 +626,7 @@ func (ct *ConnTrack) originalDst(epID TransportEndpointID) (tcpip.Address, uint1
dstAddr: epID.RemoteAddress,
dstPort: epID.RemotePort,
transProto: header.TCPProtocolNumber,
netProto: header.IPv4ProtocolNumber,
netProto: netProto,
}
conn, _ := ct.connForTID(tid)
if conn == nil {

View File

@ -502,11 +502,11 @@ func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx
// 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) {
func (it *IPTables) OriginalDst(epID TransportEndpointID, netProto tcpip.NetworkProtocolNumber) (tcpip.Address, uint16, *tcpip.Error) {
it.mu.RLock()
defer it.mu.RUnlock()
if !it.modified {
return "", 0, tcpip.ErrNotConnected
}
return it.connections.originalDst(epID)
return it.connections.originalDst(epID, netProto)
}

View File

@ -164,11 +164,15 @@ func (rt *RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, gs
return RuleDrop, 0
}
// Change the address to localhost (127.0.0.1) in Output and
// to primary address of the incoming interface in Prerouting.
// Change the address to localhost (127.0.0.1 or ::1) in Output and to
// the primary address of the incoming interface in Prerouting.
switch hook {
case Output:
rt.Addr = tcpip.Address([]byte{127, 0, 0, 1})
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
rt.Addr = tcpip.Address([]byte{127, 0, 0, 1})
} else {
rt.Addr = header.IPv6Loopback
}
case Prerouting:
rt.Addr = address
default:
@ -177,8 +181,7 @@ func (rt *RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, gs
// TODO(gvisor.dev/issue/170): Check Flags in RedirectTarget if
// we need to change dest address (for OUTPUT chain) or ports.
netHeader := header.IPv4(pkt.NetworkHeader().View())
switch protocol := netHeader.TransportProtocol(); protocol {
switch protocol := pkt.TransportProtocolNumber; protocol {
case header.UDPProtocolNumber:
udpHeader := header.UDP(pkt.TransportHeader().View())
udpHeader.SetDestinationPort(rt.Port)
@ -186,10 +189,10 @@ func (rt *RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, gs
// Calculate UDP checksum and set it.
if hook == Output {
udpHeader.SetChecksum(0)
length := uint16(pkt.Size()) - uint16(netHeader.HeaderLength())
// Only calculate the checksum if offloading isn't supported.
if r.Capabilities()&CapabilityTXChecksumOffload == 0 {
length := uint16(pkt.Size()) - uint16(len(pkt.NetworkHeader().View()))
xsum := r.PseudoHeaderChecksum(protocol, length)
for _, v := range pkt.Data.Views() {
xsum = header.Checksum(v, xsum)
@ -198,10 +201,15 @@ func (rt *RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, gs
udpHeader.SetChecksum(^udpHeader.CalculateChecksum(xsum))
}
}
// Change destination address.
netHeader.SetDestinationAddress(rt.Addr)
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
pkt.Network().SetDestinationAddress(rt.Addr)
// After modification, IPv4 packets need a valid checksum.
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
pkt.NatDone = true
case header.TCPProtocolNumber:
if ct == nil {

View File

@ -19,6 +19,7 @@ import (
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
type headerType int
@ -255,6 +256,20 @@ func (pk *PacketBuffer) Clone() *PacketBuffer {
return newPk
}
// Network returns the network header as a header.Network.
//
// Network should only be called when NetworkHeader has been set.
func (pk *PacketBuffer) Network() header.Network {
switch netProto := pk.NetworkProtocolNumber; netProto {
case header.IPv4ProtocolNumber:
return header.IPv4(pk.NetworkHeader().View())
case header.IPv6ProtocolNumber:
return header.IPv6(pk.NetworkHeader().View())
default:
panic(fmt.Sprintf("unknown network protocol number %d", netProto))
}
}
// headerInfo stores metadata about a header in a packet.
type headerInfo struct {
// buf is the memorized slice for both prepended and consumed header.

View File

@ -2099,7 +2099,7 @@ func (e *endpoint) GetSockOpt(opt tcpip.GettableSocketOption) *tcpip.Error {
case *tcpip.OriginalDestinationOption:
e.LockUser()
ipt := e.stack.IPTables()
addr, port, err := ipt.OriginalDst(e.ID)
addr, port, err := ipt.OriginalDst(e.ID, e.NetProto)
e.UnlockUser()
if err != nil {
return err

View File

@ -48,13 +48,6 @@ func singleTest(t *testing.T, test TestCase) {
}
}
// TODO(gvisor.dev/issue/3549): IPv6 NAT support.
func ipv4Test(t *testing.T, test TestCase) {
t.Run("IPv4", func(t *testing.T) {
iptablesTest(t, test, false)
})
}
func iptablesTest(t *testing.T, test TestCase, ipv6 bool) {
if _, ok := Tests[test.Name()]; !ok {
t.Fatalf("no test found with name %q. Has it been registered?", test.Name())
@ -325,66 +318,66 @@ func TestFilterOutputInvertDestination(t *testing.T) {
}
func TestNATPreRedirectUDPPort(t *testing.T) {
ipv4Test(t, NATPreRedirectUDPPort{})
singleTest(t, NATPreRedirectUDPPort{})
}
func TestNATPreRedirectTCPPort(t *testing.T) {
ipv4Test(t, NATPreRedirectTCPPort{})
singleTest(t, NATPreRedirectTCPPort{})
}
func TestNATPreRedirectTCPOutgoing(t *testing.T) {
ipv4Test(t, NATPreRedirectTCPOutgoing{})
singleTest(t, NATPreRedirectTCPOutgoing{})
}
func TestNATOutRedirectTCPIncoming(t *testing.T) {
ipv4Test(t, NATOutRedirectTCPIncoming{})
singleTest(t, NATOutRedirectTCPIncoming{})
}
func TestNATOutRedirectUDPPort(t *testing.T) {
ipv4Test(t, NATOutRedirectUDPPort{})
singleTest(t, NATOutRedirectUDPPort{})
}
func TestNATOutRedirectTCPPort(t *testing.T) {
ipv4Test(t, NATOutRedirectTCPPort{})
singleTest(t, NATOutRedirectTCPPort{})
}
func TestNATDropUDP(t *testing.T) {
ipv4Test(t, NATDropUDP{})
singleTest(t, NATDropUDP{})
}
func TestNATAcceptAll(t *testing.T) {
ipv4Test(t, NATAcceptAll{})
singleTest(t, NATAcceptAll{})
}
func TestNATOutRedirectIP(t *testing.T) {
ipv4Test(t, NATOutRedirectIP{})
singleTest(t, NATOutRedirectIP{})
}
func TestNATOutDontRedirectIP(t *testing.T) {
ipv4Test(t, NATOutDontRedirectIP{})
singleTest(t, NATOutDontRedirectIP{})
}
func TestNATOutRedirectInvert(t *testing.T) {
ipv4Test(t, NATOutRedirectInvert{})
singleTest(t, NATOutRedirectInvert{})
}
func TestNATPreRedirectIP(t *testing.T) {
ipv4Test(t, NATPreRedirectIP{})
singleTest(t, NATPreRedirectIP{})
}
func TestNATPreDontRedirectIP(t *testing.T) {
ipv4Test(t, NATPreDontRedirectIP{})
singleTest(t, NATPreDontRedirectIP{})
}
func TestNATPreRedirectInvert(t *testing.T) {
ipv4Test(t, NATPreRedirectInvert{})
singleTest(t, NATPreRedirectInvert{})
}
func TestNATRedirectRequiresProtocol(t *testing.T) {
ipv4Test(t, NATRedirectRequiresProtocol{})
singleTest(t, NATRedirectRequiresProtocol{})
}
func TestNATLoopbackSkipsPrerouting(t *testing.T) {
ipv4Test(t, NATLoopbackSkipsPrerouting{})
singleTest(t, NATLoopbackSkipsPrerouting{})
}
func TestInputSource(t *testing.T) {
@ -421,9 +414,9 @@ func TestFilterAddrs(t *testing.T) {
}
func TestNATPreOriginalDst(t *testing.T) {
ipv4Test(t, NATPreOriginalDst{})
singleTest(t, NATPreOriginalDst{})
}
func TestNATOutOriginalDst(t *testing.T) {
ipv4Test(t, NATOutOriginalDst{})
singleTest(t, NATOutOriginalDst{})
}

View File

@ -95,16 +95,10 @@ TEST(IP6TablesBasic, GetRevision) {
};
socklen_t rev_len = sizeof(rev);
// TODO(gvisor.dev/issue/3549): IPv6 redirect support.
const int retval =
getsockopt(sock, SOL_IPV6, IP6T_SO_GET_REVISION_TARGET, &rev, &rev_len);
if (IsRunningOnGvisor()) {
EXPECT_THAT(retval, SyscallFailsWithErrno(ENOPROTOOPT));
return;
}
// Revision 0 exists.
EXPECT_THAT(retval, SyscallSucceeds());
EXPECT_THAT(
getsockopt(sock, SOL_IPV6, IP6T_SO_GET_REVISION_TARGET, &rev, &rev_len),
SyscallSucceeds());
EXPECT_EQ(rev.revision, 0);
// Revisions > 0 don't exist.