From e491c44895fe3f11e24ad4d8c4f6a668144c0ef9 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:49:56 +0300 Subject: [PATCH] Runtime placeholders (#224) * Runtime placeholders: remove IPs validation in UnmarshalCaddyfile * Runtime placeholders: add placeholder replacement for IPs in Provision * Runtime placeholders: add placeholder replacement for other strings * Runtime placeholders: update README * Runtime placeholders: remove unused ParseNetworks --- README.md | 11 ++++ layer4/matchers.go | 81 ++++++++++++------------------ layer4/server.go | 10 ++-- modules/l4proxy/proxy.go | 34 ++++++------- modules/l4proxy/upstream.go | 18 +++---- modules/l4proxyprotocol/handler.go | 22 ++++---- modules/l4rdp/matcher.go | 57 ++++++++++++--------- modules/l4regexp/matcher.go | 3 +- modules/l4socks/socks4_matcher.go | 31 +++++++----- modules/l4socks/socks5_handler.go | 26 ++++++---- modules/l4tls/alpn_matcher.go | 2 + modules/l4tls/matcher.go | 43 +++++++--------- 12 files changed, 173 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index 8c45734..427561f 100644 --- a/README.md +++ b/README.md @@ -583,3 +583,14 @@ While only allowing connections from a specific network and requiring a username } ``` + +## Placeholders support + +Environment variables having `{$VAR}` syntax are supported in Caddyfile only. They are evaluated once at launch before Caddyfile is parsed. + +Runtime placeholders having `{...}` syntax, including environment variables referenced as `{env.VAR}`, are supported in both Caddyfile and pure JSON, with some caveats described below. +- Options of *int*, *float*, *big.int*, *duration*, and other numeric types don't support runtime placeholders at all. +- Options of *string* type containing IPs or CIDRs (e.g. `dial` in `upstream` of `proxy` handler), regular expressions (e.g. `cookie_hash_regexp` of `rdp` matcher), or special values (e.g. `commands` and `credentials` of `socks5` handler) support runtime placeholders, but they are evaluated __once at provision__ due to the existing optimizations. +- Other options of *string* type (e.g. `alpn` of `tls` matcher) generally support runtime placeholders, and they are evaluated __each time at match or handle__. However, there are some exceptions, e.g. `tls_*` options inside `upstream` of `proxy` handler, and all options inside `connection_policy` of `tls` handler, that don't support runtime placeholders at all. + +Please note that runtime placeholders support depends on handler/matcher implementations. Given some matchers and handlers are outside of this repository, it's up to their developers to support or restrict usage of runtime placeholders. diff --git a/layer4/matchers.go b/layer4/matchers.go index 8409cf1..b6e430d 100644 --- a/layer4/matchers.go +++ b/layer4/matchers.go @@ -19,10 +19,10 @@ import ( "fmt" "net" "net/netip" - "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" ) @@ -125,10 +125,15 @@ func (*MatchRemoteIP) CaddyModule() caddy.ModuleInfo { } // Provision parses m's IP ranges, either from IP or CIDR expressions. -func (m *MatchRemoteIP) Provision(_ caddy.Context) (err error) { - m.cidrs, err = ParseNetworks(m.Ranges) - if err != nil { - return err +func (m *MatchRemoteIP) Provision(_ caddy.Context) error { + repl := caddy.NewReplacer() + for _, addrOrCIDR := range m.Ranges { + addrOrCIDR = repl.ReplaceAll(addrOrCIDR, "") + prefix, err := caddyhttp.CIDRExpressionToPrefix(addrOrCIDR) + if err != nil { + return err + } + m.cidrs = append(m.cidrs, prefix) } return nil } @@ -173,13 +178,13 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } - prefixes, err := ParseNetworks(d.RemainingArgs()) - if err != nil { - return err - } - - for _, prefix := range prefixes { - m.Ranges = append(m.Ranges, prefix.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) + continue + } + m.Ranges = append(m.Ranges, val) } // No blocks are supported @@ -207,11 +212,15 @@ func (*MatchLocalIP) CaddyModule() caddy.ModuleInfo { // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchLocalIP) Provision(_ caddy.Context) error { - ipnets, err := ParseNetworks(m.Ranges) - if err != nil { - return err + repl := caddy.NewReplacer() + for _, addrOrCIDR := range m.Ranges { + addrOrCIDR = repl.ReplaceAll(addrOrCIDR, "") + prefix, err := caddyhttp.CIDRExpressionToPrefix(addrOrCIDR) + if err != nil { + return err + } + m.cidrs = append(m.cidrs, prefix) } - m.cidrs = ipnets return nil } @@ -255,13 +264,13 @@ func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } - prefixes, err := ParseNetworks(d.RemainingArgs()) - if err != nil { - return err - } - - for _, prefix := range prefixes { - m.Ranges = append(m.Ranges, prefix.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) + continue + } + m.Ranges = append(m.Ranges, val) } // No blocks are supported @@ -393,29 +402,3 @@ var ( _ ConnMatcher = (*MatchNot)(nil) _ caddyfile.Unmarshaler = (*MatchNot)(nil) ) - -// ParseNetworks parses a list of string IP addresses or CIDR subnets into a slice of net.IPNet's. -// It accepts for example ["127.0.0.1", "127.0.0.0/8", "::1", "2001:db8::/32"]. -func ParseNetworks(networks []string) (ipNets []netip.Prefix, err error) { - for _, str := range networks { - if strings.Contains(str, "/") { - ipNet, err := netip.ParsePrefix(str) - if err != nil { - return nil, fmt.Errorf("parsing CIDR expression: %v", err) - } - ipNets = append(ipNets, ipNet) - continue - } - - addr, err := netip.ParseAddr(str) - if err != nil { - return nil, err - } - bits := 32 - if addr.Is6() { - bits = 128 - } - ipNets = append(ipNets, netip.PrefixFrom(addr, bits)) - } - return ipNets, nil -} diff --git a/layer4/server.go b/layer4/server.go index 092f501..4cb8dd1 100644 --- a/layer4/server.go +++ b/layer4/server.go @@ -56,7 +56,9 @@ func (s *Server) Provision(ctx caddy.Context, logger *zap.Logger) error { s.MatchingTimeout = caddy.Duration(MatchingTimeoutDefault) } + repl := caddy.NewReplacer() for i, address := range s.Listen { + address = repl.ReplaceAll(address, "") addr, err := caddy.ParseNetworkAddress(address) if err != nil { return fmt.Errorf("parsing listener address '%s' in position %d: %v", address, i, err) @@ -182,7 +184,7 @@ func (s *Server) handle(conn net.Conn) { // UnmarshalCaddyfile sets up the Server from Caddyfile tokens. Syntax: // -// { +// [] { // matching_timeout // @a [] // @b { @@ -205,11 +207,7 @@ func (s *Server) handle(conn net.Conn) { func (s *Server) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Wrapper name and all same-line options are treated as network addresses for ok := true; ok; ok = d.NextArg() { - addr := d.Val() - if _, err := caddy.ParseNetworkAddress(addr); err != nil { - return d.Errf("parsing network address '%s': %v", addr, err) - } - s.Listen = append(s.Listen, addr) + s.Listen = append(s.Listen, d.Val()) } if err := ParseCaddyfileNestedRoutes(d, &s.Routes, &s.MatchingTimeout); err != nil { diff --git a/modules/l4proxy/proxy.go b/modules/l4proxy/proxy.go index f1a8e50..86151e4 100644 --- a/modules/l4proxy/proxy.go +++ b/modules/l4proxy/proxy.go @@ -57,6 +57,8 @@ type Handler struct { // Ref: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt ProxyProtocol string `json:"proxy_protocol,omitempty"` + proxyProtocolVersion uint8 + ctx caddy.Context logger *zap.Logger } @@ -83,8 +85,14 @@ func (h *Handler) Provision(ctx caddy.Context) error { h.LoadBalancing.SelectionPolicy = mod.(Selector) } - if h.ProxyProtocol != "" && h.ProxyProtocol != "v1" && h.ProxyProtocol != "v2" { - return fmt.Errorf("proxy_protocol: \"%s\" should be empty, or one of \"v1\" \"v2\"", h.ProxyProtocol) + repl := caddy.NewReplacer() + proxyProtocol := repl.ReplaceAll(h.ProxyProtocol, "") + if proxyProtocol == "v1" { + h.proxyProtocolVersion = 1 + } else if proxyProtocol == "v2" { + h.proxyProtocolVersion = 2 + } else if proxyProtocol != "" { + return fmt.Errorf("proxy_protocol: \"%s\" should be empty, or one of \"v1\" \"v2\"", proxyProtocol) } // prepare upstreams @@ -220,12 +228,12 @@ func (h *Handler) dialPeers(upstream *Upstream, repl *caddy.Replacer, down *laye // Send the PROXY protocol header. if err == nil { downConn := l4proxyprotocol.GetConn(down) - switch h.ProxyProtocol { - case "v1": + switch h.proxyProtocolVersion { + case 1: var h proxyprotocol.HeaderV1 h.FromConn(downConn, false) _, err = h.WriteTo(up) - case "v2": + case 2: var h proxyprotocol.HeaderV2 h.FromConn(downConn, false) _, err = h.WriteTo(up) @@ -401,13 +409,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { _, wrapper := d.Next(), d.Val() // consume wrapper name // Treat all same-line options as upstream addresses - for i := 0; d.NextArg(); i++ { - val := d.Val() - _, err := caddy.ParseNetworkAddress(val) - if err != nil { - return d.Errf("parsing %s upstream on position %d: %v", wrapper, i, err) - } - h.Upstreams = append(h.Upstreams, &Upstream{Dial: []string{val}}) + for d.NextArg() { + h.Upstreams = append(h.Upstreams, &Upstream{Dial: []string{d.Val()}}) } var ( @@ -591,13 +594,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Errf("duplicate %s option '%s'", wrapper, optionName) } _, h.ProxyProtocol, hasProxyProtocol = d.NextArg(), d.Val(), true - switch h.ProxyProtocol { - case "v1", "v2": - continue - default: - return d.Errf("malformed %s option '%s': unrecognized value '%s'", - wrapper, optionName, h.ProxyProtocol) - } case "upstream": u := &Upstream{} if err := u.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil { diff --git a/modules/l4proxy/upstream.go b/modules/l4proxy/upstream.go index 63dc7cf..a09f9ab 100644 --- a/modules/l4proxy/upstream.go +++ b/modules/l4proxy/upstream.go @@ -56,19 +56,23 @@ func (u *Upstream) String() string { } func (u *Upstream) provision(ctx caddy.Context, h *Handler) error { + repl := caddy.NewReplacer() for _, dialAddr := range u.Dial { + // replace runtime placeholders + replDialAddr := repl.ReplaceAll(dialAddr, "") + // parse and validate address - addr, err := caddy.ParseNetworkAddress(dialAddr) + addr, err := caddy.ParseNetworkAddress(replDialAddr) if err != nil { return err } if addr.PortRangeSize() != 1 { - return fmt.Errorf("%s: port ranges not currently supported", dialAddr) + return fmt.Errorf("%s: port ranges not currently supported", replDialAddr) } // create or load peer info p := &peer{address: addr} - existingPeer, loaded := peers.LoadOrStore(dialAddr, p) + existingPeer, loaded := peers.LoadOrStore(dialAddr, p) // peers are deleted in Handler.Cleanup if loaded { p = existingPeer.(*peer) } @@ -368,13 +372,7 @@ func (u *Upstream) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if len(shortcutArgs) == 0 { return d.Errf("malformed %s block: at least one %s address must be provided", wrapper, shortcutOptionName) } - for _, arg := range shortcutArgs { - _, err := caddy.ParseNetworkAddress(arg) - if err != nil { - return d.Errf("parsing %s option '%s': %v", wrapper, shortcutOptionName, err) - } - u.Dial = append(u.Dial, arg) - } + u.Dial = append(u.Dial, shortcutArgs...) return nil } diff --git a/modules/l4proxyprotocol/handler.go b/modules/l4proxyprotocol/handler.go index aa3242a..c021002 100644 --- a/modules/l4proxyprotocol/handler.go +++ b/modules/l4proxyprotocol/handler.go @@ -22,6 +22,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/mastercactapus/proxyprotocol" "go.uber.org/zap" @@ -55,10 +56,12 @@ func (*Handler) CaddyModule() caddy.ModuleInfo { // Provision sets up the module. func (h *Handler) Provision(ctx caddy.Context) error { - for _, s := range h.Allow { - _, n, err := net.ParseCIDR(s) + repl := caddy.NewReplacer() + for _, allowCIDR := range h.Allow { + allowCIDR = repl.ReplaceAll(allowCIDR, "") + _, n, err := net.ParseCIDR(allowCIDR) if err != nil { - return fmt.Errorf("invalid subnet '%s': %w", s, err) + return fmt.Errorf("invalid subnet '%s': %w", allowCIDR, err) } h.rules = append(h.rules, proxyprotocol.Rule{Timeout: time.Duration(h.Timeout), Subnet: n}) } @@ -190,12 +193,13 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.CountRemainingArgs() == 0 { return d.ArgErr() } - prefixes, err := layer4.ParseNetworks(d.RemainingArgs()) - if err != nil { - return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err) - } - for _, prefix := range prefixes { - h.Allow = append(h.Allow, prefix.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + h.Allow = append(h.Allow, caddyhttp.PrivateRangesCIDR()...) + continue + } + h.Allow = append(h.Allow, val) } case "timeout": if hasTimeout { diff --git a/modules/l4rdp/matcher.go b/modules/l4rdp/matcher.go index 5f99ec1..2cb2708 100644 --- a/modules/l4rdp/matcher.go +++ b/modules/l4rdp/matcher.go @@ -27,7 +27,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/mholt/caddy-l4/layer4" ) @@ -60,6 +60,13 @@ func (m *MatchRDP) CaddyModule() caddy.ModuleInfo { // Match returns true if the connection looks like RDP. func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { + // Replace placeholders in filters + repl := cx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + cookieHash := repl.ReplaceAll(m.CookieHash, "") + cookieHash = cookieHash[:min(RDPCookieHashBytesMax, uint16(len(cookieHash)))] + customInfo := repl.ReplaceAll(m.CustomInfo, "") + customInfo = customInfo[:min(RDPCustomInfoBytesMax, uint16(len(customInfo)))] + // Read a number of bytes to parse headers headerBuf := make([]byte, RDPConnReqBytesMin) n, err := io.ReadFull(cx, headerBuf) @@ -146,11 +153,10 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { hash := c[hashBytesStart : hashBytesStart+hashBytesTotal] // Add hash to the replacer - repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer) repl.Set("l4.rdp.cookie_hash", hash) // Full match - if len(m.CookieHash) > 0 && m.CookieHash != hash { + if len(cookieHash) > 0 && cookieHash != hash { break } @@ -164,7 +170,7 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { } // NOTE: we can stop validation because hash hasn't matched - if !hasValidCookie && (len(m.CookieHash) > 0 || len(m.CookieHashRegexp) > 0) { + if !hasValidCookie && (len(cookieHash) > 0 || len(m.CookieHashRegexp) > 0) { return false, nil } @@ -247,7 +253,6 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { } // Add IP and port to the replacer - repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer) repl.Set("l4.rdp.cookie_ip", ipVal.String()) repl.Set("l4.rdp.cookie_port", strconv.Itoa(int(portVal))) @@ -306,11 +311,10 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { info := c[RDPCustomInfoBytesStart : RDPCustomInfoBytesStart+infoBytesTotal] // Add info to the replacer - repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer) repl.Set("l4.rdp.custom_info", info) // Full match - if len(m.CustomInfo) > 0 && m.CustomInfo != info { + if len(customInfo) > 0 && customInfo != info { break } @@ -324,7 +328,7 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { } // NOTE: we can stop validation because info hasn't matched - if !hasValidCustom && (len(m.CustomInfo) > 0 || len(m.CustomInfoRegexp) > 0) { + if !hasValidCustom && (len(customInfo) > 0 || len(m.CustomInfoRegexp) > 0) { return false, nil } @@ -403,7 +407,6 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { } // Add base64 of identity bytes to the replacer - repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer) repl.Set("l4.rdp.correlation_id", base64.StdEncoding.EncodeToString(i.Identity[:])) // Validate RDPCorrInfo (3/3) @@ -419,15 +422,20 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) { // Provision parses m's IP ranges, either from IP or CIDR expressions, and regular expressions. func (m *MatchRDP) Provision(_ caddy.Context) (err error) { - m.cookieIPs, err = layer4.ParseNetworks(m.CookieIPs) - if err != nil { - return err + repl := caddy.NewReplacer() + for _, cookieAddrOrCIDR := range m.CookieIPs { + cookieAddrOrCIDR = repl.ReplaceAll(cookieAddrOrCIDR, "") + prefix, err := caddyhttp.CIDRExpressionToPrefix(cookieAddrOrCIDR) + if err != nil { + return err + } + m.cookieIPs = append(m.cookieIPs, prefix) } - m.cookieHashRegexp, err = regexp.Compile(m.CookieHashRegexp) + m.cookieHashRegexp, err = regexp.Compile(repl.ReplaceAll(m.CookieHashRegexp, "")) if err != nil { return err } - m.customInfoRegexp, err = regexp.Compile(m.CustomInfoRegexp) + m.customInfoRegexp, err = regexp.Compile(repl.ReplaceAll(m.CustomInfoRegexp, "")) if err != nil { return err } @@ -488,7 +496,7 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } _, val := d.NextArg(), d.Val() - m.CookieHash, hasCookieHash = val[:min(RDPCookieHashBytesMax, uint16(len(val)))], true + m.CookieHash, hasCookieHash = val, true case "cookie_hash_regexp": if hasCookieIPOrPort || hasCustomInfo { return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName) @@ -500,7 +508,7 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } _, val := d.NextArg(), d.Val() - m.CookieHashRegexp, hasCookieHash = val[:min(RDPCookieHashBytesMax, uint16(len(val)))], true + m.CookieHashRegexp, hasCookieHash = val, true case "cookie_ip": if hasCookieHash || hasCustomInfo { return d.Errf("%s option '%s' can only be combined with 'cookie_port' option", wrapper, optionName) @@ -508,12 +516,13 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.CountRemainingArgs() == 0 { return d.ArgErr() } - prefixes, err := layer4.ParseNetworks(d.RemainingArgs()) - if err != nil { - return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err) - } - for _, prefix := range prefixes { - m.CookieIPs = append(m.CookieIPs, prefix.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.CookieIPs = append(m.CookieIPs, caddyhttp.PrivateRangesCIDR()...) + continue + } + m.CookieIPs = append(m.CookieIPs, val) } hasCookieIPOrPort = true case "cookie_port": @@ -543,7 +552,7 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } _, val := d.NextArg(), d.Val() - m.CustomInfo, hasCustomInfo = val[:min(RDPCustomInfoBytesMax, uint16(len(val)))], true + m.CustomInfo, hasCustomInfo = val, true case "custom_info_regexp": if hasCookieHash || hasCookieIPOrPort { return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName) @@ -555,7 +564,7 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } _, val := d.NextArg(), d.Val() - m.CustomInfoRegexp, hasCustomInfo = val[:min(RDPCustomInfoBytesMax, uint16(len(val)))], true + m.CustomInfoRegexp, hasCustomInfo = val, true default: return d.ArgErr() } diff --git a/modules/l4regexp/matcher.go b/modules/l4regexp/matcher.go index 4bf0576..5d84595 100644 --- a/modules/l4regexp/matcher.go +++ b/modules/l4regexp/matcher.go @@ -59,10 +59,11 @@ func (m *MatchRegexp) Match(cx *layer4.Connection) (bool, error) { // Provision parses m's regular expression and sets m's minimum read bytes count. func (m *MatchRegexp) Provision(_ caddy.Context) (err error) { + repl := caddy.NewReplacer() if m.Count == 0 { m.Count = minCount } - m.compiled, err = regexp.Compile(m.Pattern) + m.compiled, err = regexp.Compile(repl.ReplaceAll(m.Pattern, "")) if err != nil { return err } diff --git a/modules/l4socks/socks4_matcher.go b/modules/l4socks/socks4_matcher.go index f869f3c..7952366 100644 --- a/modules/l4socks/socks4_matcher.go +++ b/modules/l4socks/socks4_matcher.go @@ -10,7 +10,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/mholt/caddy-l4/layer4" ) @@ -41,12 +41,13 @@ func (*Socks4Matcher) CaddyModule() caddy.ModuleInfo { } } -func (m *Socks4Matcher) Provision(_ caddy.Context) (err error) { +func (m *Socks4Matcher) Provision(_ caddy.Context) error { if len(m.Commands) == 0 { m.commands = []uint8{1, 2} // CONNECT & BIND } else { + repl := caddy.NewReplacer() for _, c := range m.Commands { - switch strings.ToUpper(c) { + switch strings.ToUpper(repl.ReplaceAll(c, "")) { case "CONNECT": m.commands = append(m.commands, 1) case "BIND": @@ -56,9 +57,14 @@ func (m *Socks4Matcher) Provision(_ caddy.Context) (err error) { } } } - m.cidrs, err = layer4.ParseNetworks(m.Networks) - if err != nil { - return err + repl := caddy.NewReplacer() + for _, networkAddrOrCIDR := range m.Networks { + networkAddrOrCIDR = repl.ReplaceAll(networkAddrOrCIDR, "") + prefix, err := caddyhttp.CIDRExpressionToPrefix(networkAddrOrCIDR) + if err != nil { + return err + } + m.cidrs = append(m.cidrs, prefix) } return nil } @@ -149,12 +155,13 @@ func (m *Socks4Matcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.CountRemainingArgs() == 0 { return d.ArgErr() } - cidrs, err := layer4.ParseNetworks(d.RemainingArgs()) - if err != nil { - return err - } - for _, cidr := range cidrs { - m.Networks = append(m.Networks, cidr.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.Networks = append(m.Networks, caddyhttp.PrivateRangesCIDR()...) + continue + } + m.Networks = append(m.Networks, val) } case "ports": if d.CountRemainingArgs() == 0 { diff --git a/modules/l4socks/socks5_handler.go b/modules/l4socks/socks5_handler.go index 3c9bb9f..647039e 100644 --- a/modules/l4socks/socks5_handler.go +++ b/modules/l4socks/socks5_handler.go @@ -37,6 +37,8 @@ func (*Socks5Handler) CaddyModule() caddy.ModuleInfo { } func (h *Socks5Handler) Provision(ctx caddy.Context) error { + repl := caddy.NewReplacer() + rule := &socks5.PermitCommand{EnableConnect: false, EnableAssociate: false, EnableBind: false} if len(h.Commands) == 0 { rule.EnableConnect = true @@ -44,7 +46,7 @@ func (h *Socks5Handler) Provision(ctx caddy.Context) error { // BIND is currently not supported, so we don't allow it by default } else { for _, c := range h.Commands { - switch strings.ToUpper(c) { + switch strings.ToUpper(repl.ReplaceAll(c, "")) { case "CONNECT": rule.EnableConnect = true case "ASSOCIATE": @@ -57,11 +59,19 @@ func (h *Socks5Handler) Provision(ctx caddy.Context) error { } } + credentials := make(map[string]string, len(h.Credentials)) + for k, v := range h.Credentials { + k, v = repl.ReplaceAll(k, ""), repl.ReplaceAll(v, "") + if len(k) > 0 { + credentials[k] = v + } + } + authMethods := []socks5.Authenticator{socks5.NoAuthAuthenticator{}} if len(h.Credentials) > 0 { authMethods = []socks5.Authenticator{ socks5.UserPassAuthenticator{ - Credentials: socks5.StaticCredentials(h.Credentials), + Credentials: socks5.StaticCredentials(credentials), }, } } @@ -69,7 +79,7 @@ func (h *Socks5Handler) Provision(ctx caddy.Context) error { h.server = socks5.NewServer( socks5.WithLogger(&socks5Logger{l: ctx.Logger(h)}), socks5.WithRule(rule), - socks5.WithBindIP(net.ParseIP(h.BindIP)), + socks5.WithBindIP(net.ParseIP(caddy.NewReplacer().ReplaceAll(h.BindIP, ""))), socks5.WithAuthMethods(authMethods), ) @@ -84,8 +94,8 @@ func (h *Socks5Handler) Handle(cx *layer4.Connection, _ layer4.Handler) error { // UnmarshalCaddyfile sets up the Socks5Handler from Caddyfile tokens. Syntax: // // socks5 { -// bind_ip <...> -// commands <...> +// bind_ip
+// commands // credentials [ ] // } // @@ -110,11 +120,7 @@ func (h *Socks5Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.CountRemainingArgs() != 1 { return d.ArgErr() } - _, bindIP := d.NextArg(), net.ParseIP(d.Val()) - if bindIP == nil { - return d.Errf("parsing %s option '%s': invalid IP address", wrapper, optionName) - } - h.BindIP, hasBindIP = bindIP.String(), true + _, h.BindIP, hasBindIP = d.NextArg(), d.Val(), true case "commands": if d.CountRemainingArgs() == 0 { return d.ArgErr() diff --git a/modules/l4tls/alpn_matcher.go b/modules/l4tls/alpn_matcher.go index 3ae55e5..e5ea95f 100644 --- a/modules/l4tls/alpn_matcher.go +++ b/modules/l4tls/alpn_matcher.go @@ -37,8 +37,10 @@ func (*MatchALPN) CaddyModule() caddy.ModuleInfo { } func (m *MatchALPN) Match(hello *tls.ClientHelloInfo) bool { + repl := hello.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) clientProtocols := hello.SupportedProtos for _, alpn := range *m { + alpn = repl.ReplaceAll(alpn, "") for _, clientProtocol := range clientProtocols { if alpn == clientProtocol { return true diff --git a/modules/l4tls/matcher.go b/modules/l4tls/matcher.go index ca12a26..7244f07 100644 --- a/modules/l4tls/matcher.go +++ b/modules/l4tls/matcher.go @@ -21,6 +21,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" "go.uber.org/zap" @@ -234,12 +235,13 @@ func unmarshalCaddyfileMatchLocalIP(d *caddyfile.Dispenser) (*caddytls.MatchLoca return nil, d.ArgErr() } - prefixes, err := layer4.ParseNetworks(d.RemainingArgs()) - if err != nil { - return nil, err - } - for _, prefix := range prefixes { - m.Ranges = append(m.Ranges, prefix.String()) + for d.NextArg() { + val := d.Val() + if val == "private_ranges" { + m.Ranges = append(m.Ranges, caddyhttp.PrivateRangesCIDR()...) + continue + } + m.Ranges = append(m.Ranges, val) } // No blocks are supported @@ -268,32 +270,23 @@ func unmarshalCaddyfileMatchRemoteIP(d *caddyfile.Dispenser) (*caddytls.MatchRem return nil, d.ArgErr() } - rangesRaw, notRangesRaw := make([]string, 0, d.CountRemainingArgs()), make([]string, 0, d.CountRemainingArgs()) for d.NextArg() { val := d.Val() + var exclamation bool if len(val) > 1 && val[0] == '!' { - notRangesRaw = append(notRangesRaw, val[1:]) + exclamation, val = true, val[1:] + } + ranges := []string{val} + if val == "private_ranges" { + ranges = caddyhttp.PrivateRangesCIDR() + } + if exclamation { + m.NotRanges = append(m.NotRanges, ranges...) } else { - rangesRaw = append(rangesRaw, val) + m.Ranges = append(m.Ranges, ranges...) } } - prefixes, err := layer4.ParseNetworks(rangesRaw) - if err != nil { - return nil, err - } - for _, prefix := range prefixes { - m.Ranges = append(m.Ranges, prefix.String()) - } - - notPrefixes, err := layer4.ParseNetworks(notRangesRaw) - if err != nil { - return nil, err - } - for _, notPrefix := range notPrefixes { - m.NotRanges = append(m.NotRanges, notPrefix.String()) - } - // No blocks are supported if d.NextBlock(d.Nesting()) { return nil, d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)