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 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ toolchain go1.23.0

require (
github.com/caddyserver/caddy/v2 v2.8.4
github.com/fsnotify/fsnotify v1.8.0
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 @@ -91,7 +93,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 Expand Up @@ -139,7 +140,7 @@ require (
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.22.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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 Expand Up @@ -580,8 +582,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
_ "github.com/mholt/caddy-l4/modules/l4quic"
_ "github.com/mholt/caddy-l4/modules/l4rdp"
_ "github.com/mholt/caddy-l4/modules/l4regexp"
_ "github.com/mholt/caddy-l4/modules/l4remoteiplist"
_ "github.com/mholt/caddy-l4/modules/l4socks"
_ "github.com/mholt/caddy-l4/modules/l4ssh"
_ "github.com/mholt/caddy-l4/modules/l4subroute"
Expand Down
47 changes: 47 additions & 0 deletions integration/caddyfile_adapt/gd_matcher_remoteiplist.caddytest
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
layer4 {
:12345 {
@f1 remote_ip_list /tmp/remote-ips
route @f1 {
proxy f1.machine.local:54321
}
}
}
}
----------
{
"apps": {
"layer4": {
"servers": {
"srv0": {
"listen": [
":12345"
],
"routes": [
{
"match": [
{
"remote_ip_list": {
"remote_ip_file": "/tmp/remote-ips"
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"f1.machine.local:54321"
]
}
]
}
]
}
]
}
}
}
}
}
186 changes: 186 additions & 0 deletions modules/l4remoteiplist/iplist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// 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 l4remoteiplist

import (
"bufio"
"fmt"
"net/netip"
"os"
"path/filepath"
"sync"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/fsnotify/fsnotify"
"go.uber.org/zap"
)

type IPList struct {
ipFile string // File containing all IPs / CIDRs to be matched, gets continously monitored
cidrs []netip.Prefix // List of currently loaded CIDRs
ctx caddy.Context // Caddy context, used to detect when to shut down
logger *zap.Logger
reloadNeededMutex sync.Mutex // Mutex to ensure proper concurrent handling of reloads
reloadNeeded bool // Flag indicating whether a reload of the IPs is needed
}

// Creates a new IPList, creating the ipFile if it is not present
func NewIPList(ipFile string, ctx caddy.Context, logger *zap.Logger) (*IPList, error) {
ipList := &IPList{
ipFile: ipFile,
ctx: ctx,
logger: logger,
reloadNeeded: true,
}

// make sure the directory containing the ipFile exists
// otherwise, the fsnotify watcher will not work
if !ipList.ipFileDirectoryExists() {
return nil, fmt.Errorf("could not find the directory containing the IP file to monitor: %v", ipFile)
}

return ipList, nil
}

// Check whether a IP address is currently contained in the IP list
func (b *IPList) IsMatched(ip netip.Addr) bool {
// First reload the IP list if needed to ensure IPs are always up to date
b.reloadNeededMutex.Lock()
if b.reloadNeeded {
err := b.loadIPAddresses()
if err != nil {
b.logger.Error("could not load IP addresses", zap.Error(err))
} else {
b.reloadNeeded = false
b.logger.Debug("reloaded IP addresses")
}
}
b.reloadNeededMutex.Unlock()

for _, cidr := range b.cidrs {
if cidr.Contains(ip) {
return true
}
}
return false
}

// Start to monitor the IP list
func (b *IPList) StartMonitoring() {
go b.monitor()
}
Comment on lines +82 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double check, this won't leak goroutines on every reload because Caddy closes/shuts the caddy.Context on config reload and/or shutdown, right?

This is the only concern I have.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye. It does seem to check the context in its loop, and return after an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @mholt said, the main loop within the goroutine currently returns as soon as ctx.Done() is true or an error occurs.
Is there anything else that should be checked to prevent a goroutine leak? Is ctx.Done() always true when the configuration is reloaded?


func (b *IPList) ipFileDirectoryExists() bool {
// Make sure the directory containing the IP list exists
dirpath := filepath.Dir(b.ipFile)
st, err := os.Lstat(dirpath)
if err != nil || !st.IsDir() {
return false
}
return true
}

func (b *IPList) ipFileExists() bool {
// Make sure the IP list exists and is a file
st, err := os.Lstat(b.ipFile)
if err != nil || st.IsDir() {
return false
}
return true
}

func (b *IPList) monitor() {
// Create a new watcher
w, err := fsnotify.NewWatcher()
if err != nil {
b.logger.Error("error creating a new filesystem watcher", zap.Error(err))
return
}
defer w.Close()

if !b.ipFileDirectoryExists() {
b.logger.Error("directory containing the IP file to monitor does not exist")
return
}

// Monitor the directory of the file
err = w.Add(filepath.Dir(b.ipFile))
if err != nil {
b.logger.Error("error watching the file", zap.Error(err))
return
}

for {
select {
case <-b.ctx.Done():
// Check if Caddy closed the context
b.logger.Debug("caddy closed the context")
return
case err, ok := <-w.Errors:
b.logger.Error("error from file watcher", zap.Error(err))
if !ok {
b.logger.Error("file watcher was closed")
return
}
case e, ok := <-w.Events:
if !ok {
b.logger.Error("file watcher was closed")
return
}

// Check if the IP list has changed
if b.ipFile == e.Name && (e.Has(fsnotify.Create) || e.Has(fsnotify.Write)) {
b.reloadNeededMutex.Lock()
b.reloadNeeded = true
b.reloadNeededMutex.Unlock()
}
}
}
}

// Loads the IP addresses from the IP list
func (b *IPList) loadIPAddresses() error {
if !b.ipFileExists() {
b.logger.Debug("ip file not found, nothing to monitor")
b.cidrs = make([]netip.Prefix, 0)
return nil
}

file, err := os.Open(b.ipFile)
if err != nil {
return fmt.Errorf("error opening the IP list file %v: %w", b.ipFile, err)
}
defer file.Close()

var cidrs []netip.Prefix
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
cidr, err := caddyhttp.CIDRExpressionToPrefix(line)
if err == nil {
// only append valid IP addresses / CIDRs (ignore lines that
// have not been parsed successfully, e.g. comments)
cidrs = append(cidrs, cidr)
}
}
err = scanner.Err()
if err != nil {
return fmt.Errorf("error reading the IPs from %v: %w", b.ipFile, err)
}

b.cidrs = cidrs
return nil
}
Loading