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
)