Move Linux client & common packages into a public repo.

This commit is contained in:
Earl Lee
2020-02-05 14:16:58 -08:00
parent c955043dfe
commit a8d8b8719a
156 changed files with 17113 additions and 0 deletions

218
wgengine/filter/filter.go Normal file
View 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
}

View 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
View 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
}