Deprecate IPWhiteList middleware in favor of IPAllowList
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
parent
9662cdca64
commit
0e92b02474
36 changed files with 1268 additions and 50 deletions
88
pkg/middlewares/ipallowlist/ip_allowlist.go
Normal file
88
pkg/middlewares/ipallowlist/ip_allowlist.go
Normal 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)
|
||||
}
|
||||
}
|
100
pkg/middlewares/ipallowlist/ip_allowlist_test.go
Normal file
100
pkg/middlewares/ipallowlist/ip_allowlist_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
65
pkg/middlewares/tcp/ipallowlist/ip_allowlist.go
Normal file
65
pkg/middlewares/tcp/ipallowlist/ip_allowlist.go
Normal 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)
|
||||
}
|
139
pkg/middlewares/tcp/ipallowlist/ip_allowlist_test.go
Normal file
139
pkg/middlewares/tcp/ipallowlist/ip_allowlist_test.go
Normal 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()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue