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

Add logging when potentially missing modules are detected #47

Merged
merged 3 commits into from
Sep 27, 2024
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ jobs:
test:
strategy:
matrix:
go-version: [1.20.x]
go-version: [1.21.x]
full-tests: [false]
include:
- go-version: 1.20.x
- go-version: 1.21.x
full-tests: true

runs-on: ubuntu-latest
Expand All @@ -22,7 +22,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.20.7' # TODO: fix matrix
go-version: '1.21.13' # TODO: fix matrix
cache: true

- name: Checkout code
Expand All @@ -32,7 +32,7 @@ jobs:
if: matrix.full-tests
run: |
curl -sL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh |
sh -s -- -b $HOME/go/bin v1.53.3
sh -s -- -b $HOME/go/bin v1.60.3
$HOME/go/bin/golangci-lint run --timeout=30m \
--max-issues-per-linter 0 \
--max-same-issues 0 \
Expand Down
23 changes: 13 additions & 10 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
{
debug
crowdsec {
api_url http://localhost:8080
api_key <api_key>
}
log {
level DEBUG
}
crowdsec {
api_url http://localhost:7080
api_key {env.CROWDSEC_API_KEY}
ticker_interval 3s
}
}

localhost {
route {
crowdsec
respond "Allowed by CrowdSec!"
}
}
route {
crowdsec
respond "Allowed by CrowdSec!"
}
}
104 changes: 104 additions & 0 deletions crowdsec/crowdsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
"errors"
"fmt"
"net"
"reflect"
"runtime/debug"
"slices"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
Expand Down Expand Up @@ -113,10 +117,110 @@ func (c *CrowdSec) Validate() error {
if c.bouncer == nil {
return errors.New("bouncer instance not available due to (potential) misconfiguration")
}
if err := c.checkModules(); err != nil {
return fmt.Errorf("failed checking CrowdSec modules: %w", err)
}

return nil
}

const (
handlerName = "http.handlers.crowdsec"
matcherName = "layer4.matchers.crowdsec"
)

var crowdSecModules = []string{handlerName, matcherName}

func (c *CrowdSec) checkModules() error {
modules, err := matchModules(crowdSecModules...)
if err != nil {
return fmt.Errorf("failed retrieving CrowdSec modules: %w", err)
}

layer4, err := matchModules("layer4")
if err != nil {
return fmt.Errorf("failed retrieving layer4 module: %w", err)
}

hasLayer4 := len(layer4) > 0
switch {
case hasLayer4 && len(modules) == 0:
c.logger.Warn(fmt.Sprintf("%s and %s modules are not available", handlerName, matcherName))
case hasLayer4 && hasModule(modules, matcherName) && !hasModule(modules, handlerName):
c.logger.Warn(fmt.Sprintf("%s module is not available", handlerName))
case hasLayer4 && hasModule(modules, handlerName) && !hasModule(modules, matcherName):
c.logger.Warn(fmt.Sprintf("%s module is not available", matcherName))
case len(modules) == 0:
c.logger.Warn(fmt.Sprintf("%s module is not available", handlerName))
}

return nil
}

type moduleInfo struct {
caddyModuleID string
standard bool
goModule *debug.Module
err error
}

func hasModule(modules []moduleInfo, moduleIdentifier string) bool {
for _, m := range modules {
if m.caddyModuleID == moduleIdentifier {
return true
}
}
return false
}

func matchModules(moduleIdentifiers ...string) (modules []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo()
if !ok {
err = fmt.Errorf("no build info")
return
}

for _, modID := range caddy.Modules() {
if !slices.Contains(moduleIdentifiers, modID) {
continue
}

modInfo, err := caddy.GetModule(modID)
if err != nil {
modules = append(modules, moduleInfo{caddyModuleID: modID, err: err})
continue
}

// to get the Caddy plugin's version info, we need to know
// the package that the Caddy module's value comes from; we
// can use reflection but we need a non-pointer value (I'm
// not sure why), and since New() should return a pointer
// value, we need to dereference it first
iface := any(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
modPkgPath := reflect.TypeOf(iface).PkgPath()

// now we find the Go module that the Caddy module's package
// belongs to; we assume the Caddy module package path will
// be prefixed by its Go module path, and we will choose the
// longest matching prefix in case there are nested modules
var matched *debug.Module
for _, dep := range bi.Deps {
if strings.HasPrefix(modPkgPath, dep.Path) {
if matched == nil || len(dep.Path) > len(matched.Path) {
matched = dep
}
}
}

standard := strings.HasPrefix(modPkgPath, caddy.ImportPath)
modules = append(modules, moduleInfo{caddyModuleID: modID, standard: standard, goModule: matched})
}
return
}

func (c *CrowdSec) Cleanup() error {
if err := c.bouncer.Shutdown(); err != nil {
return fmt.Errorf("failed cleaning up: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/hslatman/caddy-crowdsec-bouncer

go 1.20
go 1.21

require (
github.com/caddyserver/caddy/v2 v2.7.5
Expand Down
Loading
Loading