mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
caddytls: Implement remote IP connection matcher (#4123)
* caddytls: Implement remote IP connection matcher * Implement IP range negation If both Ranges and NotRanges are specified, both must match.
This commit is contained in:
parent
ff6ca577ec
commit
956f01163d
3 changed files with 202 additions and 2 deletions
|
@ -430,5 +430,13 @@ func (ctx Context) Storage() certmagic.Storage {
|
|||
|
||||
// Logger returns a logger that can be used by mod.
|
||||
func (ctx Context) Logger(mod Module) *zap.Logger {
|
||||
if ctx.cfg == nil {
|
||||
// often the case in tests; just use a dev logger
|
||||
l, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
panic("config missing, unable to create dev logger: " + err.Error())
|
||||
}
|
||||
return l
|
||||
}
|
||||
return ctx.cfg.Logging.Logger(mod)
|
||||
}
|
||||
|
|
|
@ -16,13 +16,18 @@ package caddytls
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(MatchServerName{})
|
||||
caddy.RegisterModule(MatchRemoteIP{})
|
||||
}
|
||||
|
||||
// MatchServerName matches based on SNI. Names in
|
||||
|
@ -48,5 +53,100 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ ConnectionMatcher = (*MatchServerName)(nil)
|
||||
// MatchRemoteIP matches based on the remote IP of the
|
||||
// connection. Specific IPs or CIDR ranges can be specified.
|
||||
//
|
||||
// Note that IPs can sometimes be spoofed, so do not rely
|
||||
// on this as a replacement for actual authentication.
|
||||
type MatchRemoteIP struct {
|
||||
// The IPs or CIDR ranges to match.
|
||||
Ranges []string `json:"ranges,omitempty"`
|
||||
|
||||
// The IPs or CIDR ranges to *NOT* match.
|
||||
NotRanges []string `json:"not_ranges,omitempty"`
|
||||
|
||||
cidrs []*net.IPNet
|
||||
notCidrs []*net.IPNet
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "tls.handshake_match.remote_ip",
|
||||
New: func() caddy.Module { return new(MatchRemoteIP) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
||||
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||
m.logger = ctx.Logger(m)
|
||||
for _, str := range m.Ranges {
|
||||
cidrs, err := m.parseIPRange(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.cidrs = cidrs
|
||||
}
|
||||
for _, str := range m.NotRanges {
|
||||
cidrs, err := m.parseIPRange(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.notCidrs = cidrs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match matches hello based on the connection's remote IP.
|
||||
func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool {
|
||||
remoteAddr := hello.Conn.RemoteAddr().String()
|
||||
ipStr, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
ipStr = remoteAddr // weird; maybe no port?
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
m.logger.Error("invalid client IP addresss", zap.String("ip", ipStr))
|
||||
return false
|
||||
}
|
||||
return (len(m.cidrs) == 0 || m.matches(ip, m.cidrs)) &&
|
||||
(len(m.notCidrs) == 0 || !m.matches(ip, m.notCidrs))
|
||||
}
|
||||
|
||||
func (MatchRemoteIP) parseIPRange(str string) ([]*net.IPNet, error) {
|
||||
var cidrs []*net.IPNet
|
||||
if strings.Contains(str, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(str)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing CIDR expression: %v", err)
|
||||
}
|
||||
cidrs = append(cidrs, ipNet)
|
||||
} else {
|
||||
ip := net.ParseIP(str)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", str)
|
||||
}
|
||||
mask := len(ip) * 8
|
||||
cidrs = append(cidrs, &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(mask, mask),
|
||||
})
|
||||
}
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func (MatchRemoteIP) matches(ip net.IP, ranges []*net.IPNet) bool {
|
||||
for _, ipRange := range ranges {
|
||||
if ipRange.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ ConnectionMatcher = (*MatchServerName)(nil)
|
||||
_ ConnectionMatcher = (*MatchRemoteIP)(nil)
|
||||
)
|
||||
|
|
|
@ -15,8 +15,12 @@
|
|||
package caddytls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestServerNameMatcher(t *testing.T) {
|
||||
|
@ -84,3 +88,91 @@ func TestServerNameMatcher(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteIPMatcher(t *testing.T) {
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
|
||||
for i, tc := range []struct {
|
||||
ranges []string
|
||||
notRanges []string
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
ranges: []string{"127.0.0.1"},
|
||||
input: "127.0.0.1:12345",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.1"},
|
||||
input: "127.0.0.2:12345",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.1/16"},
|
||||
input: "127.0.1.23:12345",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.1", "192.168.1.105"},
|
||||
input: "192.168.1.105:12345",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
notRanges: []string{"127.0.0.1"},
|
||||
input: "127.0.0.1:12345",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
notRanges: []string{"127.0.0.2"},
|
||||
input: "127.0.0.1:12345",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.1"},
|
||||
notRanges: []string{"127.0.0.2"},
|
||||
input: "127.0.0.1:12345",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.2"},
|
||||
notRanges: []string{"127.0.0.2"},
|
||||
input: "127.0.0.2:12345",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
ranges: []string{"127.0.0.2"},
|
||||
notRanges: []string{"127.0.0.2"},
|
||||
input: "127.0.0.3:12345",
|
||||
expect: false,
|
||||
},
|
||||
} {
|
||||
matcher := MatchRemoteIP{Ranges: tc.ranges, NotRanges: tc.notRanges}
|
||||
err := matcher.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Provision failed: %v", i, err)
|
||||
}
|
||||
|
||||
addr := testAddr(tc.input)
|
||||
chi := &tls.ClientHelloInfo{Conn: testConn{addr: addr}}
|
||||
|
||||
actual := matcher.Match(chi)
|
||||
if actual != tc.expect {
|
||||
t.Errorf("Test %d: Expected %t but got %t (input=%s ranges=%v notRanges=%v)",
|
||||
i, tc.expect, actual, tc.input, tc.ranges, tc.notRanges)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testConn struct {
|
||||
*net.TCPConn
|
||||
addr testAddr
|
||||
}
|
||||
|
||||
func (tc testConn) RemoteAddr() net.Addr { return tc.addr }
|
||||
|
||||
type testAddr string
|
||||
|
||||
func (testAddr) Network() string { return "tcp" }
|
||||
func (ta testAddr) String() string { return string(ta) }
|
||||
|
|
Loading…
Reference in a new issue