github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4dns/matcher_test.go (about) 1 // Copyright 2024 VNXME 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package l4dns 16 17 import ( 18 "context" 19 "errors" 20 "io" 21 "net" 22 "testing" 23 24 "github.com/caddyserver/caddy/v2" 25 "go.uber.org/zap" 26 27 "github.com/mholt/caddy-l4/layer4" 28 ) 29 30 func assertNoError(t *testing.T, err error) { 31 t.Helper() 32 if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { 33 t.Fatalf("Unexpected error: %s\n", err) 34 } 35 } 36 37 func Test_MatchDNS_Match(t *testing.T) { 38 type test struct { 39 matcher *MatchDNS 40 data []byte 41 shouldMatch bool 42 shouldFakeTCP bool 43 } 44 45 tests := []test{ 46 {matcher: &MatchDNS{}, data: []byte{}, shouldMatch: false}, 47 48 {matcher: &MatchDNS{}, data: udpPacketAppleComA[:12], shouldMatch: false}, 49 {matcher: &MatchDNS{}, data: udpPacketGoogleComA[:14], shouldMatch: false}, 50 {matcher: &MatchDNS{}, data: tcpPacketAppleComA[:14], shouldMatch: false, shouldFakeTCP: true}, 51 {matcher: &MatchDNS{}, data: tcpPacketGoogleComA[:16], shouldMatch: false, shouldFakeTCP: true}, 52 53 {matcher: &MatchDNS{}, data: udpPacketAppleComA, shouldMatch: true}, 54 {matcher: &MatchDNS{}, data: udpPacketGoogleComA, shouldMatch: true}, 55 {matcher: &MatchDNS{}, data: tcpPacketAppleComA, shouldMatch: true, shouldFakeTCP: true}, 56 {matcher: &MatchDNS{}, data: tcpPacketGoogleComA, shouldMatch: true, shouldFakeTCP: true}, 57 58 {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}}, 59 data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, 60 {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A", Class: "IN"}}}, 61 data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, 62 {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{TypeRegexp: "^(MX|NS)$"}}}, 63 data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, 64 {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{NameRegexp: "^(|[-0-9a-z]+\\.)example\\.com\\.$"}}}, 65 data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, 66 67 {matcher: &MatchDNS{Deny: MatchDNSRules{&MatchDNSRule{Name: ".", Class: "IN"}}}, 68 data: tcpPacketDotNS, shouldMatch: false, shouldFakeTCP: true}, 69 {matcher: &MatchDNS{Deny: MatchDNSRules{&MatchDNSRule{Type: "A"}}}, 70 data: tcpPacketDotNS, shouldMatch: true, shouldFakeTCP: true}, 71 72 {matcher: &MatchDNS{ 73 Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A"}}, 74 Deny: MatchDNSRules{&MatchDNSRule{Class: "IN"}}, 75 }, data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, 76 {matcher: &MatchDNS{ 77 Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}, 78 Deny: MatchDNSRules{&MatchDNSRule{Type: "MX"}}, 79 }, data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, 80 {matcher: &MatchDNS{ 81 Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}, 82 Deny: MatchDNSRules{&MatchDNSRule{Type: "MX"}}, 83 DefaultDeny: true, 84 }, data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, 85 {matcher: &MatchDNS{ 86 Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A"}}, 87 Deny: MatchDNSRules{&MatchDNSRule{Class: "IN"}}, 88 PreferAllow: true, 89 }, data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, 90 } 91 92 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) 93 defer cancel() 94 95 for i, tc := range tests { 96 func() { 97 err := tc.matcher.Provision(ctx) 98 assertNoError(t, err) 99 100 in, out := net.Pipe() 101 defer func() { 102 _, _ = io.Copy(io.Discard, out) 103 _ = out.Close() 104 }() 105 106 if tc.shouldFakeTCP { 107 out = &fakeTCPConn{Conn: out} 108 } 109 110 cx := layer4.WrapConnection(out, []byte{}, zap.NewNop()) 111 go func() { 112 _, err := in.Write(tc.data) 113 assertNoError(t, err) 114 _ = in.Close() 115 }() 116 117 matched, err := tc.matcher.Match(cx) 118 assertNoError(t, err) 119 120 if matched != tc.shouldMatch { 121 if tc.shouldMatch { 122 t.Fatalf("test %d: matcher did not match | %+v\n", i, tc.matcher) 123 } else { 124 t.Fatalf("test %d: matcher should not match | %+v\n", i, tc.matcher) 125 } 126 } 127 }() 128 } 129 } 130 131 type fakeTCPConn struct { 132 net.Conn 133 } 134 135 func (c *fakeTCPConn) LocalAddr() net.Addr { 136 return &net.TCPAddr{} 137 } 138 139 // Interface guard 140 var _ net.Conn = (*fakeTCPConn)(nil) 141 142 // Packet examples 143 var tcpPacketAppleComA = []byte{ 144 0, 27, 145 126, 193, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 146 5, 97, 112, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // apple.com (A, IN) 147 } 148 var udpPacketAppleComA = []byte{ 149 0, 7, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 150 5, 97, 112, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // apple.com (A, IN) 151 } 152 var tcpPacketGoogleComA = []byte{ 153 0, 28, 154 207, 90, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 155 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 5, 0, 1, // google.com. (A, IN) 156 } 157 var udpPacketGoogleComA = []byte{ 158 0, 11, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 159 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // google.com. (A, IN) 160 } 161 var tcpPacketExampleComA = []byte{ 162 0, 29, 163 101, 3, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 164 7, 101, 120, 97, 109, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // example.com. (A, IN) 165 } 166 var tcpPacketDotNS = []byte{ 167 0, 17, 168 213, 147, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 169 0, 0, 2, 0, 1, // . (NS, IN) 170 }