Skip to content

Commit

Permalink
Regular expression (regexp) matcher: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
vnxme committed Jul 24, 2024
1 parent d37fd68 commit 574476b
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Current matchers:
- **layer4.matchers.not** - matches connections that aren't matched by inner matcher sets.
- **layer4.matchers.proxy_protocol** - matches connections that start with [HAPROXY proxy protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt).
- **layer4.matchers.rdp** - matches connections that look like [RDP](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPBCGR/%5BMS-RDPBCGR%5D.pdf).
- **layer4.matchers.regexp** - matches connections that have the first packet bytes matching a regular expression.
- **layer4.matchers.socks4** - matches connections that look like [SOCKSv4](https://www.openssh.com/txt/socks4.protocol).
- **layer4.matchers.socks5** - matches connections that look like [SOCKSv5](https://www.rfc-editor.org/rfc/rfc1928.html).
- **layer4.matchers.xmpp** - matches connections that look like [XMPP](https://xmpp.org/about/technology-overview/).
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
_ "github.com/mholt/caddy-l4/modules/l4proxy"
_ "github.com/mholt/caddy-l4/modules/l4proxyprotocol"
_ "github.com/mholt/caddy-l4/modules/l4rdp"
_ "github.com/mholt/caddy-l4/modules/l4regexp"
_ "github.com/mholt/caddy-l4/modules/l4socks"
_ "github.com/mholt/caddy-l4/modules/l4ssh"
_ "github.com/mholt/caddy-l4/modules/l4subroute"
Expand Down
109 changes: 109 additions & 0 deletions modules/l4regexp/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2024 VNXME
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package l4regexp

import (
"io"
"regexp"
"strconv"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/mholt/caddy-l4/layer4"
)

func init() {
caddy.RegisterModule(&MatchRegexp{})
}

// MatchRegexp is able to match any connections with regular expressions.
type MatchRegexp struct {
Count uint16 `json:"count,omitempty"`
Pattern string `json:"pattern,omitempty"`

compiled *regexp.Regexp
}

// CaddyModule returns the Caddy module information.
func (m *MatchRegexp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "layer4.matchers.regexp",
New: func() caddy.Module { return new(MatchRegexp) },
}
}

// Match returns true if the connection bytes match the regular expression.
func (m *MatchRegexp) Match(cx *layer4.Connection) (bool, error) {
// Read a number of bytes
buf := make([]byte, m.Count)
n, err := io.ReadFull(cx, buf)
if err != nil || n < int(m.Count) {
return false, nil
}

// Match these bytes against the regular expression
return m.compiled.Match(buf), nil
}

// Provision parses m's regular expression and sets m's minimum read bytes count.
func (m *MatchRegexp) Provision(_ caddy.Context) (err error) {
if m.Count == 0 {
m.Count = minCount
}
m.compiled, err = regexp.Compile(m.Pattern)
if err != nil {
return err
}
return nil
}

// UnmarshalCaddyfile sets up the MatchRegexp from Caddyfile tokens. Syntax:
//
// regexp <pattern> [<count>]
func (m *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val() // consume wrapper name

// One or two same-line argument must be provided
if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
return d.ArgErr()
}

_, m.Pattern = d.NextArg(), d.Val()
if d.NextArg() {
val, err := strconv.ParseUint(d.Val(), 10, 16)
if err != nil {
return d.Errf("parsing %s count: %v", wrapper, err)
}
m.Count = uint16(val)
}

// No blocks are supported
if d.NextBlock(d.Nesting()) {
return d.Errf("malformed %s option: blocks are not supported", wrapper)
}

return nil
}

// Interface guards
var (
_ caddy.Provisioner = (*MatchRegexp)(nil)
_ caddyfile.Unmarshaler = (*MatchRegexp)(nil)
_ layer4.ConnMatcher = (*MatchRegexp)(nil)
)

const (
minCount uint16 = 4 // by default, read this many bytes to match against
)
90 changes: 90 additions & 0 deletions modules/l4regexp/matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2024 VNXME
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package l4regexp

import (
"context"
"io"
"net"
"testing"

"github.com/caddyserver/caddy/v2"
"github.com/mholt/caddy-l4/layer4"
"go.uber.org/zap"
)

func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("Unexpected error: %s\n", err)
}
}

func Test_MatchRegexp_Match(t *testing.T) {
type test struct {
matcher *MatchRegexp
data []byte
shouldMatch bool
}

tests := []test{
{matcher: &MatchRegexp{}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: ""}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "12"}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "^0123$"}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "^012$"}, data: packet0123, shouldMatch: false},
{matcher: &MatchRegexp{Pattern: "^0123$", Count: 5}, data: packet0123, shouldMatch: false},
{matcher: &MatchRegexp{Pattern: "^012$", Count: 3}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "^\\d+$"}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "^\\d+$", Count: 0}, data: packet0123, shouldMatch: true},
{matcher: &MatchRegexp{Pattern: "^\x30\x31\x32(\x33|\x34)$", Count: 0}, data: packet0123, shouldMatch: true},
}

ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()

for i, tc := range tests {
func() {
err := tc.matcher.Provision(ctx)
assertNoError(t, err)

in, out := net.Pipe()
defer func() {
_, _ = io.Copy(io.Discard, out)
_ = out.Close()
}()

cx := layer4.WrapConnection(out, []byte{}, zap.NewNop())
go func() {
_, err := in.Write(tc.data)
assertNoError(t, err)
_ = in.Close()
}()

matched, err := tc.matcher.Match(cx)
assertNoError(t, err)

if matched != tc.shouldMatch {
if tc.shouldMatch {
t.Fatalf("test %d: matcher did not match | %+v\n", i, tc.matcher)
} else {
t.Fatalf("test %d: matcher should not match | %+v\n", i, tc.matcher)
}
}
}()
}
}

var packet0123 = []byte{0x30, 0x31, 0x32, 0x33}

0 comments on commit 574476b

Please sign in to comment.