Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 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 reverseproxy |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "fmt" |
| 20 | "net/netip" |
| 21 | "strconv" |
| 22 | "sync" |
| 23 | "sync/atomic" |
| 24 | "time" |
| 25 | |
| 26 | "github.com/caddyserver/caddy/v2" |
| 27 | ) |
| 28 | |
| 29 | // UpstreamPool is a collection of upstreams. |
| 30 | type UpstreamPool []*Upstream |
| 31 | |
| 32 | // Upstream bridges this proxy's configuration to the |
| 33 | // state of the backend host it is correlated with. |
| 34 | // Upstream values must not be copied. |
| 35 | type Upstream struct { |
| 36 | *Host `json:"-"` |
| 37 | |
| 38 | // The [network address](/docs/conventions#network-addresses) |
| 39 | // to dial to connect to the upstream. Must represent precisely |
| 40 | // one socket (i.e. no port ranges). A valid network address |
| 41 | // either has a host and port or is a unix socket address. |
| 42 | // |
| 43 | // Placeholders may be used to make the upstream dynamic, but be |
| 44 | // aware of the health check implications of this: a single |
| 45 | // upstream that represents numerous (perhaps arbitrary) backends |
| 46 | // can be considered down if one or enough of the arbitrary |
| 47 | // backends is down. Also be aware of open proxy vulnerabilities. |
| 48 | Dial string `json:"dial,omitempty"` |
| 49 | |
| 50 | // The maximum number of simultaneous requests to allow to |
| 51 | // this upstream. If set, overrides the global passive health |
| 52 | // check UnhealthyRequestCount value. |
| 53 | MaxRequests int `json:"max_requests,omitempty"` |
| 54 | |
| 55 | // TODO: This could be really useful, to bind requests |
| 56 | // with certain properties to specific backends |
| 57 | // HeaderAffinity string |
| 58 | // IPAffinity string |
| 59 | |
| 60 | activeHealthCheckPort int |
| 61 | activeHealthCheckUpstream string |
| 62 | healthCheckPolicy *PassiveHealthChecks |
| 63 | cb CircuitBreaker |
| 64 | |
| 65 | // state from the active health checker. It lives here rather than on |
| 66 | // the shared Host because the Host is keyed by dial address alone, |
| 67 | // while an active health check is configured per handler: two handlers |
| 68 | // dialing the same address with different health_uri or health_headers |
| 69 | // are checking distinct health targets, and must not push each other |
| 70 | // over their own consecutive pass/fail thresholds. |
| 71 | unhealthy atomic.Int32 |
| 72 | activePasses atomic.Int64 |
| 73 | activeFails atomic.Int64 |
| 74 | } |
| 75 | |
| 76 | // (pointer receiver necessary to avoid a race condition, since |
| 77 | // copying the Upstream reads the 'unhealthy' field which is |
| 78 | // accessed atomically) |
| 79 | func (u *Upstream) String() string { return u.Dial } |
| 80 | |
| 81 | // Available returns true if the remote host |
| 82 | // is available to receive requests. This is |
| 83 | // the method that should be used by selection |
| 84 | // policies, etc. to determine if a backend |
| 85 | // should be able to be sent a request. |
| 86 | func (u *Upstream) Available() bool { |
| 87 | return u.Healthy() && !u.Full() |
| 88 | } |
| 89 | |
| 90 | // Healthy returns true if the remote host |
| 91 | // is currently known to be healthy or "up". |
| 92 | // It consults the circuit breaker, if any. |
| 93 | func (u *Upstream) Healthy() bool { |
| 94 | healthy := u.healthy() |
| 95 | if healthy && u.healthCheckPolicy != nil { |
| 96 | healthy = u.Host.Fails() < u.healthCheckPolicy.MaxFails |
| 97 | } |
| 98 | if healthy && u.cb != nil { |
| 99 | healthy = u.cb.OK() |
| 100 | } |
| 101 | return healthy |
| 102 | } |
| 103 | |
| 104 | // Full returns true if the remote host |
| 105 | // cannot receive more requests at this time. |
| 106 | func (u *Upstream) Full() bool { |
| 107 | return u.MaxRequests > 0 && u.Host.NumRequests() >= u.MaxRequests |
| 108 | } |
| 109 | |
| 110 | // fillDialInfo returns a filled DialInfo for upstream u, using the request |
| 111 | // context. Note that the returned value is not a pointer. |
| 112 | func (u *Upstream) fillDialInfo(repl *caddy.Replacer) (DialInfo, error) { |
| 113 | var addr caddy.NetworkAddress |
| 114 | |
| 115 | // use provided dial address |
| 116 | var err error |
| 117 | dial := repl.ReplaceAll(u.Dial, "") |
| 118 | addr, err = caddy.ParseNetworkAddress(dial) |
| 119 | if err != nil { |
| 120 | return DialInfo{}, fmt.Errorf("upstream %s: invalid dial address %s: %v", u.Dial, dial, err) |
| 121 | } |
| 122 | if numPorts := addr.PortRangeSize(); numPorts != 1 { |
| 123 | return DialInfo{}, fmt.Errorf("upstream %s: dial address must represent precisely one socket: %s represents %d", |
| 124 | u.Dial, dial, numPorts) |
| 125 | } |
| 126 | |
| 127 | return DialInfo{ |
| 128 | Upstream: u, |
| 129 | Network: addr.Network, |
| 130 | Address: addr.JoinHostPort(0), |
| 131 | Host: addr.Host, |
| 132 | Port: strconv.Itoa(int(addr.StartPort)), |
| 133 | }, nil |
| 134 | } |
| 135 | |
| 136 | func (u *Upstream) fillHost() { |
| 137 | host := new(Host) |
| 138 | existingHost, loaded := hosts.LoadOrStore(u.String(), host) |
| 139 | if loaded { |
| 140 | host = existingHost.(*Host) |
| 141 | } |
| 142 | u.Host = host |
| 143 | } |
| 144 | |
| 145 | // fillDynamicHost is like fillHost, but stores the host in the separate |
| 146 | // dynamicHosts map rather than the reference-counted UsagePool. Dynamic |
| 147 | // hosts are not reference-counted; instead, they are retained as long as |
| 148 | // they are actively seen and are evicted by a background cleanup goroutine |
| 149 | // after dynamicHostIdleExpiry of inactivity. This preserves health state |
| 150 | // (e.g. passive fail counts) across sequential requests. |
| 151 | func (u *Upstream) fillDynamicHost() { |
| 152 | dynamicHostsMu.Lock() |
| 153 | entry, ok := dynamicHosts[u.String()] |
| 154 | if ok { |
| 155 | entry.lastSeen = time.Now() |
| 156 | dynamicHosts[u.String()] = entry |
| 157 | u.Host = entry.host |
| 158 | } else { |
| 159 | h := new(Host) |
| 160 | dynamicHosts[u.String()] = dynamicHostEntry{host: h, lastSeen: time.Now()} |
| 161 | u.Host = h |
| 162 | } |
| 163 | dynamicHostsMu.Unlock() |
| 164 | |
| 165 | // ensure the cleanup goroutine is running |
| 166 | dynamicHostsCleanerOnce.Do(func() { |
| 167 | go func() { |
| 168 | for { |
| 169 | time.Sleep(dynamicHostCleanupInterval) |
| 170 | dynamicHostsMu.Lock() |
| 171 | for addr, entry := range dynamicHosts { |
| 172 | if time.Since(entry.lastSeen) > dynamicHostIdleExpiry { |
| 173 | delete(dynamicHosts, addr) |
| 174 | } |
| 175 | } |
| 176 | dynamicHostsMu.Unlock() |
| 177 | } |
| 178 | }() |
| 179 | }) |
| 180 | } |
| 181 | |
| 182 | // Host is the basic, in-memory representation of the state of a remote host. |
| 183 | // Its fields are accessed atomically and Host values must not be copied. |
| 184 | type Host struct { |
| 185 | numRequests atomic.Int64 |
| 186 | fails atomic.Int64 |
| 187 | } |
| 188 | |
| 189 | // NumRequests returns the number of active requests to the upstream. |
| 190 | func (h *Host) NumRequests() int { |
| 191 | return int(h.numRequests.Load()) |
| 192 | } |
| 193 | |
| 194 | // Fails returns the number of recent failures with the upstream. |
| 195 | func (h *Host) Fails() int { |
| 196 | return int(h.fails.Load()) |
| 197 | } |
| 198 | |
| 199 | // countRequest mutates the active request count by |
| 200 | // delta. It returns an error if the adjustment fails. |
| 201 | func (h *Host) countRequest(delta int) error { |
| 202 | result := h.numRequests.Add(int64(delta)) |
| 203 | if result < 0 { |
| 204 | return fmt.Errorf("count below 0: %d", result) |
| 205 | } |
| 206 | return nil |
| 207 | } |
| 208 | |
| 209 | // countFail mutates the recent failures count by |
| 210 | // delta. It returns an error if the adjustment fails. |
| 211 | func (h *Host) countFail(delta int) error { |
| 212 | result := h.fails.Add(int64(delta)) |
| 213 | if result < 0 { |
| 214 | return fmt.Errorf("count below 0: %d", result) |
| 215 | } |
| 216 | return nil |
| 217 | } |
| 218 | |
| 219 | // activeHealthPasses returns the number of consecutive passing |
| 220 | // active health checks observed by this upstream's checker. |
| 221 | func (u *Upstream) activeHealthPasses() int { |
| 222 | return int(u.activePasses.Load()) |
| 223 | } |
| 224 | |
| 225 | // activeHealthFails returns the number of consecutive failing |
| 226 | // active health checks observed by this upstream's checker. |
| 227 | func (u *Upstream) activeHealthFails() int { |
| 228 | return int(u.activeFails.Load()) |
| 229 | } |
| 230 | |
| 231 | // countHealthPass mutates the recent passes count by |
| 232 | // delta. It returns an error if the adjustment fails. |
| 233 | func (u *Upstream) countHealthPass(delta int) error { |
| 234 | result := u.activePasses.Add(int64(delta)) |
| 235 | if result < 0 { |
| 236 | return fmt.Errorf("count below 0: %d", result) |
| 237 | } |
| 238 | return nil |
| 239 | } |
| 240 | |
| 241 | // countHealthFail mutates the recent failures count by |
| 242 | // delta. It returns an error if the adjustment fails. |
| 243 | func (u *Upstream) countHealthFail(delta int) error { |
| 244 | result := u.activeFails.Add(int64(delta)) |
| 245 | if result < 0 { |
| 246 | return fmt.Errorf("count below 0: %d", result) |
| 247 | } |
| 248 | return nil |
| 249 | } |
| 250 | |
| 251 | // resetHealth resets the health check counters. |
| 252 | func (u *Upstream) resetHealth() { |
| 253 | u.activePasses.Store(0) |
| 254 | u.activeFails.Store(0) |
| 255 | } |
| 256 | |
| 257 | // healthy returns true if the upstream is not actively marked as unhealthy. |
| 258 | // (This returns the status only from the "active" health checks.) |
| 259 | func (u *Upstream) healthy() bool { |
| 260 | return u.unhealthy.Load() == 0 |
| 261 | } |
| 262 | |
| 263 | // SetHealthy sets the upstream has healthy or unhealthy |
| 264 | // and returns true if the new value is different. This |
| 265 | // sets the status only for the "active" health checks. |
| 266 | func (u *Upstream) setHealthy(healthy bool) bool { |
| 267 | var unhealthy, compare int32 = 1, 0 |
| 268 | if healthy { |
| 269 | unhealthy, compare = 0, 1 |
| 270 | } |
| 271 | return u.unhealthy.CompareAndSwap(compare, unhealthy) |
| 272 | } |
| 273 | |
| 274 | // DialInfo contains information needed to dial a |
| 275 | // connection to an upstream host. This information |
| 276 | // may be different than that which is represented |
| 277 | // in a URL (for example, unix sockets don't have |
| 278 | // a host that can be represented in a URL, but |
| 279 | // they certainly have a network name and address). |
| 280 | type DialInfo struct { |
| 281 | // Upstream is the Upstream associated with |
| 282 | // this DialInfo. It may be nil. |
| 283 | Upstream *Upstream |
| 284 | |
| 285 | // The network to use. This should be one of |
| 286 | // the values that is accepted by net.Dial: |
| 287 | // https://golang.org/pkg/net/#Dial |
| 288 | Network string |
| 289 | |
| 290 | // The address to dial. Follows the same |
| 291 | // semantics and rules as net.Dial. |
| 292 | Address string |
| 293 | |
| 294 | // Host and Port are components of Address. |
| 295 | Host, Port string |
| 296 | } |
| 297 | |
| 298 | // String returns the Caddy network address form |
| 299 | // by joining the network and address with a |
| 300 | // forward slash. |
| 301 | func (di DialInfo) String() string { |
| 302 | return caddy.JoinNetworkAddress(di.Network, di.Host, di.Port) |
| 303 | } |
| 304 | |
| 305 | // GetDialInfo gets the upstream dialing info out of the context, |
| 306 | // and returns true if there was a valid value; false otherwise. |
| 307 | func GetDialInfo(ctx context.Context) (DialInfo, bool) { |
| 308 | dialInfo, ok := ctx.Value(dialInfoCtxKey).(DialInfo) |
| 309 | return dialInfo, ok |
| 310 | } |
| 311 | |
| 312 | // hosts is the global repository for hosts that are |
| 313 | // currently in use by active configuration(s). This |
| 314 | // allows the state of remote hosts to be preserved |
| 315 | // through config reloads. |
| 316 | var hosts = caddy.NewUsagePool() |
| 317 | |
| 318 | // dynamicHosts tracks hosts that were provisioned from dynamic upstream |
| 319 | // sources. Unlike static upstreams which are reference-counted via the |
| 320 | // UsagePool, dynamic upstream hosts are not reference-counted. Instead, |
| 321 | // their last-seen time is updated on each request, and a background |
| 322 | // goroutine evicts entries that have been idle for dynamicHostIdleExpiry. |
| 323 | // This preserves health state (e.g. passive fail counts) across requests |
| 324 | // to the same dynamic backend. |
| 325 | var ( |
| 326 | dynamicHosts = make(map[string]dynamicHostEntry) |
| 327 | dynamicHostsMu sync.RWMutex |
| 328 | dynamicHostsCleanerOnce sync.Once |
| 329 | dynamicHostCleanupInterval = 5 * time.Minute |
| 330 | dynamicHostIdleExpiry = time.Hour |
| 331 | ) |
| 332 | |
| 333 | // dynamicHostEntry holds a Host and the last time it was seen |
| 334 | // in a set of dynamic upstreams returned for a request. |
| 335 | type dynamicHostEntry struct { |
| 336 | host *Host |
| 337 | lastSeen time.Time |
| 338 | } |
| 339 | |
| 340 | // dialInfoCtxKey is the context key used for the variable that holds |
| 341 | // the dial info for the upstream connection. |
| 342 | const dialInfoCtxKey caddy.CtxKey = "reverse_proxy.dial_info" |
| 343 | |
| 344 | // proxyProtocolInfoVarKey is the key used for the variable that holds |
| 345 | // the proxy protocol info for the upstream connection. |
| 346 | const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info" |
| 347 | |
| 348 | // ProxyProtocolInfo contains information needed to write proxy protocol to a |
| 349 | // connection to an upstream host. |
| 350 | type ProxyProtocolInfo struct { |
| 351 | AddrPort netip.AddrPort |
| 352 | } |
| 353 | |
| 354 | // tlsH1OnlyVarKey is the key used that indicates the connection will use h1 only for TLS. |
| 355 | // https://github.com/caddyserver/caddy/issues/7292 |
| 356 | const tlsH1OnlyVarKey = "reverse_proxy.tls_h1_only" |
| 357 | |
| 358 | // proxyVarKey is the key used that indicates the proxy server used for a request. |
| 359 | const proxyVarKey = "reverse_proxy.proxy" |