@marcel91200 / caddy Mirror · read-only / commits / 5fb5b814
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 14:15 UTC

reverseproxy: Multiple dynamic upstreams

This allows users to, for example, get upstreams from multiple SRV endpoints in order (such as primary and secondary clusters). Also, gofmt went to town on the comments, sigh

Matthew Holt committed Aug 25, 2022 at 21:42 UTC 5fb5b81439a521294a88cf4ad6da49fdc350f3aa
2 files changed +218 -120
modules/caddyhttp/reverseproxy/caddyfile.go
+142 -115
@@ -52,73 +52,73 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
52
53 // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
54 //
55 -// reverse_proxy [<matcher>] [<upstreams...>] {
56 -// # backends
57 -// to <upstreams...>
58 -// dynamic <name> [...]
55 +// reverse_proxy [<matcher>] [<upstreams...>] {
56 +// # backends
57 +// to <upstreams...>
58 +// dynamic <name> [...]
59 //
60 -// # load balancing
61 -// lb_policy <name> [<options...>]
62 -// lb_retries <retries>
63 -// lb_try_duration <duration>
64 -// lb_try_interval <interval>
65 -// lb_retry_match <request-matcher>
60 +// # load balancing
61 +// lb_policy <name> [<options...>]
62 +// lb_retries <retries>
63 +// lb_try_duration <duration>
64 +// lb_try_interval <interval>
65 +// lb_retry_match <request-matcher>
66 //
67 -// # active health checking
68 -// health_uri <uri>
69 -// health_port <port>
70 -// health_interval <interval>
71 -// health_timeout <duration>
72 -// health_status <status>
73 -// health_body <regexp>
74 -// health_headers {
75 -// <field> [<values...>]
76 -// }
67 +// # active health checking
68 +// health_uri <uri>
69 +// health_port <port>
70 +// health_interval <interval>
71 +// health_timeout <duration>
72 +// health_status <status>
73 +// health_body <regexp>
74 +// health_headers {
75 +// <field> [<values...>]
76 +// }
77 //
78 -// # passive health checking
79 -// fail_duration <duration>
80 -// max_fails <num>
81 -// unhealthy_status <status>
82 -// unhealthy_latency <duration>
83 -// unhealthy_request_count <num>
78 +// # passive health checking
79 +// fail_duration <duration>
80 +// max_fails <num>
81 +// unhealthy_status <status>
82 +// unhealthy_latency <duration>
83 +// unhealthy_request_count <num>
84 //
85 -// # streaming
86 -// flush_interval <duration>
87 -// buffer_requests
88 -// buffer_responses
89 -// max_buffer_size <size>
85 +// # streaming
86 +// flush_interval <duration>
87 +// buffer_requests
88 +// buffer_responses
89 +// max_buffer_size <size>
90 //
91 -// # request manipulation
92 -// trusted_proxies [private_ranges] <ranges...>
93 -// header_up [+|-]<field> [<value|regexp> [<replacement>]]
94 -// header_down [+|-]<field> [<value|regexp> [<replacement>]]
95 -// method <method>
96 -// rewrite <to>
91 +// # request manipulation
92 +// trusted_proxies [private_ranges] <ranges...>
93 +// header_up [+|-]<field> [<value|regexp> [<replacement>]]
94 +// header_down [+|-]<field> [<value|regexp> [<replacement>]]
95 +// method <method>
96 +// rewrite <to>
97 //
98 -// # round trip
99 -// transport <name> {
100 -// ...
101 -// }
98 +// # round trip
99 +// transport <name> {
100 +// ...
101 +// }
102 //
103 -// # optionally intercept responses from upstream
104 -// @name {
105 -// status <code...>
106 -// header <field> [<value>]
107 -// }
108 -// replace_status [<matcher>] <status_code>
109 -// handle_response [<matcher>] {
110 -// <directives...>
103 +// # optionally intercept responses from upstream
104 +// @name {
105 +// status <code...>
106 +// header <field> [<value>]
107 +// }
108 +// replace_status [<matcher>] <status_code>
109 +// handle_response [<matcher>] {
110 +// <directives...>
111 //
112 -// # special directives only available in handle_response
113 -// copy_response [<matcher>] [<status>] {
114 -// status <status>
115 -// }
116 -// copy_response_headers [<matcher>] {
117 -// include <fields...>
118 -// exclude <fields...>
119 -// }
120 -// }
121 -// }
112 +// # special directives only available in handle_response
113 +// copy_response [<matcher>] [<status>] {
114 +// status <status>
115 +// }
116 +// copy_response_headers [<matcher>] {
117 +// include <fields...>
118 +// exclude <fields...>
119 +// }
120 +// }
121 +// }
122 //
123 // Proxy upstream addresses should be network dial addresses such
124 // as `host:port`, or a URL such as `scheme://host:port`. Scheme
@@ -824,33 +824,32 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
824
825 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
826 //
827 -// transport http {
828 -// read_buffer <size>
829 -// write_buffer <size>
830 -// max_response_header <size>
831 -// dial_timeout <duration>
832 -// dial_fallback_delay <duration>
833 -// response_header_timeout <duration>
834 -// expect_continue_timeout <duration>
835 -// resolvers <resolvers...>
836 -// tls
837 -// tls_client_auth <automate_name> | <cert_file> <key_file>
838 -// tls_insecure_skip_verify
839 -// tls_timeout <duration>
840 -// tls_trusted_ca_certs <cert_files...>
841 -// tls_server_name <sni>
842 -// tls_renegotiation <level>
843 -// tls_except_ports <ports...>
844 -// keepalive [off|<duration>]
845 -// keepalive_interval <interval>
846 -// keepalive_idle_conns <max_count>
847 -// keepalive_idle_conns_per_host <count>
848 -// versions <versions...>
849 -// compression off
850 -// max_conns_per_host <count>
851 -// max_idle_conns_per_host <count>
852 -// }
853 -//
827 +// transport http {
828 +// read_buffer <size>
829 +// write_buffer <size>
830 +// max_response_header <size>
831 +// dial_timeout <duration>
832 +// dial_fallback_delay <duration>
833 +// response_header_timeout <duration>
834 +// expect_continue_timeout <duration>
835 +// resolvers <resolvers...>
836 +// tls
837 +// tls_client_auth <automate_name> | <cert_file> <key_file>
838 +// tls_insecure_skip_verify
839 +// tls_timeout <duration>
840 +// tls_trusted_ca_certs <cert_files...>
841 +// tls_server_name <sni>
842 +// tls_renegotiation <level>
843 +// tls_except_ports <ports...>
844 +// keepalive [off|<duration>]
845 +// keepalive_interval <interval>
846 +// keepalive_idle_conns <max_count>
847 +// keepalive_idle_conns_per_host <count>
848 +// versions <versions...>
849 +// compression off
850 +// max_conns_per_host <count>
851 +// max_idle_conns_per_host <count>
852 +// }
853 func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
854 for d.Next() {
855 for d.NextBlock(0) {
@@ -1138,10 +1137,9 @@ func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHan
1137
1138 // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
1139 //
1141 -// copy_response [<matcher>] [<status>] {
1142 -// status <status>
1143 -// }
1144 -//
1140 +// copy_response [<matcher>] [<status>] {
1141 +// status <status>
1142 +// }
1143 func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1144 for d.Next() {
1145 args := d.RemainingArgs()
@@ -1178,11 +1176,10 @@ func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.Middle
1176
1177 // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
1178 //
1181 -// copy_response_headers [<matcher>] {
1182 -// include <fields...>
1183 -// exclude <fields...>
1184 -// }
1185 -//
1179 +// copy_response_headers [<matcher>] {
1180 +// include <fields...>
1181 +// exclude <fields...>
1182 +// }
1183 func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1184 for d.Next() {
1185 args := d.RemainingArgs()
@@ -1208,16 +1205,15 @@ func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser)
1205
1206 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
1207 //
1211 -// dynamic srv [<name>] {
1212 -// service <service>
1213 -// proto <proto>
1214 -// name <name>
1215 -// refresh <interval>
1216 -// resolvers <resolvers...>
1217 -// dial_timeout <timeout>
1218 -// dial_fallback_delay <timeout>
1219 -// }
1220 -//
1208 +// dynamic srv [<name>] {
1209 +// service <service>
1210 +// proto <proto>
1211 +// name <name>
1212 +// refresh <interval>
1213 +// resolvers <resolvers...>
1214 +// dial_timeout <timeout>
1215 +// dial_fallback_delay <timeout>
1216 +// }
1217 func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1218 for d.Next() {
1219 args := d.RemainingArgs()
@@ -1307,15 +1303,14 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1303
1304 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
1305 //
1310 -// dynamic a [<name> <port] {
1311 -// name <name>
1312 -// port <port>
1313 -// refresh <interval>
1314 -// resolvers <resolvers...>
1315 -// dial_timeout <timeout>
1316 -// dial_fallback_delay <timeout>
1317 -// }
1318 -//
1306 +// dynamic a [<name> <port] {
1307 +// name <name>
1308 +// port <port>
1309 +// refresh <interval>
1310 +// resolvers <resolvers...>
1311 +// dial_timeout <timeout>
1312 +// dial_fallback_delay <timeout>
1313 +// }
1314 func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1315 for d.Next() {
1316 args := d.RemainingArgs()
@@ -1324,7 +1319,9 @@ func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1319 }
1320 if len(args) > 0 {
1321 u.Name = args[0]
1327 - u.Port = args[1]
1322 + if len(args) == 2 {
1323 + u.Port = args[1]
1324 + }
1325 }
1326
1327 for d.NextBlock(0) {
@@ -1395,6 +1392,35 @@ func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1392 return nil
1393 }
1394
1395 +// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
1396 +//
1397 +// dynamic multi {
1398 +// <source> [...]
1399 +// }
1400 +func (u *MultiUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1401 + for d.Next() {
1402 + if d.NextArg() {
1403 + return d.ArgErr()
1404 + }
1405 +
1406 + for nesting := d.Nesting(); d.NextBlock(nesting); {
1407 + dynModule := d.Val()
1408 + modID := "http.reverse_proxy.upstreams." + dynModule
1409 + unm, err := caddyfile.UnmarshalModule(d, modID)
1410 + if err != nil {
1411 + return err
1412 + }
1413 + source, ok := unm.(UpstreamSource)
1414 + if !ok {
1415 + return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm)
1416 + }
1417 + u.SourcesRaw = append(u.SourcesRaw, caddyconfig.JSONModuleObject(source, "source", dynModule, nil))
1418 + }
1419 + }
1420 +
1421 + return nil
1422 +}
1423 +
1424 const matcherPrefix = "@"
1425
1426 // Interface guards
@@ -1403,4 +1429,5 @@ var (
1429 _ caddyfile.Unmarshaler = (*HTTPTransport)(nil)
1430 _ caddyfile.Unmarshaler = (*SRVUpstreams)(nil)
1431 _ caddyfile.Unmarshaler = (*AUpstreams)(nil)
1432 + _ caddyfile.Unmarshaler = (*MultiUpstreams)(nil)
1433 )
modules/caddyhttp/reverseproxy/upstreams.go
+76 -5
@@ -2,6 +2,7 @@ package reverseproxy
2
3 import (
4 "context"
5 + "encoding/json"
6 "fmt"
7 weakrand "math/rand"
8 "net"
@@ -18,6 +19,7 @@ import (
19 func init() {
20 caddy.RegisterModule(SRVUpstreams{})
21 caddy.RegisterModule(AUpstreams{})
22 + caddy.RegisterModule(MultiUpstreams{})
23 }
24
25 // SRVUpstreams provides upstreams from SRV lookups.
@@ -211,11 +213,6 @@ func (sl srvLookup) isFresh() bool {
213 return time.Since(sl.freshness) < time.Duration(sl.srvUpstreams.Refresh)
214 }
215
214 -var (
215 - srvs = make(map[string]srvLookup)
216 - srvsMu sync.RWMutex
217 -)
218 -
216 // AUpstreams provides upstreams from A/AAAA lookups.
217 // Results are cached and refreshed at the configured
218 // refresh interval.
@@ -355,6 +352,77 @@ func (al aLookup) isFresh() bool {
352 return time.Since(al.freshness) < time.Duration(al.aUpstreams.Refresh)
353 }
354
355 +// MultiUpstreams is a single dynamic upstream source that
356 +// aggregates the results of multiple dynamic upstream sources.
357 +// All configured sources will be queried in order, with their
358 +// results appended to the end of the list. Errors returned
359 +// from individual sources will be logged and the next source
360 +// will continue to be invoked.
361 +//
362 +// This module makes it easy to implement redundant cluster
363 +// failovers, especially in conjunction with the `first` load
364 +// balancing policy: if the first source returns an error or
365 +// no upstreams, the second source's upstreams will be used
366 +// naturally.
367 +type MultiUpstreams struct {
368 + // The list of upstream source modules to get upstreams from.
369 + // They will be queried in order, with their results appended
370 + // in the order they are returned.
371 + SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=http.reverse_proxy.upstreams inline_key=source"`
372 + sources []UpstreamSource
373 +
374 + logger *zap.Logger
375 +}
376 +
377 +// CaddyModule returns the Caddy module information.
378 +func (MultiUpstreams) CaddyModule() caddy.ModuleInfo {
379 + return caddy.ModuleInfo{
380 + ID: "http.reverse_proxy.upstreams.multi",
381 + New: func() caddy.Module { return new(MultiUpstreams) },
382 + }
383 +}
384 +
385 +func (mu *MultiUpstreams) Provision(ctx caddy.Context) error {
386 + mu.logger = ctx.Logger(mu)
387 +
388 + if mu.SourcesRaw != nil {
389 + mod, err := ctx.LoadModule(mu, "SourcesRaw")
390 + if err != nil {
391 + return fmt.Errorf("loading upstream source modules: %v", err)
392 + }
393 + for _, src := range mod.([]any) {
394 + mu.sources = append(mu.sources, src.(UpstreamSource))
395 + }
396 + }
397 +
398 + return nil
399 +}
400 +
401 +func (mu MultiUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
402 + var upstreams []*Upstream
403 +
404 + for i, src := range mu.sources {
405 + select {
406 + case <-r.Context().Done():
407 + return upstreams, context.Canceled
408 + default:
409 + }
410 +
411 + up, err := src.GetUpstreams(r)
412 + if err != nil {
413 + mu.logger.Error("upstream source returned error",
414 + zap.Int("source_idx", i),
415 + zap.Error(err))
416 + } else if len(up) == 0 {
417 + mu.logger.Warn("upstream source returned 0 upstreams", zap.Int("source_idx", i))
418 + } else {
419 + upstreams = append(upstreams, up...)
420 + }
421 + }
422 +
423 + return upstreams, nil
424 +}
425 +
426 // UpstreamResolver holds the set of addresses of DNS resolvers of
427 // upstream addresses
428 type UpstreamResolver struct {
@@ -391,6 +459,9 @@ func (u *UpstreamResolver) ParseAddresses() error {
459 }
460
461 var (
462 + srvs = make(map[string]srvLookup)
463 + srvsMu sync.RWMutex
464 +
465 aAaaa = make(map[string]aLookup)
466 aAaaaMu sync.RWMutex
467 )