mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-03 19:17:29 -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
					
				@ -430,5 +430,13 @@ func (ctx Context) Storage() certmagic.Storage {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Logger returns a logger that can be used by mod.
 | 
					// Logger returns a logger that can be used by mod.
 | 
				
			||||||
func (ctx Context) Logger(mod Module) *zap.Logger {
 | 
					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)
 | 
						return ctx.cfg.Logging.Logger(mod)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -16,13 +16,18 @@ package caddytls
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/caddyserver/caddy/v2"
 | 
						"github.com/caddyserver/caddy/v2"
 | 
				
			||||||
	"github.com/caddyserver/certmagic"
 | 
						"github.com/caddyserver/certmagic"
 | 
				
			||||||
 | 
						"go.uber.org/zap"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	caddy.RegisterModule(MatchServerName{})
 | 
						caddy.RegisterModule(MatchServerName{})
 | 
				
			||||||
 | 
						caddy.RegisterModule(MatchRemoteIP{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MatchServerName matches based on SNI. Names in
 | 
					// MatchServerName matches based on SNI. Names in
 | 
				
			||||||
@ -48,5 +53,100 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
 | 
				
			|||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Interface guard
 | 
					// MatchRemoteIP matches based on the remote IP of the
 | 
				
			||||||
var _ ConnectionMatcher = (*MatchServerName)(nil)
 | 
					// 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
 | 
					package caddytls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestServerNameMatcher(t *testing.T) {
 | 
					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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user