diff --git a/modules/l4proxy/healthchecks.go b/modules/l4proxy/healthchecks.go index 3f0c2c2..a9d09c5 100644 --- a/modules/l4proxy/healthchecks.go +++ b/modules/l4proxy/healthchecks.go @@ -130,6 +130,9 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { // fails. func (h *Handler) doActiveHealthCheck(p *peer) error { addr := p.address + if addr == nil { + return nil + } // adjust the port, if configured to be different if h.HealthChecks.Active.Port > 0 { diff --git a/modules/l4proxy/proxy.go b/modules/l4proxy/proxy.go index 86151e4..c2e18e8 100644 --- a/modules/l4proxy/proxy.go +++ b/modules/l4proxy/proxy.go @@ -200,13 +200,21 @@ func (h *Handler) dialPeers(upstream *Upstream, repl *caddy.Replacer, down *laye var upConns []net.Conn for _, p := range upstream.peers { - hostPort := repl.ReplaceAll(p.address.JoinHostPort(0), "") + hostPort := repl.ReplaceAll(p.dialAddr, "") + addr := p.address var up net.Conn var err error + if addr == nil { + addr, err = ParseAddress(hostPort) + if err != nil { + return nil, err + } + } + if upstream.TLS == nil { - up, err = net.Dial(p.address.Network, hostPort) + up, err = net.Dial(addr.Network, hostPort) } else { // the prepared config could be nil if user enabled but did not customize TLS, // in which case we adopt the downstream client's TLS ClientHello for ours; @@ -218,7 +226,7 @@ func (h *Handler) dialPeers(upstream *Upstream, repl *caddy.Replacer, down *laye hellos[0].FillTLSClientConfig(tlsCfg) } } - up, err = tls.Dial(p.address.Network, hostPort, tlsCfg) + up, err = tls.Dial(addr.Network, hostPort, tlsCfg) } h.logger.Debug("dial upstream", zap.String("remote", down.RemoteAddr().String()), @@ -346,7 +354,7 @@ func (h *Handler) countFailure(p *peer) { err := p.countFail(1) if err != nil { h.HealthChecks.Passive.logger.Error("could not count failure", - zap.String("peer_address", p.address.String()), + zap.String("peer_address", p.dialAddr), zap.Error(err)) return } @@ -362,7 +370,7 @@ func (h *Handler) countFailure(p *peer) { err := p.countFail(-1) if err != nil { h.HealthChecks.Passive.logger.Error("could not forget failure", - zap.String("peer_address", p.address.String()), + zap.String("peer_address", p.dialAddr), zap.Error(err)) } }(failDuration) diff --git a/modules/l4proxy/upstream.go b/modules/l4proxy/upstream.go index c4f1255..8ea5749 100644 --- a/modules/l4proxy/upstream.go +++ b/modules/l4proxy/upstream.go @@ -30,12 +30,23 @@ import ( "github.com/mholt/caddy-l4/layer4" ) +func ParseAddress(addr string) (*caddy.NetworkAddress, error) { + address, err := caddy.ParseNetworkAddress(addr) + if err != nil { + return nil, err + } + if address.PortRangeSize() != 1 { + return nil, fmt.Errorf("%s: port ranges not currently supported", addr) + } + return &address, nil +} + // UpstreamPool is a collection of upstreams. type UpstreamPool []*Upstream // Upstream represents a proxy upstream. type Upstream struct { - // The network addresses to dial. Supports placeholders, but not port + // The network addresses to dial. Supports placeholders // ranges currently (each address must be exactly 1 socket). Dial []string `json:"dial,omitempty"` @@ -46,6 +57,12 @@ type Upstream struct { // have before being marked as unhealthy (if > 0). MaxConnections int `json:"max_connections,omitempty"` + // Is dial a dynamic upstream + // If true, skip address parsing in Upstream.provision + // then parse the address after replacing all placeholders in Handler.dialPeers. + // (support dynamic port) + Dynamic bool `json:"dynamic,omitempty"` + peers []*peer tlsConfig *tls.Config healthCheckPolicy *PassiveHealthChecks @@ -63,17 +80,17 @@ func (u *Upstream) provision(ctx caddy.Context, h *Handler) error { // in Handler.dialPeers. E.g. {l4.tls.server_name}:443 will allow for dynamic TLS SNI based upstreams. replDialAddr := repl.ReplaceKnown(dialAddr, "") - // parse and validate address - addr, err := caddy.ParseNetworkAddress(replDialAddr) - if err != nil { - return err - } - if addr.PortRangeSize() != 1 { - return fmt.Errorf("%s: port ranges not currently supported", replDialAddr) + // create or load peer info + p := &peer{dialAddr: replDialAddr} + // parse and validate address if upstream not dynamic + if !u.Dynamic { + address, err := ParseAddress(p.dialAddr) + if err != nil { + return err + } + p.address = address } - // create or load peer info - p := &peer{address: addr} existingPeer, loaded := peers.LoadOrStore(dialAddr, p) // peers are deleted in Handler.Cleanup if loaded { p = existingPeer.(*peer) @@ -171,6 +188,7 @@ func (u *Upstream) totalConns() int { // upstream [] { // dial [] // max_connections +// dynamic // // tls // tls_client_auth | @@ -198,6 +216,7 @@ func (u *Upstream) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { hasTLSTrustPool, hasTLSClientAuth bool hasTLSInsecureSkipVerify, hasTLSTimeout bool hasTLSRenegotiation, hasTLSServerName bool + hasDynamic bool ) for nesting := d.Nesting(); d.NextBlock(nesting); { optionName := d.Val() @@ -360,6 +379,11 @@ func (u *Upstream) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { u.TLS = &reverseproxy.TLSConfig{} } u.TLS.RootCAPool = append(u.TLS.RootCAPool, d.RemainingArgs()...) + case "dynamic": + if hasDynamic { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + u.Dynamic, hasDynamic = true, true default: return d.ArgErr() } @@ -387,7 +411,8 @@ type peer struct { numConns int32 unhealthy int32 fails int32 - address caddy.NetworkAddress + address *caddy.NetworkAddress + dialAddr string } // getNumConns returns the number of active connections with the peer.