Add nat table support for iptables.

Add nat table support for Prerouting hook with Redirect option.
Add tests to check redirect of ports.
This commit is contained in:
Nayana Bidari
2020-02-18 11:30:42 -08:00
parent fae3de21af
commit b30b7f3422
8 changed files with 234 additions and 5 deletions
+27
View File
@@ -250,6 +250,33 @@ type XTErrorTarget struct {
// SizeOfXTErrorTarget is the size of an XTErrorTarget.
const SizeOfXTErrorTarget = 64
// NfNATIPV4Range. It corresponds to struct nf_nat_ipv4_range
// in include/uapi/linux/netfilter/nf_nat.h.
type NfNATIPV4Range struct {
Flags uint32
MinIP [4]byte
MaxIP [4]byte
MinPort uint16
MaxPort uint16
}
// NfNATIPV4MultiRangeCompat. It corresponds to struct
// nf_nat_ipv4_multi_range_compat in include/uapi/linux/netfilter/nf_nat.h.
type NfNATIPV4MultiRangeCompat struct {
Rangesize uint32
RangeIPV4 [1]NfNATIPV4Range
}
// XTRedirectTarget triggers a redirect when reached.
type XTRedirectTarget struct {
Target XTEntryTarget
NfRange NfNATIPV4MultiRangeCompat
_ [4]byte
}
// SizeOfXTRedirectTarget is the size of an XTRedirectTarget.
const SizeOfXTRedirectTarget = 56
// IPTGetinfo is the argument for the IPT_SO_GET_INFO sockopt. It corresponds
// to struct ipt_getinfo in include/uapi/linux/netfilter_ipv4/ip_tables.h.
type IPTGetinfo struct {
+55 -3
View File
@@ -35,6 +35,11 @@ import (
// shouldn't be reached - an error has occurred if we fall through to one.
const errorTargetName = "ERROR"
// redirectTargetName is used to mark targets as redirect targets. Redirect
// targets should be reached for only NAT and Mangle tables. These targets will
// change the destination port/destination IP for packets.
const redirectTargetName = "REDIRECT"
// Metadata is used to verify that we are correctly serializing and
// deserializing iptables into structs consumable by the iptables tool. We save
// a metadata struct when the tables are written, and when they are read out we
@@ -240,6 +245,8 @@ func marshalTarget(target iptables.Target) []byte {
return marshalErrorTarget(tg.Name)
case iptables.ReturnTarget:
return marshalStandardTarget(iptables.RuleReturn)
case iptables.RedirectTarget:
return marshalRedirectTarget()
default:
panic(fmt.Errorf("unknown target of type %T", target))
}
@@ -274,6 +281,18 @@ func marshalErrorTarget(errorName string) []byte {
return binary.Marshal(ret, usermem.ByteOrder, target)
}
func marshalRedirectTarget() []byte {
// This is a redirect target named redirect
target := linux.XTRedirectTarget{
Target: linux.XTEntryTarget{
TargetSize: linux.SizeOfXTRedirectTarget,
},
}
ret := make([]byte, 0, linux.SizeOfXTRedirectTarget)
return binary.Marshal(ret, usermem.ByteOrder, target)
}
// translateFromStandardVerdict translates verdicts the same way as the iptables
// tool.
func translateFromStandardVerdict(verdict iptables.RuleVerdict) int32 {
@@ -326,6 +345,8 @@ func SetEntries(stack *stack.Stack, optVal []byte) *syserr.Error {
switch replace.Name.String() {
case iptables.TablenameFilter:
table = iptables.EmptyFilterTable()
case iptables.TablenameNat:
table = iptables.EmptyNatTable()
default:
nflog("we don't yet support writing to the %q table (gvisor.dev/issue/170)", replace.Name.String())
return syserr.ErrInvalidArgument
@@ -455,10 +476,11 @@ func SetEntries(stack *stack.Stack, optVal []byte) *syserr.Error {
}
// TODO(gvisor.dev/issue/170): Support other chains.
// Since we only support modifying the INPUT chain right now, make sure
// all other chains point to ACCEPT rules.
// Since we only support modifying the INPUT chain and redirect for
// PREROUTING chain right now, make sure all other chains point to
// ACCEPT rules.
for hook, ruleIdx := range table.BuiltinChains {
if hook != iptables.Input {
if hook != iptables.Input && hook != iptables.Prerouting {
if _, ok := table.Rules[ruleIdx].Target.(iptables.AcceptTarget); !ok {
nflog("hook %d is unsupported.", hook)
return syserr.ErrInvalidArgument
@@ -575,6 +597,36 @@ func parseTarget(optVal []byte) (iptables.Target, error) {
nflog("set entries: user-defined target %q", name)
return iptables.UserChainTarget{Name: name}, nil
}
case redirectTargetName:
// Redirect target.
if len(optVal) < linux.SizeOfXTRedirectTarget {
return nil, fmt.Errorf("netfilter.SetEntries: optVal has insufficient size for redirect target %d", len(optVal))
}
var redirectTarget linux.XTRedirectTarget
buf = optVal[:linux.SizeOfXTRedirectTarget]
binary.Unmarshal(buf, usermem.ByteOrder, &redirectTarget)
// Copy linux.XTRedirectTarget to iptables.RedirectTarget.
var target iptables.RedirectTarget
nfRange := redirectTarget.NfRange
target.RangeSize = nfRange.Rangesize
target.Flags = nfRange.RangeIPV4[0].Flags
target.MinIP = tcpip.Address(nfRange.RangeIPV4[0].MinIP[:])
target.MaxIP = tcpip.Address(nfRange.RangeIPV4[0].MaxIP[:])
// Convert port from big endian to little endian.
port := make([]byte, 2)
binary.BigEndian.PutUint16(port, nfRange.RangeIPV4[0].MinPort)
target.MinPort = binary.LittleEndian.Uint16(port)
binary.BigEndian.PutUint16(port, nfRange.RangeIPV4[0].MaxPort)
target.MaxPort = binary.LittleEndian.Uint16(port)
return target, nil
}
// Unknown target.
+21
View File
@@ -135,6 +135,27 @@ func EmptyFilterTable() Table {
}
}
// EmptyNatTable returns a Table with no rules and the filter table chains
// mapped to HookUnset.
func EmptyNatTable() Table {
return Table{
Rules: []Rule{},
BuiltinChains: map[Hook]int{
Prerouting: HookUnset,
Input: HookUnset,
Output: HookUnset,
Postrouting: HookUnset,
},
Underflows: map[Hook]int{
Prerouting: HookUnset,
Input: HookUnset,
Output: HookUnset,
Postrouting: HookUnset,
},
UserChains: map[string]int{},
}
}
// Check runs pkt through the rules for hook. It returns true when the packet
// should continue traversing the network stack and false when it should be
// dropped.
+24
View File
@@ -19,6 +19,7 @@ package iptables
import (
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
// AcceptTarget accepts packets.
@@ -65,3 +66,26 @@ type ReturnTarget struct{}
func (ReturnTarget) Action(tcpip.PacketBuffer) (RuleVerdict, string) {
return RuleReturn, ""
}
// RedirectTarget redirects the packet by modifying the destination port/IP.
type RedirectTarget struct {
RangeSize uint32
Flags uint32
MinIP tcpip.Address
MaxIP tcpip.Address
MinPort uint16
MaxPort uint16
}
// Action implements Target.Action.
func (rt RedirectTarget) Action(packet tcpip.PacketBuffer) (RuleVerdict, string) {
log.Infof("RedirectTarget triggered.")
// TODO(gvisor.dev/issue/170): Checking only for UDP protocol.
// We're yet to support for TCP protocol.
headerView := packet.Data.First()
h := header.UDP(headerView)
h.SetDestinationPort(rt.MinPort)
return RuleAccept, ""
}
+23
View File
@@ -25,6 +25,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/iptables"
)
// NIC represents a "network interface card" to which the networking stack is
@@ -1012,6 +1013,7 @@ func (n *NIC) leaveGroupLocked(addr tcpip.Address) *tcpip.Error {
func handlePacket(protocol tcpip.NetworkProtocolNumber, dst, src tcpip.Address, localLinkAddr, remotelinkAddr tcpip.LinkAddress, ref *referencedNetworkEndpoint, pkt tcpip.PacketBuffer) {
r := makeRoute(protocol, dst, src, localLinkAddr, ref, false /* handleLocal */, false /* multicastLoop */)
r.RemoteLinkAddress = remotelinkAddr
ref.ep.HandlePacket(&r, pkt)
ref.decRef()
}
@@ -1082,6 +1084,27 @@ func (n *NIC) DeliverNetworkPacket(linkEP LinkEndpoint, remote, local tcpip.Link
n.stack.stats.IP.InvalidSourceAddressesReceived.Increment()
return
}
// TODO(gvisor.dev/issue/170): Not supporting iptables for IPv6 yet.
if protocol == header.IPv4ProtocolNumber {
newPkt := pkt.Clone()
headerView := newPkt.Data.First()
h := header.IPv4(headerView)
newPkt.NetworkHeader = headerView[:h.HeaderLength()]
hlen := int(h.HeaderLength())
tlen := int(h.TotalLength())
newPkt.Data.TrimFront(hlen)
newPkt.Data.CapLength(tlen - hlen)
ipt := n.stack.IPTables()
if ok := ipt.Check(iptables.Prerouting, newPkt); !ok {
// iptables is telling us to drop the packet.
return
}
}
if ref := n.getRef(protocol, dst); ref != nil {
handlePacket(protocol, dst, src, linkEP.LinkAddress(), remote, ref, pkt)
return
+12
View File
@@ -196,12 +196,24 @@ func TestNATRedirectUDPPort(t *testing.T) {
}
}
func TestNATRedirectTCPPort(t *testing.T) {
if err := singleTest(NATRedirectTCPPort{}); err != nil {
t.Fatal(err)
}
}
func TestNATDropUDP(t *testing.T) {
if err := singleTest(NATDropUDP{}); err != nil {
t.Fatal(err)
}
}
func TestNATAcceptAll(t *testing.T) {
if err := singleTest(NATAcceptAll{}); err != nil {
t.Fatal(err)
}
}
func TestFilterInputDropTCPDestPort(t *testing.T) {
if err := singleTest(FilterInputDropTCPDestPort{}); err != nil {
t.Fatal(err)
+10
View File
@@ -35,6 +35,16 @@ func filterTable(args ...string) error {
return nil
}
// natTable calls `iptables -t nat` with the given args.
func natTable(args ...string) error {
args = append([]string{"-t", "nat"}, args...)
cmd := exec.Command(iptablesBinary, args...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("error running iptables with args %v\nerror: %v\noutput: %s", args, err, string(out))
}
return nil
}
// listenUDP listens on a UDP port and returns the value of net.Conn.Read() for
// the first read on that port.
func listenUDP(port int, timeout time.Duration) error {
+62 -2
View File
@@ -25,7 +25,9 @@ const (
func init() {
RegisterTestCase(NATRedirectUDPPort{})
RegisterTestCase(NATRedirectTCPPort{})
RegisterTestCase(NATDropUDP{})
RegisterTestCase(NATAcceptAll{})
}
// NATRedirectUDPPort tests that packets are redirected to different port.
@@ -38,13 +40,14 @@ func (NATRedirectUDPPort) Name() string {
// ContainerAction implements TestCase.ContainerAction.
func (NATRedirectUDPPort) ContainerAction(ip net.IP) error {
if err := filterTable("-t", "nat", "-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
if err := natTable("-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
return err
}
if err := listenUDP(redirectPort, sendloopDuration); err != nil {
return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %v", redirectPort, err)
}
return nil
}
@@ -53,6 +56,37 @@ func (NATRedirectUDPPort) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// NATRedirectTCPPort tests that connections are redirected on specified ports.
type NATRedirectTCPPort struct{}
// Name implements TestCase.Name.
func (NATRedirectTCPPort) Name() string {
return "NATRedirectTCPPort"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATRedirectTCPPort) ContainerAction(ip net.IP) error {
if err := natTable("-A", "PREROUTING", "-p", "tcp", "-m", "tcp", "--dport", fmt.Sprintf("%d", dropPort), "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
return err
}
// Listen for TCP packets on redirect port.
if err := listenTCP(redirectPort, sendloopDuration); err != nil {
return fmt.Errorf("connection on port %d should be accepted, but got error %v", redirectPort, err)
}
return nil
}
// LocalAction implements TestCase.LocalAction.
func (NATRedirectTCPPort) LocalAction(ip net.IP) error {
if err := connectTCP(ip, dropPort, acceptPort, sendloopDuration); err != nil {
return fmt.Errorf("connection destined to port %d should be accepted, but got error %v", dropPort, err)
}
return nil
}
// NATDropUDP tests that packets are not received in ports other than redirect port.
type NATDropUDP struct{}
@@ -63,7 +97,7 @@ func (NATDropUDP) Name() string {
// ContainerAction implements TestCase.ContainerAction.
func (NATDropUDP) ContainerAction(ip net.IP) error {
if err := filterTable("-t", "nat", "-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
if err := natTable("-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
return err
}
@@ -78,3 +112,29 @@ func (NATDropUDP) ContainerAction(ip net.IP) error {
func (NATDropUDP) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// NATAcceptAll tests that all UDP packets are accepted.
type NATAcceptAll struct{}
// Name implements TestCase.Name.
func (NATAcceptAll) Name() string {
return "NATAcceptAll"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATAcceptAll) ContainerAction(ip net.IP) error {
if err := natTable("-A", "PREROUTING", "-p", "udp", "-j", "ACCEPT"); err != nil {
return err
}
if err := listenUDP(acceptPort, sendloopDuration); err != nil {
return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %v", acceptPort, err)
}
return nil
}
// LocalAction implements TestCase.LocalAction.
func (NATAcceptAll) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}