mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-10 09:45:08 +00:00
Move Linux client & common packages into a public repo.
This commit is contained in:
218
wgengine/filter/filter.go
Normal file
218
wgengine/filter/filter.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"tailscale.com/ratelimit"
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
matches Matches
|
||||
|
||||
udpMu sync.Mutex
|
||||
udplru *lru.Cache
|
||||
}
|
||||
|
||||
type Response int
|
||||
|
||||
const (
|
||||
Drop Response = iota
|
||||
Accept
|
||||
noVerdict // Returned from subfilters to continue processing.
|
||||
)
|
||||
|
||||
func (r Response) String() string {
|
||||
switch r {
|
||||
case Drop:
|
||||
return "Drop"
|
||||
case Accept:
|
||||
return "Accept"
|
||||
case noVerdict:
|
||||
return "noVerdict"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
type RunFlags int
|
||||
|
||||
const (
|
||||
LogDrops RunFlags = 1 << iota
|
||||
LogAccepts
|
||||
HexdumpDrops
|
||||
HexdumpAccepts
|
||||
)
|
||||
|
||||
type tuple struct {
|
||||
SrcIP IP
|
||||
DstIP IP
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
const LRU_MAX = 512 // max entries in UDP LRU cache
|
||||
|
||||
var MatchAllowAll = Matches{
|
||||
Match{[]IPPortRange{IPPortRangeAny}, []IP{IPAny}},
|
||||
}
|
||||
|
||||
func NewAllowAll() *Filter {
|
||||
return New(MatchAllowAll)
|
||||
}
|
||||
|
||||
func NewAllowNone() *Filter {
|
||||
return New(nil)
|
||||
}
|
||||
|
||||
func New(matches Matches) *Filter {
|
||||
f := &Filter{
|
||||
matches: matches,
|
||||
udplru: lru.New(LRU_MAX),
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
if flag != 0 {
|
||||
return packet.Hexdump(b) + "\n"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// flood protection to hide their traffic. We could use a rate limiter in
|
||||
// the actual *filter* for SYN accepts, perhaps.
|
||||
var acceptBucket = ratelimit.Bucket{
|
||||
Burst: 3,
|
||||
FillInterval: 10 * time.Second,
|
||||
}
|
||||
var dropBucket = ratelimit.Bucket{
|
||||
Burst: 10,
|
||||
FillInterval: 5 * time.Second,
|
||||
}
|
||||
|
||||
func logRateLimit(runflags RunFlags, b []byte, q *packet.QDecode, r Response, why string) {
|
||||
if r == Drop && (runflags&LogDrops) != 0 && dropBucket.TryGet() > 0 {
|
||||
var qs string
|
||||
if q == nil {
|
||||
qs = fmt.Sprintf("(%d bytes)", len(b))
|
||||
} else {
|
||||
qs = q.String()
|
||||
}
|
||||
log.Printf("Drop: %v %v %s\n%s", qs, len(b), why, maybeHexdump(runflags&HexdumpDrops, b))
|
||||
} else if r == Accept && (runflags&LogAccepts) != 0 && acceptBucket.TryGet() > 0 {
|
||||
log.Printf("Accept: %v %v %s\n%s", q, len(b), why, maybeHexdump(runflags&HexdumpAccepts, b))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) RunIn(b []byte, q *packet.QDecode, rf RunFlags) Response {
|
||||
r := pre(b, q, rf)
|
||||
if r == Accept || r == Drop {
|
||||
// already logged
|
||||
return r
|
||||
}
|
||||
|
||||
r, why := f.runIn(q)
|
||||
logRateLimit(rf, b, q, r, why)
|
||||
return r
|
||||
}
|
||||
|
||||
func (f *Filter) RunOut(b []byte, q *packet.QDecode, rf RunFlags) Response {
|
||||
r := pre(b, q, rf)
|
||||
if r == Drop || r == Accept {
|
||||
// already logged
|
||||
return r
|
||||
}
|
||||
r, why := f.runOut(q)
|
||||
logRateLimit(rf, b, q, r, why)
|
||||
return r
|
||||
}
|
||||
|
||||
func (f *Filter) runIn(q *packet.QDecode) (r Response, why string) {
|
||||
switch q.IPProto {
|
||||
case packet.ICMP:
|
||||
// If any port is open to an IP, allow ICMP to it.
|
||||
if matchIPWithoutPorts(f.matches, q) {
|
||||
return Accept, "icmp ok"
|
||||
}
|
||||
case packet.TCP:
|
||||
// For TCP, we want to allow *outgoing* connections,
|
||||
// which means we want to allow return packets on those
|
||||
// connections. To make this restriction work, we need to
|
||||
// allow non-SYN packets (continuation of an existing session)
|
||||
// to arrive. This should be okay since a new incoming session
|
||||
// can't be initiated without first sending a SYN.
|
||||
// It happens to also be much faster.
|
||||
// TODO(apenwarr): Skip the rest of decoding in this path?
|
||||
if q.IPProto == packet.TCP && !q.IsTCPSyn() {
|
||||
return Accept, "tcp non-syn"
|
||||
}
|
||||
if matchIPPorts(f.matches, q) {
|
||||
return Accept, "tcp ok"
|
||||
}
|
||||
case packet.UDP:
|
||||
t := tuple{q.SrcIP, q.DstIP, q.SrcPort, q.DstPort}
|
||||
|
||||
f.udpMu.Lock()
|
||||
_, ok := f.udplru.Get(t)
|
||||
f.udpMu.Unlock()
|
||||
|
||||
if ok {
|
||||
return Accept, "udp cached"
|
||||
}
|
||||
if matchIPPorts(f.matches, q) {
|
||||
return Accept, "udp ok"
|
||||
}
|
||||
default:
|
||||
return Drop, "Unknown proto"
|
||||
}
|
||||
return Drop, "no rules matched"
|
||||
}
|
||||
|
||||
func (f *Filter) runOut(q *packet.QDecode) (r Response, why string) {
|
||||
if q.IPProto == packet.UDP {
|
||||
t := tuple{q.DstIP, q.SrcIP, q.DstPort, q.SrcPort}
|
||||
|
||||
f.udpMu.Lock()
|
||||
f.udplru.Add(t, t)
|
||||
f.udpMu.Unlock()
|
||||
}
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
func pre(b []byte, q *packet.QDecode, rf RunFlags) Response {
|
||||
if len(b) == 0 {
|
||||
// wireguard keepalive packet, always permit.
|
||||
return Accept
|
||||
}
|
||||
if len(b) < 20 {
|
||||
logRateLimit(rf, b, nil, Drop, "too short")
|
||||
return Drop
|
||||
}
|
||||
q.Decode(b)
|
||||
|
||||
if q.IPProto == packet.Junk {
|
||||
// Junk packets are dangerous; always drop them.
|
||||
logRateLimit(rf, b, q, Drop, "junk!")
|
||||
return Drop
|
||||
} else if q.IPProto == packet.Fragment {
|
||||
// Fragments after the first always need to be passed through.
|
||||
// Very small fragments are considered Junk by QDecode.
|
||||
logRateLimit(rf, b, q, Accept, "fragment")
|
||||
return Accept
|
||||
}
|
||||
|
||||
return noVerdict
|
||||
}
|
162
wgengine/filter/filter_test.go
Normal file
162
wgengine/filter/filter_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
|
||||
type QDecode = packet.QDecode
|
||||
|
||||
var Junk = packet.Junk
|
||||
var ICMP = packet.ICMP
|
||||
var TCP = packet.TCP
|
||||
var UDP = packet.UDP
|
||||
var Fragment = packet.Fragment
|
||||
|
||||
func ippr(ip IP, start, end uint16) []IPPortRange {
|
||||
return []IPPortRange{
|
||||
IPPortRange{ip, PortRange{start, end}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
mm := Matches{
|
||||
{SrcIPs: []IP{0x08010101, 0x08020202}, DstPorts: []IPPortRange{
|
||||
IPPortRange{0x01020304, PortRange{22, 22}},
|
||||
IPPortRange{0x05060708, PortRange{23, 24}},
|
||||
}},
|
||||
{SrcIPs: []IP{0x08010101, 0x08020202}, DstPorts: ippr(0x05060708, 27, 28)},
|
||||
{SrcIPs: []IP{0x02020202}, DstPorts: ippr(0x08010101, 22, 22)},
|
||||
{SrcIPs: []IP{0}, DstPorts: ippr(0x647a6232, 0, 65535)},
|
||||
{SrcIPs: []IP{0}, DstPorts: ippr(0, 443, 443)},
|
||||
{SrcIPs: []IP{0x99010101, 0x99010102, 0x99030303}, DstPorts: ippr(0x01020304, 999, 999)},
|
||||
}
|
||||
acl := New(mm)
|
||||
|
||||
for _, ent := range []Matches{Matches{mm[0]}, mm} {
|
||||
b, err := json.Marshal(ent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
mm2 := Matches{}
|
||||
if err := json.Unmarshal(b, &mm2); err != nil {
|
||||
t.Fatalf("unmarshal: %v (%v)", err, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// check packet filtering based on the table
|
||||
|
||||
type InOut struct {
|
||||
want Response
|
||||
p QDecode
|
||||
}
|
||||
tests := []InOut{
|
||||
// Basic
|
||||
{Accept, qdecode(TCP, 0x08010101, 0x01020304, 999, 22)},
|
||||
{Accept, qdecode(UDP, 0x08010101, 0x01020304, 999, 22)},
|
||||
{Accept, qdecode(ICMP, 0x08010101, 0x01020304, 0, 0)},
|
||||
{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, 0x647a6232, 0, 999)},
|
||||
{Accept, qdecode(TCP, 0x11223344, 0x647a6232, 0, 0)},
|
||||
|
||||
// Stateful UDP.
|
||||
// Initially empty cache
|
||||
{Drop, qdecode(UDP, 0x77777777, 0x66666666, 4242, 4343)},
|
||||
// Return packet from previous attempt is allowed
|
||||
{Accept, qdecode(UDP, 0x66666666, 0x77777777, 4343, 4242)},
|
||||
// Because of the return above, initial attempt is allowed now
|
||||
{Accept, qdecode(UDP, 0x77777777, 0x66666666, 4242, 4343)},
|
||||
}
|
||||
for i, test := range tests {
|
||||
if got, _ := acl.runIn(&test.p); test.want != got {
|
||||
t.Errorf("#%d got=%v want=%v packet:%v\n", i, got, test.want, test.p)
|
||||
}
|
||||
// Update UDP state
|
||||
_, _ = acl.runOut(&test.p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreFilter(t *testing.T) {
|
||||
packets := []struct {
|
||||
desc string
|
||||
want Response
|
||||
b []byte
|
||||
}{
|
||||
{"empty", Accept, []byte{}},
|
||||
{"short", Drop, []byte("short")},
|
||||
{"junk", Drop, rawpacket(Junk, 10)},
|
||||
{"fragment", Accept, rawpacket(Fragment, 40)},
|
||||
{"tcp", noVerdict, rawpacket(TCP, 200)},
|
||||
{"udp", noVerdict, rawpacket(UDP, 200)},
|
||||
{"icmp", noVerdict, rawpacket(ICMP, 200)},
|
||||
}
|
||||
for _, testPacket := range packets {
|
||||
got := pre([]byte(testPacket.b), &QDecode{}, LogDrops|LogAccepts)
|
||||
if got != testPacket.want {
|
||||
t.Errorf("%q got=%v want=%v packet:\n%s", testPacket.desc, got, testPacket.want, packet.Hexdump(testPacket.b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func qdecode(proto packet.IPProto, src, dst packet.IP, sport, dport uint16) QDecode {
|
||||
return QDecode{
|
||||
IPProto: proto,
|
||||
SrcIP: src,
|
||||
DstIP: dst,
|
||||
SrcPort: sport,
|
||||
DstPort: dport,
|
||||
TCPFlags: packet.TCPSyn,
|
||||
}
|
||||
}
|
||||
|
||||
func rawpacket(proto packet.IPProto, len uint16) []byte {
|
||||
bl := len
|
||||
if len < 24 {
|
||||
bl = 24
|
||||
}
|
||||
bin := binary.BigEndian
|
||||
hdr := make([]byte, bl)
|
||||
hdr[0] = 0x45
|
||||
bin.PutUint16(hdr[2:4], len)
|
||||
hdr[8] = 64
|
||||
ip := net.IPv4(8, 8, 8, 8).To4()
|
||||
copy(hdr[12:16], ip)
|
||||
copy(hdr[16:20], ip)
|
||||
// ports
|
||||
bin.PutUint16(hdr[20:22], 53)
|
||||
bin.PutUint16(hdr[22:24], 53)
|
||||
|
||||
switch proto {
|
||||
case ICMP:
|
||||
hdr[9] = 1
|
||||
case TCP:
|
||||
hdr[9] = 6
|
||||
case UDP:
|
||||
hdr[9] = 17
|
||||
case Fragment:
|
||||
hdr[9] = 6
|
||||
// flags + fragOff
|
||||
bin.PutUint16(hdr[6:8], (1<<13)|1234)
|
||||
case Junk:
|
||||
default:
|
||||
panic("unknown protocol")
|
||||
}
|
||||
|
||||
// Truncate the header if requested
|
||||
hdr = hdr[:len]
|
||||
|
||||
return hdr
|
||||
}
|
121
wgengine/filter/match.go
Normal file
121
wgengine/filter/match.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
|
||||
type IP = packet.IP
|
||||
|
||||
const IPAny = IP(0)
|
||||
|
||||
var NewIP = packet.NewIP
|
||||
|
||||
type PortRange struct {
|
||||
First, Last uint16
|
||||
}
|
||||
|
||||
var PortRangeAny = PortRange{0, 65535}
|
||||
|
||||
func (pr PortRange) String() string {
|
||||
if pr.First == 0 && pr.Last == 65535 {
|
||||
return "*"
|
||||
} else if pr.First == pr.Last {
|
||||
return fmt.Sprintf("%d", pr.First)
|
||||
} else {
|
||||
return fmt.Sprintf("%d-%d", pr.First, pr.Last)
|
||||
}
|
||||
}
|
||||
|
||||
type IPPortRange struct {
|
||||
IP IP
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
var IPPortRangeAny = IPPortRange{IPAny, PortRangeAny}
|
||||
|
||||
func (ipr IPPortRange) String() string {
|
||||
return fmt.Sprintf("%v:%v", ipr.IP, ipr.Ports)
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
DstPorts []IPPortRange
|
||||
SrcIPs []IP
|
||||
}
|
||||
|
||||
func (m Match) String() string {
|
||||
srcs := []string{}
|
||||
for _, srcip := range m.SrcIPs {
|
||||
srcs = append(srcs, srcip.String())
|
||||
}
|
||||
dsts := []string{}
|
||||
for _, dst := range m.DstPorts {
|
||||
dsts = append(dsts, dst.String())
|
||||
}
|
||||
|
||||
var ss, ds string
|
||||
if len(srcs) == 1 {
|
||||
ss = srcs[0]
|
||||
} else {
|
||||
ss = "[" + strings.Join(srcs, ",") + "]"
|
||||
}
|
||||
if len(dsts) == 1 {
|
||||
ds = dsts[0]
|
||||
} else {
|
||||
ds = "[" + strings.Join(dsts, ",") + "]"
|
||||
}
|
||||
return fmt.Sprintf("%v=>%v", ss, ds)
|
||||
}
|
||||
|
||||
type Matches []Match
|
||||
|
||||
func ipInList(ip IP, iplist []IP) bool {
|
||||
for _, ipp := range iplist {
|
||||
if ipp == IPAny || ipp == ip {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchIPPorts(mm Matches, q *packet.QDecode) bool {
|
||||
for _, acl := range mm {
|
||||
for _, dst := range acl.DstPorts {
|
||||
if dst.IP != IPAny && dst.IP != q.DstIP {
|
||||
continue
|
||||
}
|
||||
if q.DstPort < dst.Ports.First || q.DstPort > dst.Ports.Last {
|
||||
continue
|
||||
}
|
||||
if !ipInList(q.SrcIP, acl.SrcIPs) {
|
||||
// Skip other dests in this acl, since
|
||||
// the src will never match.
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchIPWithoutPorts(mm Matches, q *packet.QDecode) bool {
|
||||
for _, acl := range mm {
|
||||
for _, dst := range acl.DstPorts {
|
||||
if dst.IP != IPAny && dst.IP != q.DstIP {
|
||||
continue
|
||||
}
|
||||
if !ipInList(q.SrcIP, acl.SrcIPs) {
|
||||
// Skip other dests in this acl, since
|
||||
// the src will never match.
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user