wgengine/filter: implement a destination IP pre-filter.

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson
2020-05-22 02:41:18 +00:00
parent 35a8586f7e
commit e8b3a5e7a1
4 changed files with 94 additions and 33 deletions

View File

@@ -23,9 +23,22 @@ type filterState struct {
// Filter is a stateful packet filter.
type Filter struct {
logf logger.Logf
logf logger.Logf
// localNets is the list of IP prefixes that we know to be "local"
// to this node. All packets coming in over tailscale must have a
// destination within localNets, regardless of the policy filter
// below. A nil localNets rejects all incoming traffic.
localNets []Net
// matches is a list of match->action rules applied to all packets
// arriving over tailscale tunnels. Matches are checked in order,
// and processing stops at the first matching rule. The default
// policy if no rules match is to drop the packet.
matches Matches
state *filterState
// state is the connection tracking state attached to this
// filter. It is used to allow incoming traffic that is a response
// to an outbound connection that this node made, even if those
// incoming packets don't get accepted by matches above.
state *filterState
}
// Response is a verdict: either a Drop, Accept, or noVerdict skip to
@@ -75,21 +88,23 @@ var MatchAllowAll = Matches{
Match{[]NetPortRange{NetPortRangeAny}, []Net{NetAny}},
}
// NewAllowAll returns a packet filter that accepts everything.
func NewAllowAll(logf logger.Logf) *Filter {
return New(MatchAllowAll, nil, logf)
// NewAllowAll returns a packet filter that accepts everything to and
// from localNets.
func NewAllowAll(localNets []Net, logf logger.Logf) *Filter {
return New(MatchAllowAll, localNets, nil, logf)
}
// NewAllowNone returns a packet filter that rejects everything.
func NewAllowNone(logf logger.Logf) *Filter {
return New(nil, nil, logf)
return New(nil, nil, nil, logf)
}
// New creates a new packet Filter with the given Matches rules.
// If shareStateWith is non-nil, the returned filter shares state
// with the previous one, to enable rules to be changed at runtime
// without breaking existing flows.
func New(matches Matches, shareStateWith *Filter, logf logger.Logf) *Filter {
// New creates a new packet filter. The filter enforces that incoming
// packets must be destined to an IP in localNets, and must be allowed
// by matches. If shareStateWith is non-nil, the returned filter
// shares state with the previous one, to enable rules to be changed
// at runtime without breaking existing flows.
func New(matches Matches, localNets []Net, shareStateWith *Filter, logf logger.Logf) *Filter {
var state *filterState
if shareStateWith != nil {
state = shareStateWith.state
@@ -99,9 +114,10 @@ func New(matches Matches, shareStateWith *Filter, logf logger.Logf) *Filter {
}
}
f := &Filter{
logf: logf,
matches: matches,
state: state,
logf: logf,
matches: matches,
localNets: localNets,
state: state,
}
return f
}
@@ -159,6 +175,13 @@ func (f *Filter) RunOut(b []byte, q *packet.QDecode, rf RunFlags) Response {
}
func (f *Filter) runIn(q *packet.QDecode) (r Response, why string) {
// A compromised peer could try to send us packets for
// destinations we didn't explicitly advertise. This check is to
// prevent that.
if !ipInList(q.DstIP, f.localNets) {
return Drop, "destination not allowed"
}
switch q.IPProto {
case packet.ICMP:
if q.IsEchoResponse() || q.IsError() {

View File

@@ -55,7 +55,12 @@ func TestFilter(t *testing.T) {
{Srcs: []Net{NetAny}, Dsts: netpr(0, 0, 443, 443)},
{Srcs: nets([]IP{0x99010101, 0x99010102, 0x99030303}), Dsts: ippr(0x01020304, 999, 999)},
}
acl := New(mm, nil, t.Logf)
// Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8,
// 102.102.102.102, 119.119.119.119, 8.1.0.0/16
localNets := nets([]IP{0x647a6232, 0x01020304, 0x05060708, 0x66666666, 0x77777777})
localNets = append(localNets, Net{IP(0x08010000), Netmask(16)})
acl := New(mm, localNets, nil, t.Logf)
for _, ent := range []Matches{Matches{mm[0]}, mm} {
b, err := json.Marshal(ent)
@@ -83,12 +88,18 @@ func TestFilter(t *testing.T) {
{Drop, qdecode(TCP, 0x08010101, 0x01020304, 0, 0)},
{Accept, qdecode(TCP, 0x08010101, 0x01020304, 0, 22)},
{Drop, qdecode(TCP, 0x08010101, 0x01020304, 0, 21)},
{Accept, qdecode(TCP, 0x11223344, 0x22334455, 0, 443)},
{Drop, qdecode(TCP, 0x11223344, 0x22334455, 0, 444)},
{Accept, qdecode(TCP, 0x11223344, 0x08012233, 0, 443)},
{Drop, qdecode(TCP, 0x11223344, 0x08012233, 0, 444)},
{Accept, qdecode(TCP, 0x11223344, 0x647a6232, 0, 999)},
{Accept, qdecode(TCP, 0x11223344, 0x647a6232, 0, 0)},
// Stateful UDP.
// localNets prefilter - accepted by policy filter, but
// unexpected dst IP.
{Drop, qdecode(TCP, 0x08010101, 0x10203040, 0, 443)},
// Stateful UDP. Note each packet is run through the input
// filter, then the output filter (which sets conntrack
// state).
// Initially empty cache
{Drop, qdecode(UDP, 0x77777777, 0x66666666, 4242, 4343)},
// Return packet from previous attempt is allowed