From b4bf726395e07159ad76d99d31be72c620eb657e Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Fri, 10 Nov 2023 23:48:57 -0800 Subject: [PATCH] netstack: add iptables DNAT revision 0 support PiperOrigin-RevId: 581468023 --- pkg/sentry/socket/netfilter/BUILD | 1 + pkg/sentry/socket/netfilter/dnat.go | 118 ++++++++++++++++++++++ pkg/sentry/socket/netfilter/extensions.go | 2 +- pkg/sentry/socket/netfilter/targets.go | 7 ++ test/iptables/iptables_test.go | 4 + test/iptables/nat.go | 27 +++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 pkg/sentry/socket/netfilter/dnat.go diff --git a/pkg/sentry/socket/netfilter/BUILD b/pkg/sentry/socket/netfilter/BUILD index 7729bad51..717a2252e 100644 --- a/pkg/sentry/socket/netfilter/BUILD +++ b/pkg/sentry/socket/netfilter/BUILD @@ -8,6 +8,7 @@ package( go_library( name = "netfilter", srcs = [ + "dnat.go", "extensions.go", "ipv4.go", "ipv6.go", diff --git a/pkg/sentry/socket/netfilter/dnat.go b/pkg/sentry/socket/netfilter/dnat.go new file mode 100644 index 000000000..2ec38e2a2 --- /dev/null +++ b/pkg/sentry/socket/netfilter/dnat.go @@ -0,0 +1,118 @@ +// Copyright 2023 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 netfilter + +import ( + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/marshal" + "gvisor.dev/gvisor/pkg/syserr" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// DNATTargetName is used to mark targets as DNAT targets. DNAT targets should +// be reached for only NAT table. These targets will change the source port +// and/or IP for packets. +const DNATTargetName = "DNAT" + +type dnatTarget struct { + stack.DNATTarget +} + +func (st *dnatTarget) id() targetID { + return targetID{ + name: DNATTargetName, + networkProtocol: st.NetworkProtocol, + } +} + +type dnatTargetMakerV4 struct { + NetworkProtocol tcpip.NetworkProtocolNumber +} + +func (st *dnatTargetMakerV4) id() targetID { + return targetID{ + name: DNATTargetName, + networkProtocol: st.NetworkProtocol, + } +} + +func (*dnatTargetMakerV4) marshal(target target) []byte { + st := target.(*dnatTarget) + // This is a dnat target named dnat. + xt := linux.XTNATTargetV0{ + Target: linux.XTEntryTarget{ + TargetSize: linux.SizeOfXTNATTargetV0, + }, + } + copy(xt.Target.Name[:], DNATTargetName) + + xt.NfRange.RangeSize = 1 + xt.NfRange.RangeIPV4.Flags |= linux.NF_NAT_RANGE_MAP_IPS | linux.NF_NAT_RANGE_PROTO_SPECIFIED + xt.NfRange.RangeIPV4.MinPort = htons(st.Port) + xt.NfRange.RangeIPV4.MaxPort = xt.NfRange.RangeIPV4.MinPort + copy(xt.NfRange.RangeIPV4.MinIP[:], st.Addr.AsSlice()) + copy(xt.NfRange.RangeIPV4.MaxIP[:], st.Addr.AsSlice()) + return marshal.Marshal(&xt) +} + +func (*dnatTargetMakerV4) unmarshal(buf []byte, filter stack.IPHeaderFilter) (target, *syserr.Error) { + if len(buf) < linux.SizeOfXTNATTargetV0 { + nflog("dnatTargetMakerV4: buf has insufficient size for dnat target %d", len(buf)) + return nil, syserr.ErrInvalidArgument + } + + if p := filter.Protocol; p != header.TCPProtocolNumber && p != header.UDPProtocolNumber { + nflog("dnatTargetMakerV4: bad proto %d", p) + return nil, syserr.ErrInvalidArgument + } + + var st linux.XTNATTargetV0 + st.UnmarshalUnsafe(buf) + + // Copy linux.XTNATTargetV0 to stack.DNATTarget. + target := dnatTarget{DNATTarget: stack.DNATTarget{ + NetworkProtocol: filter.NetworkProtocol(), + }} + + // RangeSize should be 1. + nfRange := st.NfRange + if nfRange.RangeSize != 1 { + nflog("dnatTargetMakerV4: bad rangesize %d", nfRange.RangeSize) + return nil, syserr.ErrInvalidArgument + } + + // TODO(gvisor.dev/issue/5772): If the rule doesn't specify the source port, + // choose one automatically. + if nfRange.RangeIPV4.MinPort == 0 { + nflog("dnatTargetMakerV4: dnat target needs to specify a non-zero port") + return nil, syserr.ErrInvalidArgument + } + + if nfRange.RangeIPV4.MinPort != nfRange.RangeIPV4.MaxPort { + nflog("dnatTargetMakerV4: MinPort != MaxPort (%d, %d)", nfRange.RangeIPV4.MinPort, nfRange.RangeIPV4.MaxPort) + return nil, syserr.ErrInvalidArgument + } + if nfRange.RangeIPV4.MinIP != nfRange.RangeIPV4.MaxIP { + nflog("dnatTargetMakerV4: MinIP != MaxIP (%d, %d)", nfRange.RangeIPV4.MinPort, nfRange.RangeIPV4.MaxPort) + return nil, syserr.ErrInvalidArgument + } + + target.Addr = tcpip.AddrFrom4(nfRange.RangeIPV4.MinIP) + target.Port = ntohs(nfRange.RangeIPV4.MinPort) + + return &target, nil +} diff --git a/pkg/sentry/socket/netfilter/extensions.go b/pkg/sentry/socket/netfilter/extensions.go index a99ba3797..d4e632ac5 100644 --- a/pkg/sentry/socket/netfilter/extensions.go +++ b/pkg/sentry/socket/netfilter/extensions.go @@ -207,7 +207,7 @@ func unmarshalTarget(target linux.XTEntryTarget, filter stack.IPHeaderFilter, bu } targetMaker, ok := targetMakers[tid] if !ok { - nflog("unsupported target with name %q", target.Name.String()) + nflog("unsupported target with name %q, proto %d, and revision %d", target.Name.String(), tid.networkProtocol, tid.revision) return nil, syserr.ErrInvalidArgument } return targetMaker.unmarshal(buf, filter) diff --git a/pkg/sentry/socket/netfilter/targets.go b/pkg/sentry/socket/netfilter/targets.go index d6ac101a9..f7aec13bd 100644 --- a/pkg/sentry/socket/netfilter/targets.go +++ b/pkg/sentry/socket/netfilter/targets.go @@ -54,6 +54,7 @@ func init() { NetworkProtocol: header.IPv6ProtocolNumber, }) + // REDIRECT targets. registerTargetMaker(&redirectTargetMaker{ NetworkProtocol: header.IPv4ProtocolNumber, }) @@ -61,6 +62,7 @@ func init() { NetworkProtocol: header.IPv6ProtocolNumber, }) + // SNAT targets. registerTargetMaker(&snatTargetMakerV4{ NetworkProtocol: header.IPv4ProtocolNumber, }) @@ -76,6 +78,11 @@ func init() { registerTargetMaker(&snatTargetMakerR2{ NetworkProtocol: header.IPv6ProtocolNumber, }) + + // DNAT targets. + registerTargetMaker(&dnatTargetMakerV4{ + NetworkProtocol: header.IPv4ProtocolNumber, + }) } // The stack package provides some basic, useful targets for us. The following diff --git a/test/iptables/iptables_test.go b/test/iptables/iptables_test.go index cae26fe82..1accdaab7 100644 --- a/test/iptables/iptables_test.go +++ b/test/iptables/iptables_test.go @@ -364,6 +364,10 @@ func TestNATOutRedirectInvert(t *testing.T) { singleTest(t, &NATOutRedirectInvert{}) } +func TestNATOutDNAT(t *testing.T) { + singleTest(t, &NATOutDNAT{}) +} + func TestNATPreRedirectIP(t *testing.T) { singleTest(t, &NATPreRedirectIP{}) } diff --git a/test/iptables/nat.go b/test/iptables/nat.go index 9a50b8995..c6b126bda 100644 --- a/test/iptables/nat.go +++ b/test/iptables/nat.go @@ -51,6 +51,7 @@ func init() { RegisterTestCase(&NATOutRECVORIGDSTADDR{}) RegisterTestCase(&NATPostSNATUDP{}) RegisterTestCase(&NATPostSNATTCP{}) + RegisterTestCase(&NATOutDNAT{}) } // NATPreRedirectUDPPort tests that packets are redirected to different port. @@ -1035,3 +1036,29 @@ func (*NATPostSNATTCP) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) er } return nil } + +// NATOutDNAT tests that the source port/IP in the packets are modified as +// expected. +type NATOutDNAT struct{ containerCase } + +var _ TestCase = (*NATOutDNAT)(nil) + +// Name implements TestCase.Name. +func (*NATOutDNAT) Name() string { + return "NATOutDNAT" +} + +// ContainerAction implements TestCase.ContainerAction. +func (*NATOutDNAT) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + dst := nowhereIP(ipv6) + return loopbackTest(ctx, ipv6, net.ParseIP(dst), + "-A", "OUTPUT", + "-d", dst, + "-p", "udp", "-m", "udp", + "-j", "DNAT", "--to-destination", fmt.Sprintf("127.0.0.1:%d", acceptPort)) +} + +// LocalAction implements TestCase.LocalAction. +func (*NATOutDNAT) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return nil +}