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 9 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": {
"ip_file": "/tmp/remote-ips"
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"f1.machine.local:54321"
]
}
]
}
]
}
]
}
}
}
}
}
176 changes: 176 additions & 0 deletions modules/l4remoteiplist/iplist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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"
"os"
"path/filepath"
"sync"

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

type IpList struct {
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
ipFile string // File containing all IPs to be matched, gets continously monitored
ipAddresses []string // List of currently loaded IP addresses that would matched
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) {
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
ipList := &IpList{
ipFile: ipFile,
ctx: ctx,
logger: logger,
reloadNeeded: true,
}

if !ipList.ipFileExists() {
logger.Debug("could not find the file containing the IPs, trying to create it...")

// Create a new file since it does not exist
_, err := os.Create(ipFile)
if err != nil {
logger.Error("Error creating the IP list", zap.Error(err))
return nil, fmt.Errorf("cannot create a new IP list: %v", err)
}
logger.Debug("list of IP addresses successfully created")
}
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved

return ipList, nil
}

// Check whether a IP address is currently contained in the IP list
func (b *IpList) IsMatched(ip string) bool {
// First reload the IP list if needed to ensure IPs are always up to date
b.reloadNeededMutex.Lock()
reloadNeeded := b.reloadNeeded
b.reloadNeeded = false
b.reloadNeededMutex.Unlock()
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved

if reloadNeeded {
err := b.loadIpAddresses()
if err != nil {
b.logger.Error("could not load IP addresses")
} else {
b.logger.Debug("reloaded IP addresses")
}
}

for _, listIp := range b.ipAddresses {
if listIp == ip {
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}
return false
}

// Start to monitor the IP list
func (b *IpList) StartMonitoring() {
go b.monitor()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's more about code styling, but is there much sense in a one-line function that is called from a single place? I believe, it's just an additional execution hop we could eliminate.

Copy link
Author

Choose a reason for hiding this comment

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

I left it like this for the time being to simplify the code at the provision. The idea was to avoid starting goroutines there.


func (b *IpList) ipFileExists() bool {
// Make sure the IP list 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.ipFileExists() {
b.logger.Error("list of IP addresses does not exist, nothing to monitor")
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.Error("list of IP addresses does not exist, could not load IP addresses")
return fmt.Errorf("list of IP addresses %v does not exist, could not load IP addresses", b.ipFile)
}

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

var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
b.logger.Error("error reading the IP list", zap.Error(err))
return fmt.Errorf("error reading the IPs from %v", b.ipFile)
}

b.ipAddresses = lines
trefzaxSICKAG marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
Loading