|
|
@@ -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.
|