wgengine/packet: refactor and expose UDP header marshaling (#408)

Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
Dmytro Shynkevych
2020-06-04 18:42:44 -04:00
committed by GitHub
parent 5e0ff494a5
commit 059b1d10bb
11 changed files with 793 additions and 216 deletions

View File

@@ -137,7 +137,7 @@ func maybeHexdump(flag RunFlags, b []byte) string {
var acceptBucket = rate.NewLimiter(rate.Every(10*time.Second), 3)
var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
func (f *Filter) logRateLimit(runflags RunFlags, b []byte, q *packet.QDecode, r Response, why string) {
func (f *Filter) logRateLimit(runflags RunFlags, b []byte, q *packet.ParsedPacket, r Response, why string) {
var verdict string
if r == Drop && (runflags&LogDrops) != 0 && dropBucket.Allow() {
@@ -161,7 +161,7 @@ func (f *Filter) logRateLimit(runflags RunFlags, b []byte, q *packet.QDecode, r
}
}
func (f *Filter) RunIn(b []byte, q *packet.QDecode, rf RunFlags) Response {
func (f *Filter) RunIn(b []byte, q *packet.ParsedPacket, rf RunFlags) Response {
r := f.pre(b, q, rf)
if r == Accept || r == Drop {
// already logged
@@ -173,7 +173,7 @@ func (f *Filter) RunIn(b []byte, q *packet.QDecode, rf RunFlags) Response {
return r
}
func (f *Filter) RunOut(b []byte, q *packet.QDecode, rf RunFlags) Response {
func (f *Filter) RunOut(b []byte, q *packet.ParsedPacket, rf RunFlags) Response {
r := f.pre(b, q, rf)
if r == Drop || r == Accept {
// already logged
@@ -184,7 +184,7 @@ func (f *Filter) RunOut(b []byte, q *packet.QDecode, rf RunFlags) Response {
return r
}
func (f *Filter) runIn(q *packet.QDecode) (r Response, why string) {
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
// prevent that.
@@ -239,7 +239,7 @@ func (f *Filter) runIn(q *packet.QDecode) (r Response, why string) {
return Drop, "no rules matched"
}
func (f *Filter) runOut(q *packet.QDecode) (r Response, why string) {
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}
var ti interface{} = t // allocate once, rather than twice inside mutex
@@ -251,7 +251,7 @@ func (f *Filter) runOut(q *packet.QDecode) (r Response, why string) {
return Accept, "ok out"
}
func (f *Filter) pre(b []byte, q *packet.QDecode, rf RunFlags) Response {
func (f *Filter) pre(b []byte, q *packet.ParsedPacket, rf RunFlags) Response {
if len(b) == 0 {
// wireguard keepalive packet, always permit.
return Accept
@@ -262,13 +262,17 @@ func (f *Filter) pre(b []byte, q *packet.QDecode, rf RunFlags) Response {
}
q.Decode(b)
if q.IPProto == packet.Junk {
// Junk packets are dangerous; always drop them.
f.logRateLimit(rf, b, q, Drop, "junk")
switch q.IPProto {
case packet.Unknown:
// Unknown packets are dangerous; always drop them.
f.logRateLimit(rf, b, q, Drop, "unknown")
return Drop
} else if q.IPProto == packet.Fragment {
case packet.IPv6:
f.logRateLimit(rf, b, q, Drop, "ipv6")
return Drop
case packet.Fragment:
// Fragments after the first always need to be passed through.
// Very small fragments are considered Junk by QDecode.
// Very small fragments are considered Junk by ParsedPacket.
f.logRateLimit(rf, b, q, Accept, "fragment")
return Accept
}

View File

@@ -14,10 +14,10 @@ import (
)
// Type aliases only in test code: (but ideally nowhere)
type QDecode = packet.QDecode
type ParsedPacket = packet.ParsedPacket
type IP = packet.IP
var Junk = packet.Junk
var Unknown = packet.Unknown
var ICMP = packet.ICMP
var TCP = packet.TCP
var UDP = packet.UDP
@@ -84,34 +84,34 @@ func TestFilter(t *testing.T) {
type InOut struct {
want Response
p QDecode
p ParsedPacket
}
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, 0x08012233, 0, 443)},
{Drop, qdecode(TCP, 0x11223344, 0x08012233, 0, 444)},
{Accept, qdecode(TCP, 0x11223344, 0x647a6232, 0, 999)},
{Accept, qdecode(TCP, 0x11223344, 0x647a6232, 0, 0)},
{Accept, parsed(TCP, 0x08010101, 0x01020304, 999, 22)},
{Accept, parsed(UDP, 0x08010101, 0x01020304, 999, 22)},
{Accept, parsed(ICMP, 0x08010101, 0x01020304, 0, 0)},
{Drop, parsed(TCP, 0x08010101, 0x01020304, 0, 0)},
{Accept, parsed(TCP, 0x08010101, 0x01020304, 0, 22)},
{Drop, parsed(TCP, 0x08010101, 0x01020304, 0, 21)},
{Accept, parsed(TCP, 0x11223344, 0x08012233, 0, 443)},
{Drop, parsed(TCP, 0x11223344, 0x08012233, 0, 444)},
{Accept, parsed(TCP, 0x11223344, 0x647a6232, 0, 999)},
{Accept, parsed(TCP, 0x11223344, 0x647a6232, 0, 0)},
// localNets prefilter - accepted by policy filter, but
// unexpected dst IP.
{Drop, qdecode(TCP, 0x08010101, 0x10203040, 0, 443)},
{Drop, parsed(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)},
{Drop, parsed(UDP, 0x77777777, 0x66666666, 4242, 4343)},
// Return packet from previous attempt is allowed
{Accept, qdecode(UDP, 0x66666666, 0x77777777, 4343, 4242)},
{Accept, parsed(UDP, 0x66666666, 0x77777777, 4343, 4242)},
// Because of the return above, initial attempt is allowed now
{Accept, qdecode(UDP, 0x77777777, 0x66666666, 4242, 4343)},
{Accept, parsed(UDP, 0x77777777, 0x66666666, 4242, 4343)},
}
for i, test := range tests {
if got, _ := acl.runIn(&test.p); test.want != got {
@@ -144,7 +144,7 @@ func TestNoAllocs(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := int(testing.AllocsPerRun(1000, func() {
var q QDecode
var q ParsedPacket
if test.in {
acl.RunIn(test.packet, &q, 0)
} else {
@@ -187,7 +187,7 @@ func BenchmarkFilter(b *testing.B) {
for _, bench := range benches {
b.Run(bench.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
var q QDecode
var q ParsedPacket
// This branch seems to have no measurable impact on performance.
if bench.in {
acl.RunIn(bench.packet, &q, 0)
@@ -207,7 +207,7 @@ func TestPreFilter(t *testing.T) {
}{
{"empty", Accept, []byte{}},
{"short", Drop, []byte("short")},
{"junk", Drop, rawdefault(Junk, 10)},
{"junk", Drop, rawdefault(Unknown, 10)},
{"fragment", Accept, rawdefault(Fragment, 40)},
{"tcp", noVerdict, rawdefault(TCP, 200)},
{"udp", noVerdict, rawdefault(UDP, 200)},
@@ -215,15 +215,15 @@ func TestPreFilter(t *testing.T) {
}
f := NewAllowNone(t.Logf)
for _, testPacket := range packets {
got := f.pre([]byte(testPacket.b), &QDecode{}, LogDrops|LogAccepts)
got := f.pre([]byte(testPacket.b), &ParsedPacket{}, 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{
func parsed(proto packet.IPProto, src, dst packet.IP, sport, dport uint16) ParsedPacket {
return ParsedPacket{
IPProto: proto,
SrcIP: src,
DstIP: dst,
@@ -277,7 +277,7 @@ func rawpacket(proto packet.IPProto, src, dst packet.IP, sport, dport uint16, tr
hdr[9] = 6
// flags + fragOff
bin.PutUint16(hdr[6:8], (1<<13)|1234)
case Junk:
case Unknown:
default:
panic("unknown protocol")
}

View File

@@ -133,7 +133,7 @@ func ipInList(ip packet.IP, netlist []Net) bool {
return false
}
func matchIPPorts(mm Matches, q *packet.QDecode) bool {
func matchIPPorts(mm Matches, q *packet.ParsedPacket) bool {
for _, acl := range mm {
for _, dst := range acl.Dsts {
if !dst.Net.Includes(q.DstIP) {
@@ -153,7 +153,7 @@ func matchIPPorts(mm Matches, q *packet.QDecode) bool {
return false
}
func matchIPWithoutPorts(mm Matches, q *packet.QDecode) bool {
func matchIPWithoutPorts(mm Matches, q *packet.ParsedPacket) bool {
for _, acl := range mm {
for _, dst := range acl.Dsts {
if !dst.Net.Includes(q.DstIP) {