Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

L4 module "remote_ip_list" to support fail2ban #266

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ go 1.22.0
toolchain go1.23.0

require (
github.com/Javex/caddy-fail2ban v0.0.0-20240525135356-cbf9f5a003ce
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
github.com/caddyserver/caddy/v2 v2.8.4
github.com/mastercactapus/proxyprotocol v0.0.4
github.com/miekg/dns v1.1.62
github.com/quic-go/quic-go v0.44.0
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
github.com/things-go/go-socks5 v0.0.5
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.28.0
Expand Down Expand Up @@ -41,6 +43,7 @@ require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
Expand Down Expand Up @@ -91,7 +94,6 @@ require (
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.44.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOv
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Javex/caddy-fail2ban v0.0.0-20240525135356-cbf9f5a003ce h1:5quSf3g4qNVX1iiPkc3BqTj7xh34txsVtYxmG+7DSR0=
github.com/Javex/caddy-fail2ban v0.0.0-20240525135356-cbf9f5a003ce/go.mod h1:J1TiCEqAglEaeHnO48l80bkS2z9yx4dMhes34Lp52uE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
Expand Down Expand Up @@ -135,6 +137,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
_ "github.com/mholt/caddy-l4/modules/l4clock"
_ "github.com/mholt/caddy-l4/modules/l4dns"
_ "github.com/mholt/caddy-l4/modules/l4echo"
_ "github.com/mholt/caddy-l4/modules/l4fail2ban"
_ "github.com/mholt/caddy-l4/modules/l4http"
_ "github.com/mholt/caddy-l4/modules/l4openvpn"
_ "github.com/mholt/caddy-l4/modules/l4postgres"
Expand Down
129 changes: 129 additions & 0 deletions modules/l4fail2ban/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) 2024 SICK AG
//
// 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 l4fail2ban

import (
"fmt"
"net"
"net/netip"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"

caddy_fail2ban "github.com/Javex/caddy-fail2ban"
"github.com/mholt/caddy-l4/layer4"
"go.uber.org/zap"
)

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

type Fail2Ban struct {
Banfile string `json:"banfile"`
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved

logger *zap.Logger
banlist caddy_fail2ban.Banlist
}

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

// Provision implements caddy.Provisioner.
func (m *Fail2Ban) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()

// Create new banlist, same as in http.matchers.fail2ban (https://github.com/Javex/caddy-fail2ban/blob/main/banlist.go)
m.banlist = caddy_fail2ban.NewBanlist(ctx, m.logger, &m.Banfile)
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
m.banlist.Start()
return nil
}

// The Match will return true if the remote IP is found in the ban list
func (m *Fail2Ban) Match(cx *layer4.Connection) (bool, error) {
clientIP, err := m.getRemoteIP(cx)
if err != nil {
// Error, tread IP as banned
m.logger.Error("Error parsing the remote IP from the connection", zap.Error(err))
return true, err
}

strClientIP := clientIP.String()
if m.banlist.IsBanned(strClientIP) {
// IP is banned
m.logger.Info("banned IP", zap.String("remote_addr", strClientIP))
return true, nil
}

// IP not found in banlist, everything ok
m.logger.Debug("received request", zap.String("remote_addr", strClientIP))
return false, nil
}

// Returns the remote IP address for a given layer4 connection.
// Same method as in layer4.MatchRemoteIP.getRemoteIP
func (m *Fail2Ban) getRemoteIP(cx *layer4.Connection) (netip.Addr, error) {
remote := cx.Conn.RemoteAddr().String()

ipStr, _, err := net.SplitHostPort(remote)
if err != nil {
ipStr = remote
}

ip, err := netip.ParseAddr(ipStr)
if err != nil {
return netip.Addr{}, fmt.Errorf("invalid remote IP address: %s", ipStr)
}
return ip, nil
}

// UnmarshalCaddyfile sets up the banfile_path from Caddyfile. Syntax:
//
// fail2ban <banfile_path>
func (m *Fail2Ban) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val() // consume wrapper name

