Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime placeholders #224

Merged
merged 5 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,14 @@ While only allowing connections from a specific network and requiring a username
}
```
</details>

## 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.
81 changes: 32 additions & 49 deletions layer4/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this function should move to caddy instead of caddyhttp (since it can be used for more than just HTTP).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed the PR back at the caddy repo but I still wonder about this. What's the right package for this, do we think?

if err != nil {
return err
}
m.cidrs = append(m.cidrs, prefix)
}
return nil
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
10 changes: 4 additions & 6 deletions layer4/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -182,7 +184,7 @@ func (s *Server) handle(conn net.Conn) {

// UnmarshalCaddyfile sets up the Server from Caddyfile tokens. Syntax:
//
// <addresses> {
// <address:port> [<address:port>] {
// matching_timeout <duration>
// @a <matcher> [<matcher_args>]
// @b {
Expand All @@ -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 {
Expand Down
34 changes: 15 additions & 19 deletions modules/l4proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand 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 (
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 8 additions & 10 deletions modules/l4proxy/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 13 additions & 9 deletions modules/l4proxyprotocol/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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})
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading