1
0
Fork 0

Deprecate IPWhiteList middleware in favor of IPAllowList

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Landry Benguigui 2024-01-02 17:04:06 +01:00 committed by GitHub
parent 9662cdca64
commit 0e92b02474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1268 additions and 50 deletions

View file

@ -0,0 +1,88 @@
package ipallowlist
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares"
"github.com/traefik/traefik/v2/pkg/tracing"
)
const (
typeName = "IPAllowLister"
)
// ipAllowLister is a middleware that provides Checks of the Requesting IP against a set of Allowlists.
type ipAllowLister struct {
next http.Handler
allowLister *ip.Checker
strategy ip.Strategy
name string
}
// New builds a new IPAllowLister given a list of CIDR-Strings to allow.
func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, name string) (http.Handler, error) {
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName))
logger.Debug("Creating middleware")
if len(config.SourceRange) == 0 {
return nil, errors.New("sourceRange is empty, IPAllowLister not created")
}
checker, err := ip.NewChecker(config.SourceRange)
if err != nil {
return nil, fmt.Errorf("cannot parse CIDRs %s: %w", config.SourceRange, err)
}
strategy, err := config.IPStrategy.Get()
if err != nil {
return nil, err
}
logger.Debugf("Setting up IPAllowLister with sourceRange: %s", config.SourceRange)
return &ipAllowLister{
strategy: strategy,
allowLister: checker,
next: next,
name: name,
}, nil
}
func (al *ipAllowLister) GetTracingInformation() (string, ext.SpanKindEnum) {
return al.name, tracing.SpanKindNoneEnum
}
func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := middlewares.GetLoggerCtx(req.Context(), al.name, typeName)
logger := log.FromContext(ctx)
clientIP := al.strategy.GetIP(req)
err := al.allowLister.IsAuthorized(clientIP)
if err != nil {
msg := fmt.Sprintf("Rejecting IP %s: %v", clientIP, err)
logger.Debug(msg)
tracing.SetErrorWithEvent(req, msg)
reject(ctx, rw)
return
}
logger.Debugf("Accepting IP %s", clientIP)
al.next.ServeHTTP(rw, req)
}
func reject(ctx context.Context, rw http.ResponseWriter) {
statusCode := http.StatusForbidden
rw.WriteHeader(statusCode)
_, err := rw.Write([]byte(http.StatusText(statusCode)))
if err != nil {
log.FromContext(ctx).Error(err)
}
}

View file

@ -0,0 +1,100 @@
package ipallowlist
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
)
func TestNewIPAllowLister(t *testing.T) {
testCases := []struct {
desc string
allowList dynamic.IPAllowList
expectedError bool
}{
{
desc: "invalid IP",
allowList: dynamic.IPAllowList{
SourceRange: []string{"foo"},
},
expectedError: true,
},
{
desc: "valid IP",
allowList: dynamic.IPAllowList{
SourceRange: []string{"10.10.10.10"},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
allowLister, err := New(context.Background(), next, test.allowList, "traefikTest")
if test.expectedError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.NotNil(t, allowLister)
}
})
}
}
func TestIPAllowLister_ServeHTTP(t *testing.T) {
testCases := []struct {
desc string
allowList dynamic.IPAllowList
remoteAddr string
expected int
}{
{
desc: "authorized with remote address",
allowList: dynamic.IPAllowList{
SourceRange: []string{"20.20.20.20"},
},
remoteAddr: "20.20.20.20:1234",
expected: 200,
},
{
desc: "non authorized with remote address",
allowList: dynamic.IPAllowList{
SourceRange: []string{"20.20.20.20"},
},
remoteAddr: "20.20.20.21:1234",
expected: 403,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
allowLister, err := New(context.Background(), next, test.allowList, "traefikTest")
require.NoError(t, err)
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://10.10.10.10", nil)
if len(test.remoteAddr) > 0 {
req.RemoteAddr = test.remoteAddr
}
allowLister.ServeHTTP(recorder, req)
assert.Equal(t, test.expected, recorder.Code)
})
}
}

View file

@ -0,0 +1,65 @@
package ipallowlist
import (
"context"
"errors"
"fmt"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares"
"github.com/traefik/traefik/v2/pkg/tcp"
)
const (
typeName = "IPAllowListerTCP"
)
// ipAllowLister is a middleware that provides Checks of the Requesting IP against a set of Allowlists.
type ipAllowLister struct {
next tcp.Handler
allowLister *ip.Checker
name string
}
// New builds a new TCP IPAllowLister given a list of CIDR-Strings to allow.
func New(ctx context.Context, next tcp.Handler, config dynamic.TCPIPAllowList, name string) (tcp.Handler, error) {
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName))
logger.Debug("Creating middleware")
if len(config.SourceRange) == 0 {
return nil, errors.New("sourceRange is empty, IPAllowLister not created")
}
checker, err := ip.NewChecker(config.SourceRange)
if err != nil {
return nil, fmt.Errorf("cannot parse CIDRs %s: %w", config.SourceRange, err)
}
logger.Debugf("Setting up IPAllowLister with sourceRange: %s", config.SourceRange)
return &ipAllowLister{
allowLister: checker,
next: next,
name: name,
}, nil
}
func (al *ipAllowLister) ServeTCP(conn tcp.WriteCloser) {
ctx := middlewares.GetLoggerCtx(context.Background(), al.name, typeName)
logger := log.FromContext(ctx)
addr := conn.RemoteAddr().String()
err := al.allowLister.IsAuthorized(addr)
if err != nil {
logger.Errorf("Connection from %s rejected: %v", addr, err)
conn.Close()
return
}
logger.Debugf("Connection from %s accepted", addr)
al.next.ServeTCP(conn)
}

View file

@ -0,0 +1,139 @@
package ipallowlist
import (
"context"
"io"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/tcp"
)
func TestNewIPAllowLister(t *testing.T) {
testCases := []struct {
desc string
allowList dynamic.TCPIPAllowList
expectedError bool
}{
{
desc: "Empty config",
allowList: dynamic.TCPIPAllowList{},
expectedError: true,
},
{
desc: "invalid IP",
allowList: dynamic.TCPIPAllowList{
SourceRange: []string{"foo"},
},
expectedError: true,
},
{
desc: "valid IP",
allowList: dynamic.TCPIPAllowList{
SourceRange: []string{"10.10.10.10"},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := tcp.HandlerFunc(func(conn tcp.WriteCloser) {})
allowLister, err := New(context.Background(), next, test.allowList, "traefikTest")
if test.expectedError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.NotNil(t, allowLister)
}
})
}
}
func TestIPAllowLister_ServeHTTP(t *testing.T) {
testCases := []struct {
desc string
allowList dynamic.TCPIPAllowList
remoteAddr string
expected string
}{
{
desc: "authorized with remote address",
allowList: dynamic.TCPIPAllowList{
SourceRange: []string{"20.20.20.20"},
},
remoteAddr: "20.20.20.20:1234",
expected: "OK",
},
{
desc: "non authorized with remote address",
allowList: dynamic.TCPIPAllowList{
SourceRange: []string{"20.20.20.20"},
},
remoteAddr: "20.20.20.21:1234",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := tcp.HandlerFunc(func(conn tcp.WriteCloser) {
write, err := conn.Write([]byte("OK"))
require.NoError(t, err)
assert.Equal(t, 2, write)
err = conn.Close()
require.NoError(t, err)
})
allowLister, err := New(context.Background(), next, test.allowList, "traefikTest")
require.NoError(t, err)
server, client := net.Pipe()
go func() {
allowLister.ServeTCP(&contextWriteCloser{client, addr{test.remoteAddr}})
}()
read, err := io.ReadAll(server)
require.NoError(t, err)
assert.Equal(t, test.expected, string(read))
})
}
}
type contextWriteCloser struct {
net.Conn
addr
}
type addr struct {
remoteAddr string
}
func (a addr) Network() string {
panic("implement me")
}
func (a addr) String() string {
return a.remoteAddr
}
func (c contextWriteCloser) CloseWrite() error {
panic("implement me")
}
func (c contextWriteCloser) RemoteAddr() net.Addr { return c.addr }
func (c contextWriteCloser) Context() context.Context {
return context.Background()
}