|
|
|
|
@ -2,7 +2,7 @@ |
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
// Package filter contains a stateful packet filter.
|
|
|
|
|
// Package filter is a stateful packet filter.
|
|
|
|
|
package filter |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
@ -14,15 +14,9 @@ import ( |
|
|
|
|
"golang.org/x/time/rate" |
|
|
|
|
"inet.af/netaddr" |
|
|
|
|
"tailscale.com/net/packet" |
|
|
|
|
"tailscale.com/tailcfg" |
|
|
|
|
"tailscale.com/types/logger" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
type filterState struct { |
|
|
|
|
mu sync.Mutex |
|
|
|
|
lru *lru.Cache // of tuple
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Filter is a stateful packet filter.
|
|
|
|
|
type Filter struct { |
|
|
|
|
logf logger.Logf |
|
|
|
|
@ -45,14 +39,31 @@ type Filter struct { |
|
|
|
|
state *filterState |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Response is a verdict: either a Drop, Accept, or noVerdict skip to
|
|
|
|
|
// continue processing.
|
|
|
|
|
// tuple is a 4-tuple of source and destination IPv4 and port. It's
|
|
|
|
|
// used as a lookup key in filterState.
|
|
|
|
|
type tuple struct { |
|
|
|
|
SrcIP packet.IP4 |
|
|
|
|
DstIP packet.IP4 |
|
|
|
|
SrcPort uint16 |
|
|
|
|
DstPort uint16 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// filterState is a state cache of past seen packets.
|
|
|
|
|
type filterState struct { |
|
|
|
|
mu sync.Mutex |
|
|
|
|
lru *lru.Cache // of tuple
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// lruMax is the size of the LRU cache in filterState.
|
|
|
|
|
const lruMax = 512 |
|
|
|
|
|
|
|
|
|
// Response is a verdict from the packet filter.
|
|
|
|
|
type Response int |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
Drop Response = iota |
|
|
|
|
Accept |
|
|
|
|
noVerdict // Returned from subfilters to continue processing.
|
|
|
|
|
Drop Response = iota // do not continue processing packet.
|
|
|
|
|
Accept // continue processing packet.
|
|
|
|
|
noVerdict // no verdict yet, continue running filter
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
func (r Response) String() string { |
|
|
|
|
@ -72,30 +83,16 @@ func (r Response) String() string { |
|
|
|
|
type RunFlags int |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
LogDrops RunFlags = 1 << iota |
|
|
|
|
LogAccepts |
|
|
|
|
HexdumpDrops |
|
|
|
|
HexdumpAccepts |
|
|
|
|
LogDrops RunFlags = 1 << iota // write dropped packet info to logf
|
|
|
|
|
LogAccepts // write accepted packet info to logf
|
|
|
|
|
HexdumpDrops // print packet hexdump when logging drops
|
|
|
|
|
HexdumpAccepts // print packet hexdump when logging accepts
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
type tuple struct { |
|
|
|
|
SrcIP packet.IP4 |
|
|
|
|
DstIP packet.IP4 |
|
|
|
|
SrcPort uint16 |
|
|
|
|
DstPort uint16 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const lruMax = 512 // max entries in UDP LRU cache
|
|
|
|
|
|
|
|
|
|
// MatchAllowAll matches all packets.
|
|
|
|
|
var MatchAllowAll = Matches{ |
|
|
|
|
Match{NetPortRangeAny, NetAny}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// NewAllowAll returns a packet filter that accepts everything to and
|
|
|
|
|
// from localNets.
|
|
|
|
|
func NewAllowAll(localNets []netaddr.IPPrefix, logf logger.Logf) *Filter { |
|
|
|
|
return New(MatchAllowAll, localNets, nil, logf) |
|
|
|
|
return New(Matches{Match{NetPortRangeAny, NetAny}}, localNets, nil, logf) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// NewAllowNone returns a packet filter that rejects everything.
|
|
|
|
|
@ -106,8 +103,8 @@ func NewAllowNone(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.
|
|
|
|
|
// shares state with the previous one, to enable changing rules at
|
|
|
|
|
// runtime without breaking existing stateful flows.
|
|
|
|
|
func New(matches Matches, localNets []netaddr.IPPrefix, shareStateWith *Filter, logf logger.Logf) *Filter { |
|
|
|
|
var state *filterState |
|
|
|
|
if shareStateWith != nil { |
|
|
|
|
@ -133,82 +130,6 @@ func maybeHexdump(flag RunFlags, b []byte) string { |
|
|
|
|
return packet.Hexdump(b) + "\n" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MatchesFromFilterRules parse a number of wire-format FilterRule values into
|
|
|
|
|
// the Matches format.
|
|
|
|
|
// If an error is returned, the Matches result is still valid, containing the rules that
|
|
|
|
|
// were successfully converted.
|
|
|
|
|
func MatchesFromFilterRules(pf []tailcfg.FilterRule) (Matches, error) { |
|
|
|
|
mm := make([]Match, 0, len(pf)) |
|
|
|
|
var erracc error |
|
|
|
|
|
|
|
|
|
for _, r := range pf { |
|
|
|
|
m := Match{} |
|
|
|
|
|
|
|
|
|
for i, s := range r.SrcIPs { |
|
|
|
|
bits := 32 |
|
|
|
|
if len(r.SrcBits) > i { |
|
|
|
|
bits = r.SrcBits[i] |
|
|
|
|
} |
|
|
|
|
net, err := parseIP(s, bits) |
|
|
|
|
if err != nil && erracc == nil { |
|
|
|
|
erracc = err |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
m.Srcs = append(m.Srcs, net) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for _, d := range r.DstPorts { |
|
|
|
|
bits := 32 |
|
|
|
|
if d.Bits != nil { |
|
|
|
|
bits = *d.Bits |
|
|
|
|
} |
|
|
|
|
net, err := parseIP(d.IP, bits) |
|
|
|
|
if err != nil && erracc == nil { |
|
|
|
|
erracc = err |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
m.Dsts = append(m.Dsts, NetPortRange{ |
|
|
|
|
Net: net, |
|
|
|
|
Ports: PortRange{ |
|
|
|
|
First: d.Ports.First, |
|
|
|
|
Last: d.Ports.Last, |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
mm = append(mm, m) |
|
|
|
|
} |
|
|
|
|
return mm, erracc |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func parseIP(host string, defaultBits int) (netaddr.IPPrefix, error) { |
|
|
|
|
if host == "*" { |
|
|
|
|
// User explicitly requested wildcard dst ip.
|
|
|
|
|
// TODO: ipv6
|
|
|
|
|
return netaddr.IPPrefix{IP: netaddr.IPv4(0, 0, 0, 0), Bits: 0}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ip, err := netaddr.ParseIP(host) |
|
|
|
|
if err != nil { |
|
|
|
|
return netaddr.IPPrefix{}, fmt.Errorf("ports=%#v: invalid IP address", host) |
|
|
|
|
} |
|
|
|
|
if ip == netaddr.IPv4(0, 0, 0, 0) { |
|
|
|
|
// For clarity, reject 0.0.0.0 as an input
|
|
|
|
|
return netaddr.IPPrefix{}, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host) |
|
|
|
|
} |
|
|
|
|
if !ip.Is4() { |
|
|
|
|
// TODO: ipv6
|
|
|
|
|
return netaddr.IPPrefix{}, fmt.Errorf("ports=%#v: invalid IPv4 address", host) |
|
|
|
|
} |
|
|
|
|
if defaultBits < 0 || defaultBits > 32 { |
|
|
|
|
return netaddr.IPPrefix{}, fmt.Errorf("invalid CIDR size %d for host %q", defaultBits, host) |
|
|
|
|
} |
|
|
|
|
return netaddr.IPPrefix{ |
|
|
|
|
IP: ip, |
|
|
|
|
Bits: uint8(defaultBits), |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO(apenwarr): use a bigger bucket for specifically TCP SYN accept logging?
|
|
|
|
|
// Logging is a quick way to record every newly opened TCP connection, but
|
|
|
|
|
// we have to be cautious about flooding the logs vs letting people use
|
|
|
|
|
@ -240,7 +161,8 @@ func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, dir dir |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// RunIn determines whether this node is allowed to receive q from a Tailscale peer.
|
|
|
|
|
// RunIn determines whether this node is allowed to receive q from a
|
|
|
|
|
// Tailscale peer.
|
|
|
|
|
func (f *Filter) RunIn(q *packet.ParsedPacket, rf RunFlags) Response { |
|
|
|
|
dir := in |
|
|
|
|
r := f.pre(q, rf, dir) |
|
|
|
|
@ -254,7 +176,8 @@ func (f *Filter) RunIn(q *packet.ParsedPacket, rf RunFlags) Response { |
|
|
|
|
return r |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// RunOut determines whether this node is allowed to send q to a Tailscale peer.
|
|
|
|
|
// RunOut determines whether this node is allowed to send q to a
|
|
|
|
|
// Tailscale peer.
|
|
|
|
|
func (f *Filter) RunOut(q *packet.ParsedPacket, rf RunFlags) Response { |
|
|
|
|
dir := out |
|
|
|
|
r := f.pre(q, rf, dir) |
|
|
|
|
@ -267,6 +190,7 @@ func (f *Filter) RunOut(q *packet.ParsedPacket, rf RunFlags) Response { |
|
|
|
|
return r |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// runIn runs the input-specific part of the filter logic.
|
|
|
|
|
func (f *Filter) runIn(q *packet.ParsedPacket) (r Response, why string) { |
|
|
|
|
// A compromised peer could try to send us packets for
|
|
|
|
|
// destinations we didn't explicitly advertise. This check is to
|
|
|
|
|
@ -327,6 +251,7 @@ func (f *Filter) runIn(q *packet.ParsedPacket) (r Response, why string) { |
|
|
|
|
return Drop, "no rules matched" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// runIn runs the output-specific part of the filter logic.
|
|
|
|
|
func (f *Filter) runOut(q *packet.ParsedPacket) (r Response, why string) { |
|
|
|
|
if q.IPProto == packet.UDP { |
|
|
|
|
t := tuple{q.DstIP, q.SrcIP, q.DstPort, q.SrcPort} |
|
|
|
|
@ -339,12 +264,13 @@ func (f *Filter) runOut(q *packet.ParsedPacket) (r Response, why string) { |
|
|
|
|
return Accept, "ok out" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// direction is whether a packet was flowing in to this machine, or flowing out.
|
|
|
|
|
// direction is whether a packet was flowing in to this machine, or
|
|
|
|
|
// flowing out.
|
|
|
|
|
type direction int |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
in direction = iota |
|
|
|
|
out |
|
|
|
|
in direction = iota // from Tailscale peer to local machine
|
|
|
|
|
out // from local machine to Tailscale peer
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
func (d direction) String() string { |
|
|
|
|
@ -358,6 +284,8 @@ func (d direction) String() string { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// pre runs the direction-agnostic filter logic. dir is only used for
|
|
|
|
|
// logging.
|
|
|
|
|
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Response { |
|
|
|
|
if len(q.Buffer()) == 0 { |
|
|
|
|
// wireguard keepalive packet, always permit.
|
|
|
|
|
|