if d.Val() != "fail2ban" {
vnxme marked this conversation as resolved.
Show resolved Hide resolved
return d.ArgErr()
}

// Only one same-line argument is supported
if d.CountRemainingArgs() > 1 {
return d.ArgErr()
}

if d.NextArg() {
m.Banfile = d.Val()
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 (
_ layer4.ConnMatcher = (*Fail2Ban)(nil)
_ caddy.Provisioner = (*Fail2Ban)(nil)
_ caddyfile.Unmarshaler = (*Fail2Ban)(nil)
)
173 changes: 173 additions & 0 deletions modules/l4fail2ban/matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) 2024 SICK AG
//
// 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 l4fail2ban

import (
"context"
"fmt"
"net"
"os"
"path"
"testing"
"time"

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

// Setup dummy structs for test cases as in
// https://github.com/mholt/caddy-l4/blob/master/layer4/matchers_test.go
var _ net.Conn = &dummyConn{}
var _ net.Addr = dummyAddr{}

type dummyAddr struct {
ip string
network string
}

// Network implements net.Addr.
func (da dummyAddr) Network() string {
return da.network
}

// String implements net.Addr.
func (da dummyAddr) String() string {
return da.ip
}

type dummyConn struct {
net.Conn
remoteAddr net.Addr
}

// RemoteAddr implements net.Conn.
func (dc *dummyConn) RemoteAddr() net.Addr {
return dc.remoteAddr
}

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

// Create a temporary directory and a ban file
func createBanFile(t *testing.T) (string, string) {
t.Helper()
tempDir, err := os.MkdirTemp("", "caddy-l4-fail2ban-test")
assertNoError(t, err)

banFile := path.Join(tempDir, "banned-ips")
return tempDir, banFile
}

// Cleanup the temporary directory and the ban file
func cleanupBanFile(t *testing.T, tempDir string) {
t.Helper()
err := os.RemoveAll(tempDir)
assertNoError(t, err)
}

// Test the Caddyfile unmarshaller
func TestFail2BanUnmarshaller(t *testing.T) {
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
tempDir, banFile := createBanFile(t)
defer cleanupBanFile(t, tempDir)

dispenser := caddyfile.NewTestDispenser(fmt.Sprintf(`fail2ban %s`, banFile))

matcher := Fail2Ban{}
err := matcher.UnmarshalCaddyfile(dispenser)
assertNoError(t, err)

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

err = matcher.Provision(ctx)
assertNoError(t, err)

// Wait for the banlist to be loaded by the matcher
time.Sleep(100 * time.Millisecond)

if matcher.Banfile != banFile {
t.Fatalf("Expected %s, got %s", banFile, matcher.Banfile)
}
}

// Test if a banned IP is matched
func TestFail2BanMatch(t *testing.T) {
tempDir, banFile := createBanFile(t)
defer cleanupBanFile(t, tempDir)

os.WriteFile(banFile, []byte("127.0.0.99"), 0644)

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

matcher := Fail2Ban{
Banfile: banFile,
}

err := matcher.Provision(ctx)
assertNoError(t, err)

cx := &layer4.Connection{
Conn: &dummyConn{
remoteAddr: dummyAddr{ip: "127.0.0.99", network: "tcp"},
},
Logger: zap.NewNop(),
}

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

if !matched {
t.Fatalf("Matcher did not match")
}
}

// Test if a non-banned IP is not matched
func TestFail2BanNoMatch(t *testing.T) {
tempDir, banFile := createBanFile(t)
defer cleanupBanFile(t, tempDir)

os.WriteFile(banFile, []byte("127.0.0.1"), 0644)

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

matcher := Fail2Ban{
Banfile: banFile,
}

err := matcher.Provision(ctx)
assertNoError(t, err)

cx := &layer4.Connection{
Conn: &dummyConn{
remoteAddr: dummyAddr{ip: "127.0.0.99", network: "tcp"},
},
Logger: zap.NewNop(),
}

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

if matched {
t.Fatalf("Matcher did match")
}
}