|
|
|
@ -5,9 +5,7 @@
|
|
|
|
|
package filter
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
@ -18,189 +16,155 @@ import (
|
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var Unknown = packet.Unknown
|
|
|
|
|
var ICMPv4 = packet.ICMPv4
|
|
|
|
|
var TCP = packet.TCP
|
|
|
|
|
var UDP = packet.UDP
|
|
|
|
|
var Fragment = packet.Fragment
|
|
|
|
|
|
|
|
|
|
func mustIP4(s string) packet.IP4 {
|
|
|
|
|
ip, err := netaddr.ParseIP(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return packet.IP4FromNetaddr(ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pfx(s string) netaddr.IPPrefix {
|
|
|
|
|
pfx, err := netaddr.ParseIPPrefix(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return pfx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nets(nets ...string) (ret []netaddr.IPPrefix) {
|
|
|
|
|
for _, s := range nets {
|
|
|
|
|
if i := strings.IndexByte(s, '/'); i == -1 {
|
|
|
|
|
ip, err := netaddr.ParseIP(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
bits := uint8(32)
|
|
|
|
|
if ip.Is6() {
|
|
|
|
|
bits = 128
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, netaddr.IPPrefix{IP: ip, Bits: bits})
|
|
|
|
|
} else {
|
|
|
|
|
pfx, err := netaddr.ParseIPPrefix(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, pfx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ports(s string) PortRange {
|
|
|
|
|
if s == "*" {
|
|
|
|
|
return PortRange{First: 0, Last: 65535}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fs, ls string
|
|
|
|
|
i := strings.IndexByte(s, '-')
|
|
|
|
|
if i == -1 {
|
|
|
|
|
fs = s
|
|
|
|
|
ls = fs
|
|
|
|
|
} else {
|
|
|
|
|
fs = s[:i]
|
|
|
|
|
ls = s[i+1:]
|
|
|
|
|
}
|
|
|
|
|
first, err := strconv.ParseInt(fs, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
last, err := strconv.ParseInt(ls, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
return PortRange{uint16(first), uint16(last)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func netports(netPorts ...string) (ret []NetPortRange) {
|
|
|
|
|
for _, s := range netPorts {
|
|
|
|
|
i := strings.LastIndexByte(s, ':')
|
|
|
|
|
if i == -1 {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
npr := NetPortRange{
|
|
|
|
|
Net: nets(s[:i])[0],
|
|
|
|
|
Ports: ports(s[i+1:]),
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, npr)
|
|
|
|
|
func newFilter(logf logger.Logf) *Filter {
|
|
|
|
|
matches := []Match{
|
|
|
|
|
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("1.2.3.4:22", "5.6.7.8:23-24")},
|
|
|
|
|
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("5.6.7.8:27-28")},
|
|
|
|
|
{Srcs: nets("2.2.2.2"), Dsts: netports("8.1.1.1:22")},
|
|
|
|
|
{Srcs: nets("0.0.0.0/0"), Dsts: netports("100.122.98.50:*")},
|
|
|
|
|
{Srcs: nets("0.0.0.0/0"), Dsts: netports("0.0.0.0/0:443")},
|
|
|
|
|
{Srcs: nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), Dsts: netports("1.2.3.4:999")},
|
|
|
|
|
{Srcs: nets("::1", "::2"), Dsts: netports("2001::1:22")},
|
|
|
|
|
{Srcs: nets("::/0"), Dsts: netports("::/0:443")},
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var matches = []Match{
|
|
|
|
|
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("1.2.3.4:22", "5.6.7.8:23-24")},
|
|
|
|
|
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("5.6.7.8:27-28")},
|
|
|
|
|
{Srcs: nets("2.2.2.2"), Dsts: netports("8.1.1.1:22")},
|
|
|
|
|
{Srcs: nets("0.0.0.0/0"), Dsts: netports("100.122.98.50:*")},
|
|
|
|
|
{Srcs: nets("0.0.0.0/0"), Dsts: netports("0.0.0.0/0:443")},
|
|
|
|
|
{Srcs: nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), Dsts: netports("1.2.3.4:999")},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newFilter(logf logger.Logf) *Filter {
|
|
|
|
|
// Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8,
|
|
|
|
|
// 102.102.102.102, 119.119.119.119, 8.1.0.0/16
|
|
|
|
|
localNets := nets("100.122.98.50", "1.2.3.4", "5.6.7.8", "102.102.102.102", "119.119.119.119", "8.1.0.0/16")
|
|
|
|
|
localNets := nets("100.122.98.50", "1.2.3.4", "5.6.7.8", "102.102.102.102", "119.119.119.119", "8.1.0.0/16", "2001::/16")
|
|
|
|
|
|
|
|
|
|
return New(matches, localNets, nil, logf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMarshal(t *testing.T) {
|
|
|
|
|
for _, ent := range [][]Match{[]Match{matches[0]}, matches} {
|
|
|
|
|
b, err := json.Marshal(ent)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("marshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mm2 := []Match{}
|
|
|
|
|
if err := json.Unmarshal(b, &mm2); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v (%v)", err, string(b))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFilter(t *testing.T) {
|
|
|
|
|
acl := newFilter(t.Logf)
|
|
|
|
|
// check packet filtering based on the table
|
|
|
|
|
|
|
|
|
|
type InOut struct {
|
|
|
|
|
want Response
|
|
|
|
|
p packet.Parsed
|
|
|
|
|
}
|
|
|
|
|
tests := []InOut{
|
|
|
|
|
// Basic
|
|
|
|
|
{Accept, parsed(TCP, 0x08010101, 0x01020304, 999, 22)},
|
|
|
|
|
{Accept, parsed(UDP, 0x08010101, 0x01020304, 999, 22)},
|
|
|
|
|
{Accept, parsed(ICMPv4, 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)},
|
|
|
|
|
// allow 8.1.1.1 => 1.2.3.4:22
|
|
|
|
|
{Accept, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22)},
|
|
|
|
|
{Accept, parsed(packet.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 0)},
|
|
|
|
|
{Accept, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 22)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 21)},
|
|
|
|
|
// allow 8.2.2.2. => 1.2.3.4:22
|
|
|
|
|
{Accept, parsed(packet.TCP, "8.2.2.2", "1.2.3.4", 0, 22)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "8.2.2.2", "1.2.3.4", 0, 23)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "8.3.3.3", "1.2.3.4", 0, 22)},
|
|
|
|
|
// allow * => *:443
|
|
|
|
|
{Accept, parsed(packet.TCP, "17.34.51.68", "8.1.34.51", 0, 443)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "17.34.51.68", "8.1.34.51", 0, 444)},
|
|
|
|
|
// allow * => 100.122.98.50:*
|
|
|
|
|
{Accept, parsed(packet.TCP, "17.34.51.68", "100.122.98.50", 0, 999)},
|
|
|
|
|
{Accept, parsed(packet.TCP, "17.34.51.68", "100.122.98.50", 0, 0)},
|
|
|
|
|
|
|
|
|
|
// allow ::1, ::2 => [2001::1]:22
|
|
|
|
|
{Accept, parsed(packet.TCP, "::1", "2001::1", 0, 22)},
|
|
|
|
|
{Accept, parsed(packet.ICMPv6, "::1", "2001::1", 0, 0)},
|
|
|
|
|
{Accept, parsed(packet.TCP, "::2", "2001::1", 0, 22)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "::1", "2001::1", 0, 23)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "::1", "2001::2", 0, 22)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "::3", "2001::1", 0, 22)},
|
|
|
|
|
// allow * => *:443
|
|
|
|
|
{Accept, parsed(packet.TCP, "::1", "2001::1", 0, 443)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "::1", "2001::1", 0, 444)},
|
|
|
|
|
|
|
|
|
|
// localNets prefilter - accepted by policy filter, but
|
|
|
|
|
// unexpected dst IP.
|
|
|
|
|
{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, parsed(UDP, 0x77777777, 0x66666666, 4242, 4343)},
|
|
|
|
|
// Return packet from previous attempt is allowed
|
|
|
|
|
{Accept, parsed(UDP, 0x66666666, 0x77777777, 4343, 4242)},
|
|
|
|
|
// Because of the return above, initial attempt is allowed now
|
|
|
|
|
{Accept, parsed(UDP, 0x77777777, 0x66666666, 4242, 4343)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "8.1.1.1", "16.32.48.64", 0, 443)},
|
|
|
|
|
{Drop, parsed(packet.TCP, "1::", "2602::1", 0, 443)},
|
|
|
|
|
}
|
|
|
|
|
for i, test := range tests {
|
|
|
|
|
if got, _ := acl.runIn(&test.p); test.want != got {
|
|
|
|
|
t.Errorf("#%d runIn got=%v want=%v packet:%v", i, got, test.want, test.p)
|
|
|
|
|
aclFunc := acl.runIn4
|
|
|
|
|
if test.p.IPVersion == 6 {
|
|
|
|
|
aclFunc = acl.runIn6
|
|
|
|
|
}
|
|
|
|
|
if test.p.IPProto == TCP {
|
|
|
|
|
if got := acl.CheckTCP(test.p.SrcIP4.Netaddr(), test.p.DstIP4.Netaddr(), test.p.DstPort); test.want != got {
|
|
|
|
|
if got, why := aclFunc(&test.p); test.want != got {
|
|
|
|
|
t.Errorf("#%d runIn4 got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p)
|
|
|
|
|
}
|
|
|
|
|
if test.p.IPProto == packet.TCP {
|
|
|
|
|
var got Response
|
|
|
|
|
if test.p.IPVersion == 4 {
|
|
|
|
|
got = acl.CheckTCP(test.p.SrcIP4.Netaddr(), test.p.DstIP4.Netaddr(), test.p.DstPort)
|
|
|
|
|
} else {
|
|
|
|
|
got = acl.CheckTCP(test.p.SrcIP6.Netaddr(), test.p.DstIP6.Netaddr(), test.p.DstPort)
|
|
|
|
|
}
|
|
|
|
|
if test.want != got {
|
|
|
|
|
t.Errorf("#%d CheckTCP got=%v want=%v packet:%v", i, got, test.want, test.p)
|
|
|
|
|
}
|
|
|
|
|
// TCP and UDP are treated equivalently in the filter - verify that.
|
|
|
|
|
test.p.IPProto = packet.UDP
|
|
|
|
|
if got, why := aclFunc(&test.p); test.want != got {
|
|
|
|
|
t.Errorf("#%d runIn4 (UDP) got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Update UDP state
|
|
|
|
|
_, _ = acl.runOut(&test.p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUDPState(t *testing.T) {
|
|
|
|
|
acl := newFilter(t.Logf)
|
|
|
|
|
flags := LogDrops | LogAccepts
|
|
|
|
|
|
|
|
|
|
a4 := parsed(packet.UDP, "119.119.119.119", "102.102.102.102", 4242, 4343)
|
|
|
|
|
b4 := parsed(packet.UDP, "102.102.102.102", "119.119.119.119", 4343, 4242)
|
|
|
|
|
|
|
|
|
|
// Unsollicited UDP traffic gets dropped
|
|
|
|
|
if got := acl.RunIn(&a4, flags); got != Drop {
|
|
|
|
|
t.Fatalf("incoming initial packet not dropped, got=%v: %v", got, a4)
|
|
|
|
|
}
|
|
|
|
|
// We talk to that peer
|
|
|
|
|
if got := acl.RunOut(&b4, flags); got != Accept {
|
|
|
|
|
t.Fatalf("outbound packet didn't egress, got=%v: %v", got, b4)
|
|
|
|
|
}
|
|
|
|
|
// Now, the same packet as before is allowed back.
|
|
|
|
|
if got := acl.RunIn(&a4, flags); got != Accept {
|
|
|
|
|
t.Fatalf("incoming response packet not accepted, got=%v: %v", got, a4)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a6 := parsed(packet.UDP, "2001::2", "2001::1", 4242, 4343)
|
|
|
|
|
b6 := parsed(packet.UDP, "2001::1", "2001::2", 4343, 4242)
|
|
|
|
|
|
|
|
|
|
// Unsollicited UDP traffic gets dropped
|
|
|
|
|
if got := acl.RunIn(&a6, flags); got != Drop {
|
|
|
|
|
t.Fatalf("incoming initial packet not dropped: %v", a4)
|
|
|
|
|
}
|
|
|
|
|
// We talk to that peer
|
|
|
|
|
if got := acl.RunOut(&b6, flags); got != Accept {
|
|
|
|
|
t.Fatalf("outbound packet didn't egress: %v", b4)
|
|
|
|
|
}
|
|
|
|
|
// Now, the same packet as before is allowed back.
|
|
|
|
|
if got := acl.RunIn(&a6, flags); got != Accept {
|
|
|
|
|
t.Fatalf("incoming response packet not accepted: %v", a4)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNoAllocs(t *testing.T) {
|
|
|
|
|
acl := newFilter(t.Logf)
|
|
|
|
|
|
|
|
|
|
tcpPacket := rawpacket(TCP, 0x08010101, 0x01020304, 999, 22, 0)
|
|
|
|
|
udpPacket := rawpacket(UDP, 0x08010101, 0x01020304, 999, 22, 0)
|
|
|
|
|
tcp4Packet := raw4(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0)
|
|
|
|
|
udp4Packet := raw4(packet.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0)
|
|
|
|
|
tcp6Packet := raw6(packet.TCP, "2001::1", "2001::2", 999, 22, 0)
|
|
|
|
|
udp6Packet := raw6(packet.UDP, "2001::1", "2001::2", 999, 22, 0)
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
in bool
|
|
|
|
|
dir direction
|
|
|
|
|
want int
|
|
|
|
|
packet []byte
|
|
|
|
|
}{
|
|
|
|
|
{"tcp_in", true, 0, tcpPacket},
|
|
|
|
|
{"tcp_out", false, 0, tcpPacket},
|
|
|
|
|
{"udp_in", true, 0, udpPacket},
|
|
|
|
|
{"tcp4_in", in, 0, tcp4Packet},
|
|
|
|
|
{"tcp6_in", in, 0, tcp6Packet},
|
|
|
|
|
{"tcp4_out", out, 0, tcp4Packet},
|
|
|
|
|
{"tcp6_out", out, 0, tcp6Packet},
|
|
|
|
|
{"udp4_in", in, 0, udp4Packet},
|
|
|
|
|
{"udp6_in", in, 0, udp6Packet},
|
|
|
|
|
// One alloc is inevitable (an lru cache update)
|
|
|
|
|
{"udp_out", false, 1, udpPacket},
|
|
|
|
|
{"udp4_out", out, 1, udp4Packet},
|
|
|
|
|
{"udp6_out", out, 1, udp6Packet},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
@ -208,9 +172,10 @@ func TestNoAllocs(t *testing.T) {
|
|
|
|
|
got := int(testing.AllocsPerRun(1000, func() {
|
|
|
|
|
q := &packet.Parsed{}
|
|
|
|
|
q.Decode(test.packet)
|
|
|
|
|
if test.in {
|
|
|
|
|
switch test.dir {
|
|
|
|
|
case in:
|
|
|
|
|
acl.RunIn(q, 0)
|
|
|
|
|
} else {
|
|
|
|
|
case out:
|
|
|
|
|
acl.RunOut(q, 0)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
@ -231,11 +196,13 @@ func TestParseIP(t *testing.T) {
|
|
|
|
|
wantErr string
|
|
|
|
|
}{
|
|
|
|
|
{"8.8.8.8", 24, pfx("8.8.8.8/24"), ""},
|
|
|
|
|
{"2601:1234::", 64, pfx("2601:1234::/64"), ""},
|
|
|
|
|
{"8.8.8.8", 33, noaddr, `invalid CIDR size 33 for host "8.8.8.8"`},
|
|
|
|
|
{"8.8.8.8", -1, noaddr, `invalid CIDR size -1 for host "8.8.8.8"`},
|
|
|
|
|
{"2601:1234::", 129, noaddr, `invalid CIDR size 129 for host "2601:1234::"`},
|
|
|
|
|
{"0.0.0.0", 24, noaddr, `ports="0.0.0.0": to allow all IP addresses, use *:port, not 0.0.0.0:port`},
|
|
|
|
|
{"::", 64, noaddr, `ports="::": to allow all IP addresses, use *:port, not [::]:port`},
|
|
|
|
|
{"*", 24, pfx("0.0.0.0/0"), ""},
|
|
|
|
|
{"fe80::1", 128, pfx("255.255.255.255/32"), `ports="fe80::1": invalid IPv4 address`},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got, err := parseIP(tt.host, tt.bits)
|
|
|
|
@ -253,38 +220,42 @@ func TestParseIP(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkFilter(b *testing.B) {
|
|
|
|
|
acl := newFilter(b.Logf)
|
|
|
|
|
tcp4Packet := raw4(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0)
|
|
|
|
|
udp4Packet := raw4(packet.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0)
|
|
|
|
|
icmp4Packet := raw4(packet.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
tcpPacket := rawpacket(TCP, 0x08010101, 0x01020304, 999, 22, 0)
|
|
|
|
|
udpPacket := rawpacket(UDP, 0x08010101, 0x01020304, 999, 22, 0)
|
|
|
|
|
icmpPacket := rawpacket(ICMPv4, 0x08010101, 0x01020304, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
tcpSynPacket := rawpacket(TCP, 0x08010101, 0x01020304, 999, 22, 0)
|
|
|
|
|
// TCP filtering is trivial (Accept) for non-SYN packets.
|
|
|
|
|
tcpSynPacket[33] = packet.TCPSyn
|
|
|
|
|
tcp6Packet := raw6(packet.TCP, "::1", "2001::1", 999, 22, 0)
|
|
|
|
|
udp6Packet := raw6(packet.UDP, "::1", "2001::1", 999, 22, 0)
|
|
|
|
|
icmp6Packet := raw6(packet.ICMPv6, "::1", "2001::1", 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
benches := []struct {
|
|
|
|
|
name string
|
|
|
|
|
in bool
|
|
|
|
|
dir direction
|
|
|
|
|
packet []byte
|
|
|
|
|
}{
|
|
|
|
|
// Non-SYN TCP and ICMP have similar code paths in and out.
|
|
|
|
|
{"icmp", true, icmpPacket},
|
|
|
|
|
{"tcp", true, tcpPacket},
|
|
|
|
|
{"tcp_syn_in", true, tcpSynPacket},
|
|
|
|
|
{"tcp_syn_out", false, tcpSynPacket},
|
|
|
|
|
{"udp_in", true, udpPacket},
|
|
|
|
|
{"udp_out", false, udpPacket},
|
|
|
|
|
{"icmp4", in, icmp4Packet},
|
|
|
|
|
{"tcp4_syn_in", in, tcp4Packet},
|
|
|
|
|
{"tcp4_syn_out", out, tcp4Packet},
|
|
|
|
|
{"udp4_in", in, udp4Packet},
|
|
|
|
|
{"udp4_out", out, udp4Packet},
|
|
|
|
|
{"icmp6", in, icmp6Packet},
|
|
|
|
|
{"tcp6_syn_in", in, tcp6Packet},
|
|
|
|
|
{"tcp6_syn_out", out, tcp6Packet},
|
|
|
|
|
{"udp6_in", in, udp6Packet},
|
|
|
|
|
{"udp6_out", out, udp6Packet},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, bench := range benches {
|
|
|
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
|
|
|
acl := newFilter(b.Logf)
|
|
|
|
|
b.ReportAllocs()
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
q := &packet.Parsed{}
|
|
|
|
|
q.Decode(bench.packet)
|
|
|
|
|
// This branch seems to have no measurable impact on performance.
|
|
|
|
|
if bench.in {
|
|
|
|
|
if bench.dir == in {
|
|
|
|
|
acl.RunIn(q, 0)
|
|
|
|
|
} else {
|
|
|
|
|
acl.RunOut(q, 0)
|
|
|
|
@ -302,11 +273,11 @@ func TestPreFilter(t *testing.T) {
|
|
|
|
|
}{
|
|
|
|
|
{"empty", Accept, []byte{}},
|
|
|
|
|
{"short", Drop, []byte("short")},
|
|
|
|
|
{"junk", Drop, rawdefault(Unknown, 10)},
|
|
|
|
|
{"fragment", Accept, rawdefault(Fragment, 40)},
|
|
|
|
|
{"tcp", noVerdict, rawdefault(TCP, 200)},
|
|
|
|
|
{"udp", noVerdict, rawdefault(UDP, 200)},
|
|
|
|
|
{"icmp", noVerdict, rawdefault(ICMPv4, 200)},
|
|
|
|
|
{"junk", Drop, raw4default(packet.Unknown, 10)},
|
|
|
|
|
{"fragment", Accept, raw4default(packet.Fragment, 40)},
|
|
|
|
|
{"tcp", noVerdict, raw4default(packet.TCP, 0)},
|
|
|
|
|
{"udp", noVerdict, raw4default(packet.UDP, 0)},
|
|
|
|
|
{"icmp", noVerdict, raw4default(packet.ICMPv4, 0)},
|
|
|
|
|
}
|
|
|
|
|
f := NewAllowNone(t.Logf)
|
|
|
|
|
for _, testPacket := range packets {
|
|
|
|
@ -319,90 +290,6 @@ func TestPreFilter(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parsed(proto packet.IPProto, src, dst packet.IP4, sport, dport uint16) packet.Parsed {
|
|
|
|
|
return packet.Parsed{
|
|
|
|
|
IPProto: proto,
|
|
|
|
|
SrcIP4: src,
|
|
|
|
|
DstIP4: dst,
|
|
|
|
|
SrcPort: sport,
|
|
|
|
|
DstPort: dport,
|
|
|
|
|
TCPFlags: packet.TCPSyn,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rawpacket generates a packet with given source and destination ports and IPs
|
|
|
|
|
// and resizes the header to trimLength if it is nonzero.
|
|
|
|
|
func rawpacket(proto packet.IPProto, src, dst packet.IP4, sport, dport uint16, trimLength int) []byte {
|
|
|
|
|
var headerLength int
|
|
|
|
|
|
|
|
|
|
switch proto {
|
|
|
|
|
case ICMPv4:
|
|
|
|
|
headerLength = 24
|
|
|
|
|
case TCP:
|
|
|
|
|
headerLength = 40
|
|
|
|
|
case UDP:
|
|
|
|
|
headerLength = 28
|
|
|
|
|
default:
|
|
|
|
|
headerLength = 24
|
|
|
|
|
}
|
|
|
|
|
if trimLength > headerLength {
|
|
|
|
|
headerLength = trimLength
|
|
|
|
|
}
|
|
|
|
|
if trimLength == 0 {
|
|
|
|
|
trimLength = headerLength
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bin := binary.BigEndian
|
|
|
|
|
hdr := make([]byte, headerLength)
|
|
|
|
|
hdr[0] = 0x45
|
|
|
|
|
bin.PutUint16(hdr[2:4], uint16(trimLength))
|
|
|
|
|
hdr[8] = 64
|
|
|
|
|
bin.PutUint32(hdr[12:16], uint32(src))
|
|
|
|
|
bin.PutUint32(hdr[16:20], uint32(dst))
|
|
|
|
|
// ports
|
|
|
|
|
bin.PutUint16(hdr[20:22], sport)
|
|
|
|
|
bin.PutUint16(hdr[22:24], dport)
|
|
|
|
|
|
|
|
|
|
switch proto {
|
|
|
|
|
case ICMPv4:
|
|
|
|
|
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 Unknown:
|
|
|
|
|
default:
|
|
|
|
|
panic("unknown protocol")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trim the header if requested
|
|
|
|
|
hdr = hdr[:trimLength]
|
|
|
|
|
|
|
|
|
|
return hdr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rawdefault calls rawpacket with default ports and IPs.
|
|
|
|
|
func rawdefault(proto packet.IPProto, trimLength int) []byte {
|
|
|
|
|
ip := packet.IP4(0x08080808) // 8.8.8.8
|
|
|
|
|
port := uint16(53)
|
|
|
|
|
return rawpacket(proto, ip, ip, port, port, trimLength)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseHexPkt(t *testing.T, h string) *packet.Parsed {
|
|
|
|
|
t.Helper()
|
|
|
|
|
b, err := hex.DecodeString(strings.ReplaceAll(h, " ", ""))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to read hex %q: %v", h, err)
|
|
|
|
|
}
|
|
|
|
|
p := new(packet.Parsed)
|
|
|
|
|
p.Decode(b)
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOmitDropLogging(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
@ -469,3 +356,198 @@ func TestOmitDropLogging(t *testing.T) {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mustIP(s string) netaddr.IP {
|
|
|
|
|
ip, err := netaddr.ParseIP(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return ip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parsed(proto packet.IPProto, src, dst string, sport, dport uint16) packet.Parsed {
|
|
|
|
|
sip, dip := mustIP(src), mustIP(dst)
|
|
|
|
|
|
|
|
|
|
var ret packet.Parsed
|
|
|
|
|
ret.Decode(dummyPacket)
|
|
|
|
|
ret.IPProto = proto
|
|
|
|
|
ret.SrcPort = sport
|
|
|
|
|
ret.DstPort = dport
|
|
|
|
|
ret.TCPFlags = packet.TCPSyn
|
|
|
|
|
|
|
|
|
|
if sip.Is4() {
|
|
|
|
|
ret.IPVersion = 4
|
|
|
|
|
ret.SrcIP4 = packet.IP4FromNetaddr(sip)
|
|
|
|
|
ret.DstIP4 = packet.IP4FromNetaddr(dip)
|
|
|
|
|
} else {
|
|
|
|
|
ret.IPVersion = 6
|
|
|
|
|
ret.SrcIP6 = packet.IP6FromNetaddr(sip)
|
|
|
|
|
ret.DstIP6 = packet.IP6FromNetaddr(dip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raw6(proto packet.IPProto, src, dst string, sport, dport uint16, trimLen int) []byte {
|
|
|
|
|
u := packet.UDP6Header{
|
|
|
|
|
IP6Header: packet.IP6Header{
|
|
|
|
|
SrcIP: packet.IP6FromNetaddr(mustIP(src)),
|
|
|
|
|
DstIP: packet.IP6FromNetaddr(mustIP(dst)),
|
|
|
|
|
},
|
|
|
|
|
SrcPort: sport,
|
|
|
|
|
DstPort: dport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload := make([]byte, 12)
|
|
|
|
|
// Set the right bit to look like a TCP SYN, if the packet ends up interpreted as TCP
|
|
|
|
|
payload[5] = packet.TCPSyn
|
|
|
|
|
|
|
|
|
|
b := packet.Generate(&u, payload) // payload large enough to possibly be TCP
|
|
|
|
|
|
|
|
|
|
// UDP marshaling clobbers IPProto, so override it here.
|
|
|
|
|
u.IP6Header.IPProto = proto
|
|
|
|
|
if err := u.IP6Header.Marshal(b); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trimLen > 0 {
|
|
|
|
|
return b[:trimLen]
|
|
|
|
|
} else {
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength int) []byte {
|
|
|
|
|
u := packet.UDP4Header{
|
|
|
|
|
IP4Header: packet.IP4Header{
|
|
|
|
|
SrcIP: packet.IP4FromNetaddr(mustIP(src)),
|
|
|
|
|
DstIP: packet.IP4FromNetaddr(mustIP(dst)),
|
|
|
|
|
},
|
|
|
|
|
SrcPort: sport,
|
|
|
|
|
DstPort: dport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload := make([]byte, 12)
|
|
|
|
|
// Set the right bit to look like a TCP SYN, if the packet ends up interpreted as TCP
|
|
|
|
|
payload[5] = packet.TCPSyn
|
|
|
|
|
|
|
|
|
|
b := packet.Generate(&u, payload) // payload large enough to possibly be TCP
|
|
|
|
|
|
|
|
|
|
// UDP marshaling clobbers IPProto, so override it here.
|
|
|
|
|
switch proto {
|
|
|
|
|
case packet.Unknown, packet.Fragment:
|
|
|
|
|
default:
|
|
|
|
|
u.IP4Header.IPProto = proto
|
|
|
|
|
}
|
|
|
|
|
if err := u.IP4Header.Marshal(b); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if proto == packet.Fragment {
|
|
|
|
|
// Set some fragment offset. This makes the IP
|
|
|
|
|
// checksum wrong, but we don't validate the checksum
|
|
|
|
|
// when parsing.
|
|
|
|
|
b[7] = 255
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trimLength > 0 {
|
|
|
|
|
return b[:trimLength]
|
|
|
|
|
} else {
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raw4default(proto packet.IPProto, trimLength int) []byte {
|
|
|
|
|
return raw4(proto, "8.8.8.8", "8.8.8.8", 53, 53, trimLength)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseHexPkt(t *testing.T, h string) *packet.Parsed {
|
|
|
|
|
t.Helper()
|
|
|
|
|
b, err := hex.DecodeString(strings.ReplaceAll(h, " ", ""))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to read hex %q: %v", h, err)
|
|
|
|
|
}
|
|
|
|
|
p := new(packet.Parsed)
|
|
|
|
|
p.Decode(b)
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mustIP4(s string) packet.IP4 {
|
|
|
|
|
ip, err := netaddr.ParseIP(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return packet.IP4FromNetaddr(ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pfx(s string) netaddr.IPPrefix {
|
|
|
|
|
pfx, err := netaddr.ParseIPPrefix(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return pfx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nets(nets ...string) (ret []netaddr.IPPrefix) {
|
|
|
|
|
for _, s := range nets {
|
|
|
|
|
if i := strings.IndexByte(s, '/'); i == -1 {
|
|
|
|
|
ip, err := netaddr.ParseIP(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
bits := uint8(32)
|
|
|
|
|
if ip.Is6() {
|
|
|
|
|
bits = 128
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, netaddr.IPPrefix{IP: ip, Bits: bits})
|
|
|
|
|
} else {
|
|
|
|
|
pfx, err := netaddr.ParseIPPrefix(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, pfx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ports(s string) PortRange {
|
|
|
|
|
if s == "*" {
|
|
|
|
|
return PortRange{First: 0, Last: 65535}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fs, ls string
|
|
|
|
|
i := strings.IndexByte(s, '-')
|
|
|
|
|
if i == -1 {
|
|
|
|
|
fs = s
|
|
|
|
|
ls = fs
|
|
|
|
|
} else {
|
|
|
|
|
fs = s[:i]
|
|
|
|
|
ls = s[i+1:]
|
|
|
|
|
}
|
|
|
|
|
first, err := strconv.ParseInt(fs, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
last, err := strconv.ParseInt(ls, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
return PortRange{uint16(first), uint16(last)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func netports(netPorts ...string) (ret []NetPortRange) {
|
|
|
|
|
for _, s := range netPorts {
|
|
|
|
|
i := strings.LastIndexByte(s, ':')
|
|
|
|
|
if i == -1 {
|
|
|
|
|
panic(fmt.Sprintf("invalid NetPortRange %q", s))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
npr := NetPortRange{
|
|
|
|
|
Net: nets(s[:i])[0],
|
|
|
|
|
Ports: ports(s[i+1:]),
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, npr)
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|