github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4proxy/upstream.go (about) 1 // Copyright 2020 Matthew Holt 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package l4proxy 16 17 import ( 18 "crypto/tls" 19 "fmt" 20 "strconv" 21 "strings" 22 "sync/atomic" 23 24 "github.com/caddyserver/caddy/v2" 25 "github.com/caddyserver/caddy/v2/caddyconfig" 26 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 27 "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" 28 "github.com/caddyserver/caddy/v2/modules/caddytls" 29 30 "github.com/mholt/caddy-l4/layer4" 31 ) 32 33 // UpstreamPool is a collection of upstreams. 34 type UpstreamPool []*Upstream 35 36 // Upstream represents a proxy upstream. 37 type Upstream struct { 38 // The network addresses to dial. Supports placeholders, but not port 39 // ranges currently (each address must be exactly 1 socket). 40 Dial []string `json:"dial,omitempty"` 41 42 // Set this field to enable TLS to the upstream. 43 TLS *reverseproxy.TLSConfig `json:"tls,omitempty"` 44 45 // How many connections this upstream is allowed to 46 // have before being marked as unhealthy (if > 0). 47 MaxConnections int `json:"max_connections,omitempty"` 48 49 peers []*peer 50 tlsConfig *tls.Config 51 healthCheckPolicy *PassiveHealthChecks 52 } 53 54 func (u *Upstream) String() string { 55 return strings.Join(u.Dial, ",") 56 } 57 58 func (u *Upstream) provision(ctx caddy.Context, h *Handler) error { 59 repl := caddy.NewReplacer() 60 for _, dialAddr := range u.Dial { 61 // replace runtime placeholders 62 // Note: ReplaceKnown is used here instead of ReplaceAll to let unknown placeholders be replaced later 63 // in Handler.dialPeers. E.g. {l4.tls.server_name}:443 will allow for dynamic TLS SNI based upstreams. 64 replDialAddr := repl.ReplaceKnown(dialAddr, "") 65 66 // parse and validate address 67 addr, err := caddy.ParseNetworkAddress(replDialAddr) 68 if err != nil { 69 return err 70 } 71 if addr.PortRangeSize() != 1 { 72 return fmt.Errorf("%s: port ranges not currently supported", replDialAddr) 73 } 74 75 // create or load peer info 76 p := &peer{address: addr} 77 existingPeer, loaded := peers.LoadOrStore(dialAddr, p) // peers are deleted in Handler.Cleanup 78 if loaded { 79 p = existingPeer.(*peer) 80 } 81 u.peers = append(u.peers, p) 82 } 83 84 // set up TLS client 85 if u.TLS != nil { 86 var err error 87 u.tlsConfig, err = u.TLS.MakeTLSClientConfig(ctx) 88 if err != nil { 89 return fmt.Errorf("making TLS client config: %v", err) 90 } 91 } 92 93 // if the passive health checker has a non-zero UnhealthyConnectionCount 94 // but the upstream has no MaxConnections set (they are the same thing, 95 // but the passive health checker is a default value for upstreams 96 // without MaxConnections), copy the value into this upstream, since the 97 // value in the upstream (MaxConnections) is what is used during 98 // availability checks 99 if h.HealthChecks != nil && h.HealthChecks.Passive != nil { 100 h.HealthChecks.Passive.logger = h.logger.Named("health_checker.passive") 101 if h.HealthChecks.Passive.UnhealthyConnectionCount > 0 && 102 u.MaxConnections == 0 { 103 u.MaxConnections = h.HealthChecks.Passive.UnhealthyConnectionCount 104 } 105 } 106 107 // upstreams need independent access to the passive 108 // health check policy because passive health checks 109 // run without access to h. 110 if h.HealthChecks != nil { 111 u.healthCheckPolicy = h.HealthChecks.Passive 112 } 113 114 return nil 115 } 116 117 // available returns true if the remote host 118 // is available to receive connections. This is 119 // the method that should be used by selection 120 // policies, etc. to determine if a backend 121 // is usable at the moment. 122 func (u *Upstream) available() bool { 123 return u.healthy() && !u.full() 124 } 125 126 // healthy returns true if the remote host 127 // is currently known to be healthy or "up". 128 // It consults the circuit breaker, if any. 129 func (u *Upstream) healthy() bool { 130 for _, p := range u.peers { 131 if !p.healthy() { 132 return false 133 } 134 } 135 if u.healthCheckPolicy != nil && u.healthCheckPolicy.MaxFails > 0 { 136 for _, p := range u.peers { 137 if atomic.LoadInt32(&p.fails) >= int32(u.healthCheckPolicy.MaxFails) { 138 return false 139 } 140 } 141 } 142 return true 143 } 144 145 // full returns true if any of the peers cannot 146 // receive more connections at this time. 147 func (u *Upstream) full() bool { 148 if u.MaxConnections == 0 { 149 return false 150 } 151 for _, p := range u.peers { 152 if p.getNumConns() >= u.MaxConnections { 153 return true 154 } 155 } 156 return false 157 } 158 159 // totalConns returns the total number of active connections 160 // to this upstream (across all peers). 161 func (u *Upstream) totalConns() int { 162 var totalConns int 163 for _, p := range u.peers { 164 totalConns += p.getNumConns() 165 } 166 return totalConns 167 } 168 169 // UnmarshalCaddyfile sets up the Upstream from Caddyfile tokens. Syntax: 170 // 171 // upstream [<address:port>] { 172 // dial <address:port> [<address:port>] 173 // max_connections <int> 174 // 175 // tls 176 // tls_client_auth <automate_name> | <cert_file> <key_file> 177 // tls_curves <curves...> 178 // tls_except_ports <ports...> 179 // tls_insecure_skip_verify 180 // tls_renegotiation <never|once|freely> 181 // tls_server_name <name> 182 // tls_timeout <duration> 183 // tls_trust_pool <module> 184 // 185 // # DEPRECATED: 186 // tls_trusted_ca_certs <certificates...> 187 // tls_trusted_ca_pool <certificates...> 188 // } 189 // upstream <address:port> 190 func (u *Upstream) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 191 _, wrapper := d.Next(), "proxy "+d.Val() // consume wrapper name 192 193 // Treat all same-line options as dial arguments 194 shortcutArgs := d.RemainingArgs() 195 196 var ( 197 hasMaxConnections, hasTLS bool 198 hasTLSTrustPool, hasTLSClientAuth bool 199 hasTLSInsecureSkipVerify, hasTLSTimeout bool 200 hasTLSRenegotiation, hasTLSServerName bool 201 ) 202 for nesting := d.Nesting(); d.NextBlock(nesting); { 203 optionName := d.Val() 204 switch optionName { 205 case "dial": 206 if d.CountRemainingArgs() == 0 { 207 return d.ArgErr() 208 } 209 shortcutArgs = append(shortcutArgs, d.RemainingArgs()...) 210 case "max_connections": 211 if hasMaxConnections { 212 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 213 } 214 if d.CountRemainingArgs() != 1 { 215 return d.ArgErr() 216 } 217 d.NextArg() 218 val, err := strconv.ParseInt(d.Val(), 10, 32) 219 if err != nil { 220 return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err) 221 } 222 u.MaxConnections, hasMaxConnections = int(val), true 223 case "tls": 224 if hasTLS { 225 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 226 } 227 if u.TLS == nil { 228 u.TLS = &reverseproxy.TLSConfig{} 229 } 230 hasTLS = true 231 case "tls_client_auth": 232 if hasTLSClientAuth { 233 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 234 } 235 if u.TLS == nil { 236 u.TLS = &reverseproxy.TLSConfig{} 237 } 238 if d.CountRemainingArgs() == 1 { 239 _, u.TLS.ClientCertificateAutomate = d.NextArg(), d.Val() 240 } else if d.CountRemainingArgs() == 2 { 241 _, u.TLS.ClientCertificateFile = d.NextArg(), d.Val() 242 _, u.TLS.ClientCertificateKeyFile = d.NextArg(), d.Val() 243 } else { 244 return d.ArgErr() 245 } 246 hasTLSClientAuth = true 247 case "tls_curves": 248 if d.CountRemainingArgs() == 0 { 249 return d.ArgErr() 250 } 251 if u.TLS == nil { 252 u.TLS = &reverseproxy.TLSConfig{} 253 } 254 u.TLS.Curves = append(u.TLS.Curves, d.RemainingArgs()...) 255 case "tls_except_ports": 256 if d.CountRemainingArgs() == 0 { 257 return d.ArgErr() 258 } 259 if u.TLS == nil { 260 u.TLS = &reverseproxy.TLSConfig{} 261 } 262 u.TLS.ExceptPorts = append(u.TLS.ExceptPorts, d.RemainingArgs()...) 263 case "tls_insecure_skip_verify": 264 if hasTLSInsecureSkipVerify { 265 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 266 } 267 if d.CountRemainingArgs() > 0 { 268 return d.ArgErr() 269 } 270 if u.TLS == nil { 271 u.TLS = &reverseproxy.TLSConfig{} 272 } 273 u.TLS.InsecureSkipVerify, hasTLSInsecureSkipVerify = true, true 274 case "tls_renegotiation": 275 if hasTLSRenegotiation { 276 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 277 } 278 if d.CountRemainingArgs() != 1 { 279 return d.ArgErr() 280 } 281 if u.TLS == nil { 282 u.TLS = &reverseproxy.TLSConfig{} 283 } 284 _, u.TLS.Renegotiation, hasTLSRenegotiation = d.NextArg(), d.Val(), true 285 286 switch u.TLS.Renegotiation { 287 case "never", "once", "freely": 288 continue 289 default: 290 return d.Errf("malformed %s option '%s': unrecognized value '%s'", 291 wrapper, optionName, u.TLS.Renegotiation) 292 } 293 case "tls_server_name": 294 if hasTLSServerName { 295 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 296 } 297 if d.CountRemainingArgs() != 1 { 298 return d.ArgErr() 299 } 300 if u.TLS == nil { 301 u.TLS = &reverseproxy.TLSConfig{} 302 } 303 _, u.TLS.ServerName, hasTLSServerName = d.NextArg(), d.Val(), true 304 case "tls_timeout": 305 if hasTLSTimeout { 306 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 307 } 308 if d.CountRemainingArgs() != 1 { 309 return d.ArgErr() 310 } 311 d.NextArg() 312 val, err := caddy.ParseDuration(d.Val()) 313 if err != nil { 314 return d.Errf("parsing %s option '%s' duration: %v", wrapper, optionName, err) 315 } 316 if u.TLS == nil { 317 u.TLS = &reverseproxy.TLSConfig{} 318 } 319 u.TLS.HandshakeTimeout, hasTLSTimeout = caddy.Duration(val), true 320 case "tls_trust_pool": 321 if hasTLSTrustPool { 322 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 323 } 324 if d.CountRemainingArgs() == 0 { 325 return d.ArgErr() 326 } 327 _, moduleName := d.NextArg(), d.Val() 328 329 unm, err := caddyfile.UnmarshalModule(d, "tls.ca_pool.source."+moduleName) 330 if err != nil { 331 return err 332 } 333 ca, ok := unm.(caddytls.CA) 334 if !ok { 335 return d.Errf("CA module '%s' is not a certificate pool provider", moduleName) 336 } 337 moduleRaw := caddyconfig.JSON(ca, nil) 338 339 moduleRaw, err = layer4.SetModuleNameInline("provider", moduleName, moduleRaw) 340 if err != nil { 341 return err 342 } 343 if u.TLS == nil { 344 u.TLS = &reverseproxy.TLSConfig{} 345 } 346 u.TLS.CARaw, hasTLSTrustPool = moduleRaw, true 347 case "tls_trusted_ca_certs": // DEPRECATED 348 if d.CountRemainingArgs() == 0 { 349 return d.ArgErr() 350 } 351 if u.TLS == nil { 352 u.TLS = &reverseproxy.TLSConfig{} 353 } 354 u.TLS.RootCAPEMFiles = append(u.TLS.RootCAPEMFiles, d.RemainingArgs()...) 355 case "tls_trusted_ca_pool": // DEPRECATED 356 if d.CountRemainingArgs() == 0 { 357 return d.ArgErr() 358 } 359 if u.TLS == nil { 360 u.TLS = &reverseproxy.TLSConfig{} 361 } 362 u.TLS.RootCAPool = append(u.TLS.RootCAPool, d.RemainingArgs()...) 363 default: 364 return d.ArgErr() 365 } 366 367 // No nested blocks are supported 368 if d.NextBlock(nesting + 1) { 369 return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName) 370 } 371 } 372 373 shortcutOptionName := "dial" 374 if len(shortcutArgs) == 0 { 375 return d.Errf("malformed %s block: at least one %s address must be provided", wrapper, shortcutOptionName) 376 } 377 u.Dial = append(u.Dial, shortcutArgs...) 378 379 return nil 380 } 381 382 // peer holds the state for a singular proxy backend; 383 // must not be copied, because peers are singular 384 // (even if there is more than 1 instance of a config, 385 // that does not duplicate the actual backend). 386 type peer struct { 387 numConns int32 388 unhealthy int32 389 fails int32 390 address caddy.NetworkAddress 391 } 392 393 // getNumConns returns the number of active connections with the peer. 394 func (p *peer) getNumConns() int { 395 return int(atomic.LoadInt32(&p.numConns)) 396 } 397 398 // healthy returns true if the peer is not unhealthy. 399 func (p *peer) healthy() bool { 400 return atomic.LoadInt32(&p.unhealthy) == 0 401 } 402 403 // countConn mutates the active connection count by 404 // delta. It returns an error if the adjustment fails. 405 func (p *peer) countConn(delta int) error { 406 result := atomic.AddInt32(&p.numConns, int32(delta)) 407 if result < 0 { 408 return fmt.Errorf("count below 0: %d", result) 409 } 410 return nil 411 } 412 413 // countFail mutates the recent failures count by 414 // delta. It returns an error if the adjustment fails. 415 func (p *peer) countFail(delta int) error { 416 result := atomic.AddInt32(&p.fails, int32(delta)) 417 if result < 0 { 418 return fmt.Errorf("count below 0: %d", result) 419 } 420 return nil 421 } 422 423 // setHealthy sets the upstream has healthy or unhealthy 424 // and returns true if the new value is different. 425 func (p *peer) setHealthy(healthy bool) (bool, error) { 426 var unhealthy, compare int32 = 1, 0 427 if healthy { 428 unhealthy, compare = 0, 1 429 } 430 swapped := atomic.CompareAndSwapInt32(&p.unhealthy, compare, unhealthy) 431 return swapped, nil 432 } 433 434 // Interface guard 435 var _ caddyfile.Unmarshaler = (*Upstream)(nil)