iptables - filter packets using outgoing interface.

Enables commands with -o (--out-interface) for iptables rules.
$ iptables -A OUTPUT -o eth0 -j ACCEPT

PiperOrigin-RevId: 310642286
This commit is contained in:
gVisor bot
2020-05-08 15:44:54 -07:00
parent e4d2d21f6b
commit cfd30665c1
10 changed files with 309 additions and 23 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ type IPTIP struct {
// OutputInterface is the output network interface.
OutputInterface [IFNAMSIZ]byte
// InputInterfaceMask is the intput interface mask.
// InputInterfaceMask is the input interface mask.
InputInterfaceMask [IFNAMSIZ]byte
// OuputInterfaceMask is the output interface mask.
+33 -7
View File
@@ -17,6 +17,7 @@
package netfilter
import (
"bytes"
"errors"
"fmt"
@@ -204,6 +205,16 @@ func convertNetstackToBinary(tablename string, table stack.Table) (linux.KernelI
TargetOffset: linux.SizeOfIPTEntry,
},
}
copy(entry.IPTEntry.IP.Dst[:], rule.Filter.Dst)
copy(entry.IPTEntry.IP.DstMask[:], rule.Filter.DstMask)
copy(entry.IPTEntry.IP.OutputInterface[:], rule.Filter.OutputInterface)
copy(entry.IPTEntry.IP.OutputInterfaceMask[:], rule.Filter.OutputInterfaceMask)
if rule.Filter.DstInvert {
entry.IPTEntry.IP.InverseFlags |= linux.IPT_INV_DSTIP
}
if rule.Filter.OutputInterfaceInvert {
entry.IPTEntry.IP.InverseFlags |= linux.IPT_INV_VIA_OUT
}
for _, matcher := range rule.Matchers {
// Serialize the matcher and add it to the
@@ -719,11 +730,27 @@ func filterFromIPTIP(iptip linux.IPTIP) (stack.IPHeaderFilter, error) {
if len(iptip.Dst) != header.IPv4AddressSize || len(iptip.DstMask) != header.IPv4AddressSize {
return stack.IPHeaderFilter{}, fmt.Errorf("incorrect length of destination (%d) and/or destination mask (%d) fields", len(iptip.Dst), len(iptip.DstMask))
}
n := bytes.IndexByte([]byte(iptip.OutputInterface[:]), 0)
if n == -1 {
n = len(iptip.OutputInterface)
}
ifname := string(iptip.OutputInterface[:n])
n = bytes.IndexByte([]byte(iptip.OutputInterfaceMask[:]), 0)
if n == -1 {
n = len(iptip.OutputInterfaceMask)
}
ifnameMask := string(iptip.OutputInterfaceMask[:n])
return stack.IPHeaderFilter{
Protocol: tcpip.TransportProtocolNumber(iptip.Protocol),
Dst: tcpip.Address(iptip.Dst[:]),
DstMask: tcpip.Address(iptip.DstMask[:]),
DstInvert: iptip.InverseFlags&linux.IPT_INV_DSTIP != 0,
Protocol: tcpip.TransportProtocolNumber(iptip.Protocol),
Dst: tcpip.Address(iptip.Dst[:]),
DstMask: tcpip.Address(iptip.DstMask[:]),
DstInvert: iptip.InverseFlags&linux.IPT_INV_DSTIP != 0,
OutputInterface: ifname,
OutputInterfaceMask: ifnameMask,
OutputInterfaceInvert: iptip.InverseFlags&linux.IPT_INV_VIA_OUT != 0,
}, nil
}
@@ -732,16 +759,15 @@ func containsUnsupportedFields(iptip linux.IPTIP) bool {
// - Protocol
// - Dst and DstMask
// - The inverse destination IP check flag
// - OutputInterface, OutputInterfaceMask and its inverse.
var emptyInetAddr = linux.InetAddr{}
var emptyInterface = [linux.IFNAMSIZ]byte{}
// Disable any supported inverse flags.
inverseMask := uint8(linux.IPT_INV_DSTIP)
inverseMask := uint8(linux.IPT_INV_DSTIP) | uint8(linux.IPT_INV_VIA_OUT)
return iptip.Src != emptyInetAddr ||
iptip.SrcMask != emptyInetAddr ||
iptip.InputInterface != emptyInterface ||
iptip.OutputInterface != emptyInterface ||
iptip.InputInterfaceMask != emptyInterface ||
iptip.OutputInterfaceMask != emptyInterface ||
iptip.Flags != 0 ||
iptip.InverseFlags&^inverseMask != 0
}
+5 -3
View File
@@ -249,10 +249,11 @@ func (e *endpoint) WritePacket(r *stack.Route, gso *stack.GSO, params stack.Netw
ip := e.addIPHeader(r, &pkt.Header, pkt.Data.Size(), params)
pkt.NetworkHeader = buffer.View(ip)
nicName := e.stack.FindNICNameFromID(e.NICID())
// iptables filtering. All packets that reach here are locally
// generated.
ipt := e.stack.IPTables()
if ok := ipt.Check(stack.Output, &pkt, gso, r, ""); !ok {
if ok := ipt.Check(stack.Output, &pkt, gso, r, "", nicName); !ok {
// iptables is telling us to drop the packet.
return nil
}
@@ -319,10 +320,11 @@ func (e *endpoint) WritePackets(r *stack.Route, gso *stack.GSO, pkts stack.Packe
pkt = pkt.Next()
}
nicName := e.stack.FindNICNameFromID(e.NICID())
// iptables filtering. All packets that reach here are locally
// generated.
ipt := e.stack.IPTables()
dropped, natPkts := ipt.CheckPackets(stack.Output, pkts, gso, r)
dropped, natPkts := ipt.CheckPackets(stack.Output, pkts, gso, r, nicName)
if len(dropped) == 0 && len(natPkts) == 0 {
// Fast path: If no packets are to be dropped then we can just invoke the
// faster WritePackets API directly.
@@ -445,7 +447,7 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt stack.PacketBuffer) {
// iptables filtering. All packets that reach here are intended for
// this machine and will not be forwarded.
ipt := e.stack.IPTables()
if ok := ipt.Check(stack.Input, &pkt, nil, nil, ""); !ok {
if ok := ipt.Check(stack.Input, &pkt, nil, nil, "", ""); !ok {
// iptables is telling us to drop the packet.
return
}
+32 -10
View File
@@ -16,6 +16,7 @@ package stack
import (
"fmt"
"strings"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
@@ -178,7 +179,7 @@ const (
// dropped.
//
// Precondition: pkt.NetworkHeader is set.
func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, address tcpip.Address) bool {
func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, address tcpip.Address, nicName string) bool {
// Packets are manipulated only if connection and matching
// NAT rule exists.
it.connections.HandlePacket(pkt, hook, gso, r)
@@ -187,7 +188,7 @@ func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, addr
for _, tablename := range it.Priorities[hook] {
table := it.Tables[tablename]
ruleIdx := table.BuiltinChains[hook]
switch verdict := it.checkChain(hook, pkt, table, ruleIdx, gso, r, address); verdict {
switch verdict := it.checkChain(hook, pkt, table, ruleIdx, gso, r, address, nicName); verdict {
// If the table returns Accept, move on to the next table.
case chainAccept:
continue
@@ -228,10 +229,10 @@ func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, addr
//
// NOTE: unlike the Check API the returned map contains packets that should be
// dropped.
func (it *IPTables) CheckPackets(hook Hook, pkts PacketBufferList, gso *GSO, r *Route) (drop map[*PacketBuffer]struct{}, natPkts map[*PacketBuffer]struct{}) {
func (it *IPTables) CheckPackets(hook Hook, pkts PacketBufferList, gso *GSO, r *Route, nicName string) (drop map[*PacketBuffer]struct{}, natPkts map[*PacketBuffer]struct{}) {
for pkt := pkts.Front(); pkt != nil; pkt = pkt.Next() {
if !pkt.NatDone {
if ok := it.Check(hook, pkt, gso, r, ""); !ok {
if ok := it.Check(hook, pkt, gso, r, "", nicName); !ok {
if drop == nil {
drop = make(map[*PacketBuffer]struct{})
}
@@ -251,11 +252,11 @@ func (it *IPTables) CheckPackets(hook Hook, pkts PacketBufferList, gso *GSO, r *
// Precondition: pkt is a IPv4 packet of at least length header.IPv4MinimumSize.
// TODO(gvisor.dev/issue/170): pkt.NetworkHeader will always be set as a
// precondition.
func (it *IPTables) checkChain(hook Hook, pkt *PacketBuffer, table Table, ruleIdx int, gso *GSO, r *Route, address tcpip.Address) chainVerdict {
func (it *IPTables) checkChain(hook Hook, pkt *PacketBuffer, table Table, ruleIdx int, gso *GSO, r *Route, address tcpip.Address, nicName string) chainVerdict {
// Start from ruleIdx and walk the list of rules until a rule gives us
// a verdict.
for ruleIdx < len(table.Rules) {
switch verdict, jumpTo := it.checkRule(hook, pkt, table, ruleIdx, gso, r, address); verdict {
switch verdict, jumpTo := it.checkRule(hook, pkt, table, ruleIdx, gso, r, address, nicName); verdict {
case RuleAccept:
return chainAccept
@@ -272,7 +273,7 @@ func (it *IPTables) checkChain(hook Hook, pkt *PacketBuffer, table Table, ruleId
ruleIdx++
continue
}
switch verdict := it.checkChain(hook, pkt, table, jumpTo, gso, r, address); verdict {
switch verdict := it.checkChain(hook, pkt, table, jumpTo, gso, r, address, nicName); verdict {
case chainAccept:
return chainAccept
case chainDrop:
@@ -298,7 +299,7 @@ func (it *IPTables) checkChain(hook Hook, pkt *PacketBuffer, table Table, ruleId
// Precondition: pkt is a IPv4 packet of at least length header.IPv4MinimumSize.
// TODO(gvisor.dev/issue/170): pkt.NetworkHeader will always be set as a
// precondition.
func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx int, gso *GSO, r *Route, address tcpip.Address) (RuleVerdict, int) {
func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx int, gso *GSO, r *Route, address tcpip.Address, nicName string) (RuleVerdict, int) {
rule := table.Rules[ruleIdx]
// If pkt.NetworkHeader hasn't been set yet, it will be contained in
@@ -313,7 +314,7 @@ func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx
}
// Check whether the packet matches the IP header filter.
if !filterMatch(rule.Filter, header.IPv4(pkt.NetworkHeader)) {
if !filterMatch(rule.Filter, header.IPv4(pkt.NetworkHeader), hook, nicName) {
// Continue on to the next rule.
return RuleJump, ruleIdx + 1
}
@@ -335,7 +336,7 @@ func (it *IPTables) checkRule(hook Hook, pkt *PacketBuffer, table Table, ruleIdx
return rule.Target.Action(pkt, &it.connections, hook, gso, r, address)
}
func filterMatch(filter IPHeaderFilter, hdr header.IPv4) bool {
func filterMatch(filter IPHeaderFilter, hdr header.IPv4, hook Hook, nicName string) bool {
// TODO(gvisor.dev/issue/170): Support other fields of the filter.
// Check the transport protocol.
if filter.Protocol != 0 && filter.Protocol != hdr.TransportProtocol() {
@@ -355,5 +356,26 @@ func filterMatch(filter IPHeaderFilter, hdr header.IPv4) bool {
return false
}
// Check the output interface.
// TODO(gvisor.dev/issue/170): Add the check for FORWARD and POSTROUTING
// hooks after supported.
if hook == Output {
n := len(filter.OutputInterface)
if n == 0 {
return true
}
// If the interface name ends with '+', any interface which begins
// with the name should be matched.
ifName := filter.OutputInterface
matches = true
if strings.HasSuffix(ifName, "+") {
matches = strings.HasPrefix(nicName, ifName[:n-1])
} else {
matches = nicName == ifName
}
return filter.OutputInterfaceInvert != matches
}
return true
}
+13
View File
@@ -158,6 +158,19 @@ type IPHeaderFilter struct {
// true the filter will match packets that fail the destination
// comparison.
DstInvert bool
// OutputInterface matches the name of the outgoing interface for the
// packet.
OutputInterface string
// OutputInterfaceMask masks the characters of the interface name when
// comparing with OutputInterface.
OutputInterfaceMask string
// OutputInterfaceInvert inverts the meaning of outgoing interface check,
// i.e. when true the filter will match packets that fail the outgoing
// interface comparison.
OutputInterfaceInvert bool
}
// A Matcher is the interface for matching packets.
+1 -1
View File
@@ -1233,7 +1233,7 @@ func (n *NIC) DeliverNetworkPacket(linkEP LinkEndpoint, remote, local tcpip.Link
// iptables filtering.
ipt := n.stack.IPTables()
address := n.primaryAddress(protocol)
if ok := ipt.Check(Prerouting, &pkt, nil, nil, address.Address); !ok {
if ok := ipt.Check(Prerouting, &pkt, nil, nil, address.Address, ""); !ok {
// iptables is telling us to drop the packet.
return
}
+15 -1
View File
@@ -1898,9 +1898,23 @@ func (s *Stack) FindNetworkEndpoint(netProto tcpip.NetworkProtocolNumber, addres
nic.mu.RLock()
defer nic.mu.RUnlock()
// An endpoint with this id exists, check if it can be used and return it.
// An endpoint with this id exists, check if it can be
// used and return it.
return ref.ep, nil
}
}
return nil, tcpip.ErrBadAddress
}
// FindNICNameFromID returns the name of the nic for the given NICID.
func (s *Stack) FindNICNameFromID(id tcpip.NICID) string {
s.mu.Lock()
defer s.mu.Unlock()
nic, ok := s.nics[id]
if !ok {
return ""
}
return nic.Name()
}
+170
View File
@@ -29,6 +29,12 @@ func init() {
RegisterTestCase(FilterOutputAcceptUDPOwner{})
RegisterTestCase(FilterOutputDropUDPOwner{})
RegisterTestCase(FilterOutputOwnerFail{})
RegisterTestCase(FilterOutputInterfaceAccept{})
RegisterTestCase(FilterOutputInterfaceDrop{})
RegisterTestCase(FilterOutputInterface{})
RegisterTestCase(FilterOutputInterfaceBeginsWith{})
RegisterTestCase(FilterOutputInterfaceInvertDrop{})
RegisterTestCase(FilterOutputInterfaceInvertAccept{})
}
// FilterOutputDropTCPDestPort tests that connections are not accepted on
@@ -286,3 +292,167 @@ func (FilterOutputInvertDestination) ContainerAction(ip net.IP) error {
func (FilterOutputInvertDestination) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}
// FilterOutputInterfaceAccept tests that packets are sent via interface
// matching the iptables rule.
type FilterOutputInterfaceAccept struct{}
// Name implements TestCase.Name.
func (FilterOutputInterfaceAccept) Name() string {
return "FilterOutputInterfaceAccept"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterfaceAccept) ContainerAction(ip net.IP) error {
ifname, ok := getInterfaceName()
if !ok {
return fmt.Errorf("no interface is present, except loopback")
}
if err := filterTable("-A", "OUTPUT", "-p", "udp", "-o", ifname, "-j", "ACCEPT"); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterfaceAccept) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}
// FilterOutputInterfaceDrop tests that packets are not sent via interface
// matching the iptables rule.
type FilterOutputInterfaceDrop struct{}
// Name implements TestCase.Name.
func (FilterOutputInterfaceDrop) Name() string {
return "FilterOutputInterfaceDrop"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterfaceDrop) ContainerAction(ip net.IP) error {
ifname, ok := getInterfaceName()
if !ok {
return fmt.Errorf("no interface is present, except loopback")
}
if err := filterTable("-A", "OUTPUT", "-p", "udp", "-o", ifname, "-j", "DROP"); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterfaceDrop) LocalAction(ip net.IP) error {
if err := listenUDP(acceptPort, sendloopDuration); err == nil {
return fmt.Errorf("packets should not be received on port %v, but are received", acceptPort)
}
return nil
}
// FilterOutputInterface tests that packets are sent via interface which is
// not matching the interface name in the iptables rule.
type FilterOutputInterface struct{}
// Name implements TestCase.Name.
func (FilterOutputInterface) Name() string {
return "FilterOutputInterface"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterface) ContainerAction(ip net.IP) error {
if err := filterTable("-A", "OUTPUT", "-p", "udp", "-o", "lo", "-j", "DROP"); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterface) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}
// FilterOutputInterfaceBeginsWith tests that packets are not sent via an
// interface which begins with the given interface name.
type FilterOutputInterfaceBeginsWith struct{}
// Name implements TestCase.Name.
func (FilterOutputInterfaceBeginsWith) Name() string {
return "FilterOutputInterfaceBeginsWith"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterfaceBeginsWith) ContainerAction(ip net.IP) error {
if err := filterTable("-A", "OUTPUT", "-p", "udp", "-o", "e+", "-j", "DROP"); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterfaceBeginsWith) LocalAction(ip net.IP) error {
if err := listenUDP(acceptPort, sendloopDuration); err == nil {
return fmt.Errorf("packets should not be received on port %v, but are received", acceptPort)
}
return nil
}
// FilterOutputInterfaceInvertDrop tests that we selectively do not send
// packets via interface not matching the interface name.
type FilterOutputInterfaceInvertDrop struct{}
// Name implements TestCase.Name.
func (FilterOutputInterfaceInvertDrop) Name() string {
return "FilterOutputInterfaceInvertDrop"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterfaceInvertDrop) ContainerAction(ip net.IP) error {
if err := filterTable("-A", "OUTPUT", "-p", "tcp", "!", "-o", "lo", "-j", "DROP"); err != nil {
return err
}
// Listen for TCP packets on accept port.
if err := listenTCP(acceptPort, sendloopDuration); err == nil {
return fmt.Errorf("connection on port %d should not be accepted, but got accepted", acceptPort)
}
return nil
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterfaceInvertDrop) LocalAction(ip net.IP) error {
if err := connectTCP(ip, acceptPort, sendloopDuration); err == nil {
return fmt.Errorf("connection destined to port %d should not be accepted, but got accepted", acceptPort)
}
return nil
}
// FilterOutputInterfaceInvertAccept tests that we can selectively send packets
// not matching the specific outgoing interface.
type FilterOutputInterfaceInvertAccept struct{}
// Name implements TestCase.Name.
func (FilterOutputInterfaceInvertAccept) Name() string {
return "FilterOutputInterfaceInvertAccept"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInterfaceInvertAccept) ContainerAction(ip net.IP) error {
if err := filterTable("-A", "OUTPUT", "-p", "tcp", "!", "-o", "lo", "-j", "ACCEPT"); err != nil {
return err
}
// Listen for TCP packets on accept port.
return listenTCP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInterfaceInvertAccept) LocalAction(ip net.IP) error {
return connectTCP(ip, acceptPort, sendloopDuration)
}
+24
View File
@@ -167,6 +167,30 @@ func TestFilterOutputOwnerFail(t *testing.T) {
singleTest(t, FilterOutputOwnerFail{})
}
func TestFilterOutputInterfaceAccept(t *testing.T) {
singleTest(t, FilterOutputInterfaceAccept{})
}
func TestFilterOutputInterfaceDrop(t *testing.T) {
singleTest(t, FilterOutputInterfaceDrop{})
}
func TestFilterOutputInterface(t *testing.T) {
singleTest(t, FilterOutputInterface{})
}
func TestFilterOutputInterfaceBeginsWith(t *testing.T) {
singleTest(t, FilterOutputInterfaceBeginsWith{})
}
func TestFilterOutputInterfaceInvertDrop(t *testing.T) {
singleTest(t, FilterOutputInterfaceInvertDrop{})
}
func TestFilterOutputInterfaceInvertAccept(t *testing.T) {
singleTest(t, FilterOutputInterfaceInvertAccept{})
}
func TestJumpSerialize(t *testing.T) {
singleTest(t, FilterInputSerializeJump{})
}
+15
View File
@@ -169,3 +169,18 @@ func localAddrs() ([]string, error) {
}
return addrStrs, nil
}
// getInterfaceName returns the name of the interface other than loopback.
func getInterfaceName() (string, bool) {
var ifname string
if interfaces, err := net.Interfaces(); err == nil {
for _, intf := range interfaces {
if intf.Name != "lo" {
ifname = intf.Name
break
}
}
}
return ifname, ifname != ""
}