v2.31.0 e02f654

traffic.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"net"
6	"strconv"
7	"strings"
8	"time"
9)
10
11type EntryPointProtocol string
12
13const (
14	EntryPointProtocolHTTP     EntryPointProtocol = "http"
15	EntryPointProtocolTLSMux   EntryPointProtocol = "tls_mux"
16	EntryPointProtocolSmartTCP EntryPointProtocol = "smart_tcp"
17	EntryPointProtocolTCP      EntryPointProtocol = "tcp"
18	EntryPointProtocolUDP      EntryPointProtocol = "udp"
19)
20
21type RouterProtocol string
22
23const (
24	RouterProtocolHTTP           RouterProtocol = "http"
25	RouterProtocolTCP            RouterProtocol = "tcp"
26	RouterProtocolUDP            RouterProtocol = "udp"
27	RouterProtocolTLSPassthrough RouterProtocol = "tls_passthrough"
28)
29
30type NetworkProtocol string
31
32const (
33	NetworkProtocolTCP NetworkProtocol = "tcp"
34	NetworkProtocolUDP NetworkProtocol = "udp"
35)
36
37type TrafficGraph struct {
38	Options     TrafficOptions
39	EntryPoints []EntryPoint
40	Routers     []TrafficRouter
41	Services    []TrafficService
42}
43
44type TrafficOptions struct {
45	TCP TCPOptions
46	UDP UDPOptions
47}
48
49type TCPOptions struct {
50	DialTimeout    time.Duration
51	IdleTimeout    time.Duration
52	DrainTimeout   time.Duration
53	MaxConnections int
54}
55
56type UDPOptions struct {
57	IdleTimeout  time.Duration
58	DrainTimeout time.Duration
59	MaxSessions  int
60}
61
62type EntryPoint struct {
63	Name                    string
64	Address                 string
65	Protocol                EntryPointProtocol
66	TrustedCIDRs            []string
67	RawFallback             string
68	RawFallbackTrustedCIDRs []string
69	AllowPublicRawFallback  bool
70}
71
72type TrafficRouter struct {
73	Name       string
74	EntryPoint string
75	Protocol   RouterProtocol
76	Rule       TrafficRule
77	Service    string
78}
79
80type TrafficRule struct {
81	Host string
82	SNI  string
83}
84
85type TrafficService struct {
86	Name     string
87	Backends []TrafficBackend
88}
89
90type TrafficBackend struct {
91	Name     string
92	Host     string
93	Port     int
94	Protocol NetworkProtocol
95}
96
97type TrafficStatus struct {
98	LastReloadStatus string
99	LastReloadError  string
100	EntryPoints      []EntryPointStatus
101	Routers          []TrafficRouterStatus
102	Services         []TrafficServiceStatus
103	Counters         TrafficCounters
104}
105
106type EntryPointStatus struct {
107	Name                 string
108	Address              string
109	Protocol             EntryPointProtocol
110	Active               bool
111	ActiveTCPConnections int64
112	ActiveUDPSessions    int64
113	TotalAccepted        int64
114	TotalRefused         int64
115	TotalErrors          int64
116	BytesIn              int64
117	BytesOut             int64
118	SmartTCP             SmartTCPCounters
119}
120
121type TrafficRouterStatus struct {
122	Name       string
123	EntryPoint string
124	Protocol   RouterProtocol
125	Rule       TrafficRule
126	Service    string
127	Active     bool
128}
129
130type TrafficServiceStatus struct {
131	Name     string
132	Active   bool
133	Backends []TrafficBackendStatus
134}
135
136type TrafficBackendStatus struct {
137	Name     string
138	Host     string
139	Port     int
140	Protocol NetworkProtocol
141	Active   bool
142}
143
144type TrafficCounters struct {
145	ActiveTCPConnections int64
146	ActiveUDPSessions    int64
147	TotalAccepted        int64
148	TotalRefused         int64
149	TotalErrors          int64
150	BytesIn              int64
151	BytesOut             int64
152	SmartTCP             SmartTCPCounters
153}
154
155type SmartTCPCounters struct {
156	HTTPAccepted             int64
157	H2CAccepted              int64
158	HTTPSFallbackAccepted    int64
159	TLSPassthroughAccepted   int64
160	RawFallbackAccepted      int64
161	EntrypointCIDRRefused    int64
162	RawFallbackCIDRRefused   int64
163	PROXYRefused             int64
164	UnknownNoFallbackRefused int64
165	MalformedRejected        int64
166	SniffTimeout             int64
167	ClientHelloTooLarge      int64
168}
169
170type TrafficServiceRefKind string
171
172const (
173	TrafficServiceRefRoute          TrafficServiceRefKind = "route"
174	TrafficServiceRefExternalRoute  TrafficServiceRefKind = "external_route"
175	TrafficServiceRefNetworkService TrafficServiceRefKind = "network_service"
176	TrafficServiceRefService        TrafficServiceRefKind = "service"
177	TrafficServiceRefStatic         TrafficServiceRefKind = "static"
178)
179
180type TrafficServiceRef struct {
181	Kind     TrafficServiceRefKind
182	Domain   string
183	Name     string
184	PortName string
185	Reserved bool
186}
187
188func ParseTrafficServiceRef(value string) (TrafficServiceRef, error) {
189	kind, rest, ok := strings.Cut(strings.TrimSpace(value), ":")
190	if !ok || rest == "" {
191		return TrafficServiceRef{}, fmt.Errorf("invalid traffic service ref %q", value)
192	}
193	switch TrafficServiceRefKind(kind) {
194	case TrafficServiceRefRoute:
195		return TrafficServiceRef{Kind: TrafficServiceRefRoute, Domain: rest}, nil
196	case TrafficServiceRefExternalRoute:
197		return TrafficServiceRef{Kind: TrafficServiceRefExternalRoute, Domain: rest}, nil
198	case TrafficServiceRefNetworkService:
199		return parseNamedPortServiceRef(value, rest, TrafficServiceRefNetworkService, "network service")
200	case TrafficServiceRefService:
201		return parseNamedPortServiceRef(value, rest, TrafficServiceRefService, "service")
202	case TrafficServiceRefStatic:
203		return TrafficServiceRef{Kind: TrafficServiceRefStatic, Name: rest, Reserved: true}, nil
204	default:
205		return TrafficServiceRef{}, fmt.Errorf("invalid traffic service ref kind %q", kind)
206	}
207}
208
209func parseNamedPortServiceRef(value, rest string, kind TrafficServiceRefKind, label string) (TrafficServiceRef, error) {
210	name, portName, ok := strings.Cut(rest, ":")
211	if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
212		return TrafficServiceRef{}, fmt.Errorf("invalid %s ref %q", label, value)
213	}
214	return TrafficServiceRef{Kind: kind, Name: name, PortName: portName}, nil
215}
216
217func (g TrafficGraph) Validate() error {
218	if err := validateTrafficOptions(g.Options); err != nil {
219		return err
220	}
221	entryPoints, err := validateTrafficEntryPoints(g.EntryPoints)
222	if err != nil {
223		return err
224	}
225	services, err := validateTrafficServices(g.Services)
226	if err != nil {
227		return err
228	}
229	return g.validateRouters(entryPoints, services)
230}
231
232func validateTrafficOptions(options TrafficOptions) error {
233	if err := validateOptionalPositiveDuration(options.TCP.DialTimeout, "traffic.tcp.dial_timeout"); err != nil {
234		return err
235	}
236	if err := validateOptionalPositiveDuration(options.TCP.IdleTimeout, "traffic.tcp.idle_timeout"); err != nil {
237		return err
238	}
239	if err := validateOptionalPositiveDuration(options.TCP.DrainTimeout, "traffic.tcp.drain_timeout"); err != nil {
240		return err
241	}
242	if options.TCP.MaxConnections < 0 {
243		return fmt.Errorf("traffic.tcp.max_connections must be non-negative")
244	}
245	if err := validateOptionalPositiveDuration(options.UDP.IdleTimeout, "traffic.udp.idle_timeout"); err != nil {
246		return err
247	}
248	if err := validateOptionalPositiveDuration(options.UDP.DrainTimeout, "traffic.udp.drain_timeout"); err != nil {
249		return err
250	}
251	if options.UDP.MaxSessions < 0 {
252		return fmt.Errorf("traffic.udp.max_sessions must be non-negative")
253	}
254	return nil
255}
256
257func validateOptionalPositiveDuration(value time.Duration, name string) error {
258	if value < 0 {
259		return fmt.Errorf("%s must be positive", name)
260	}
261	return nil
262}
263
264func validateTrafficEntryPoints(entries []EntryPoint) (map[string]EntryPoint, error) {
265	entryPoints := make(map[string]EntryPoint, len(entries))
266	listenAddresses := newProtocolListenAddressSet()
267	for _, entryPoint := range entries {
268		if err := validateTrafficEntryPoint(entryPoint, entryPoints, listenAddresses); err != nil {
269			return nil, err
270		}
271		entryPoints[entryPoint.Name] = entryPoint
272	}
273	return entryPoints, nil
274}
275
276type protocolListenAddressSet struct {
277	tcp listenAddressSet
278	udp listenAddressSet
279}
280
281func newProtocolListenAddressSet() *protocolListenAddressSet {
282	return &protocolListenAddressSet{tcp: newListenAddressSet(), udp: newListenAddressSet()}
283}
284
285func (s *protocolListenAddressSet) add(entryPoint EntryPoint, addr listenAddress) error {
286	if entryPoint.Protocol == EntryPointProtocolUDP {
287		return s.udp.add(addr, entryPoint.Name, "udp")
288	}
289	return s.tcp.add(addr, entryPoint.Name, "tcp")
290}
291
292func validateTrafficEntryPoint(entryPoint EntryPoint, existing map[string]EntryPoint, listenAddresses *protocolListenAddressSet) error {
293	if entryPoint.Name == "" {
294		return fmt.Errorf("entrypoint name is required")
295	}
296	if _, exists := existing[entryPoint.Name]; exists {
297		return fmt.Errorf("duplicate entrypoint name %q", entryPoint.Name)
298	}
299	if err := validateEntryPointProtocol(entryPoint.Protocol); err != nil {
300		return fmt.Errorf("invalid entrypoint protocol for %q: %w", entryPoint.Name, err)
301	}
302	if err := validateRawFallbackEntrypoint(entryPoint); err != nil {
303		return err
304	}
305	if err := validateTrustedCIDRs(entryPoint); err != nil {
306		return err
307	}
308	addr, err := parseListenAddress(entryPoint.Address)
309	if err != nil {
310		return fmt.Errorf("invalid entrypoint address for %q: %w", entryPoint.Name, err)
311	}
312	return listenAddresses.add(entryPoint, addr)
313}
314
315func validateTrafficServices(values []TrafficService) (map[string]TrafficService, error) {
316	services := make(map[string]TrafficService, len(values))
317	for _, service := range values {
318		if err := validateTrafficService(service, services); err != nil {
319			return nil, err
320		}
321		services[service.Name] = service
322	}
323	return services, nil
324}
325
326func validateTrafficService(service TrafficService, existing map[string]TrafficService) error {
327	if service.Name == "" {
328		return fmt.Errorf("traffic service name is required")
329	}
330	if _, exists := existing[service.Name]; exists {
331		return fmt.Errorf("duplicate traffic service name %q", service.Name)
332	}
333	if _, err := ParseTrafficServiceRef(service.Name); err != nil {
334		return fmt.Errorf("invalid traffic service ref %q: %w", service.Name, err)
335	}
336	for _, backend := range service.Backends {
337		if err := validateTrafficBackend(service.Name, backend); err != nil {
338			return err
339		}
340	}
341	return nil
342}
343
344func validateTrafficBackend(serviceName string, backend TrafficBackend) error {
345	if strings.TrimSpace(backend.Host) == "" {
346		return fmt.Errorf("backend host is required for service %q", serviceName)
347	}
348	if backend.Port < 1 || backend.Port > 65535 {
349		return fmt.Errorf("invalid backend port %d for service %q", backend.Port, serviceName)
350	}
351	if err := validateNetworkProtocol(backend.Protocol); err != nil {
352		return fmt.Errorf("invalid backend protocol for service %q: %w", serviceName, err)
353	}
354	return nil
355}
356
357func (g TrafficGraph) validateRouters(entryPoints map[string]EntryPoint, services map[string]TrafficService) error {
358	state := newRouterValidationState()
359	for _, router := range g.Routers {
360		if err := state.validateRouter(router, entryPoints, services); err != nil {
361			return err
362		}
363	}
364	return validateRawFallbackRouters(entryPoints, state.routersByName)
365}
366
367type routerValidationState struct {
368	routerNames   map[string]struct{}
369	routersByName map[string]TrafficRouter
370	exactSNI      map[string]map[string]string
371	wildSNI       map[string]map[string]string
372	httpHosts     map[string]map[string]string
373	tcpRouters    map[string]string
374	udpRouters    map[string]string
375}
376
377func newRouterValidationState() *routerValidationState {
378	return &routerValidationState{
379		routerNames:   map[string]struct{}{},
380		routersByName: map[string]TrafficRouter{},
381		exactSNI:      map[string]map[string]string{},
382		wildSNI:       map[string]map[string]string{},
383		httpHosts:     map[string]map[string]string{},
384		tcpRouters:    map[string]string{},
385		udpRouters:    map[string]string{},
386	}
387}
388
389func (s *routerValidationState) validateRouter(router TrafficRouter, entryPoints map[string]EntryPoint, services map[string]TrafficService) error {
390	if err := s.validateRouterName(router); err != nil {
391		return err
392	}
393	entryPoint, serviceRef, service, err := resolveRouterTargets(router, entryPoints, services)
394	if err != nil {
395		return err
396	}
397	if err := validateRouterEntryPointProtocol(router, entryPoint); err != nil {
398		return err
399	}
400	if err := validateRouterServiceRef(router, serviceRef, service); err != nil {
401		return err
402	}
403	if err := s.validateRouterRule(router, entryPoint); err != nil {
404		return err
405	}
406	return s.validateTLSPassthroughRule(router, entryPoint)
407}
408
409func (s *routerValidationState) validateRouterName(router TrafficRouter) error {
410	if router.Name == "" {
411		return fmt.Errorf("traffic router name is required")
412	}
413	if _, exists := s.routerNames[router.Name]; exists {
414		return fmt.Errorf("duplicate traffic router name %q", router.Name)
415	}
416	s.routerNames[router.Name] = struct{}{}
417	s.routersByName[router.Name] = router
418	return nil
419}
420
421func resolveRouterTargets(router TrafficRouter, entryPoints map[string]EntryPoint, services map[string]TrafficService) (EntryPoint, TrafficServiceRef, TrafficService, error) {
422	entryPoint, ok := entryPoints[router.EntryPoint]
423	if !ok {
424		return EntryPoint{}, TrafficServiceRef{}, TrafficService{}, fmt.Errorf("router %q references unknown entrypoint %q", router.Name, router.EntryPoint)
425	}
426	serviceRef, err := ParseTrafficServiceRef(router.Service)
427	if err != nil {
428		return EntryPoint{}, TrafficServiceRef{}, TrafficService{}, fmt.Errorf("router %q has invalid service ref: %w", router.Name, err)
429	}
430	service, ok := services[router.Service]
431	if !ok {
432		return EntryPoint{}, TrafficServiceRef{}, TrafficService{}, fmt.Errorf("router %q references unknown service %q", router.Name, router.Service)
433	}
434	if err := validateRouterProtocol(router.Protocol); err != nil {
435		return EntryPoint{}, TrafficServiceRef{}, TrafficService{}, fmt.Errorf("invalid router protocol for %q: %w", router.Name, err)
436	}
437	return entryPoint, serviceRef, service, nil
438}
439
440func (s *routerValidationState) validateRouterRule(router TrafficRouter, entryPoint EntryPoint) error {
441	switch router.Protocol {
442	case RouterProtocolTCP:
443		return validateSingleL4Router(s.tcpRouters, "tcp", entryPoint.Name, router.Name)
444	case RouterProtocolUDP:
445		return validateSingleL4Router(s.udpRouters, "udp", entryPoint.Name, router.Name)
446	case RouterProtocolHTTP:
447		return s.validateHTTPRule(router, entryPoint)
448	default:
449		return nil
450	}
451}
452
453func validateSingleL4Router(routers map[string]string, protocol string, entryPoint string, routerName string) error {
454	if existing := routers[entryPoint]; existing != "" {
455		return fmt.Errorf("ambiguous %s routers on entrypoint %q: %q and %q", protocol, entryPoint, existing, routerName)
456	}
457	routers[entryPoint] = routerName
458	return nil
459}
460
461func (s *routerValidationState) validateHTTPRule(router TrafficRouter, entryPoint EntryPoint) error {
462	if router.Rule.Host == "" {
463		return nil
464	}
465	host := normalizeHost(router.Rule.Host)
466	if existing := s.httpHosts[entryPoint.Name][host]; existing != "" {
467		return fmt.Errorf("duplicate http host %q on entrypoint %q used by routers %q and %q", host, entryPoint.Name, existing, router.Name)
468	}
469	if err := s.validateHTTPRuleTLSConflicts(entryPoint.Name, host); err != nil {
470		return err
471	}
472	putNested(s.httpHosts, entryPoint.Name, host, router.Name)
473	return nil
474}
475
476func (s *routerValidationState) validateHTTPRuleTLSConflicts(entryPoint string, host string) error {
477	if s.exactSNI[entryPoint][host] != "" {
478		return fmt.Errorf("http host conflicts with tls passthrough sni %q on entrypoint %q", host, entryPoint)
479	}
480	if wildcard := matchingWildcardSNI(host, s.wildSNI[entryPoint]); wildcard != "" {
481		return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q", wildcard, entryPoint)
482	}
483	return nil
484}
485
486func (s *routerValidationState) validateTLSPassthroughRule(router TrafficRouter, entryPoint EntryPoint) error {
487	if router.Protocol != RouterProtocolTLSPassthrough {
488		return nil
489	}
490	if strings.TrimSpace(router.Rule.SNI) == "" {
491		return fmt.Errorf("tls passthrough router %q requires sni", router.Name)
492	}
493	sni := normalizeHost(router.Rule.SNI)
494	if strings.HasPrefix(sni, "*.") {
495		return s.validateWildcardSNI(entryPoint.Name, sni, router.Name)
496	}
497	return s.validateExactSNI(entryPoint.Name, sni, router.Name)
498}
499
500func (s *routerValidationState) validateWildcardSNI(entryPoint string, sni string, routerName string) error {
501	if s.wildSNI[entryPoint][sni] != "" {
502		return fmt.Errorf("ambiguous wildcard tls sni %q on entrypoint %q", sni, entryPoint)
503	}
504	if wildcard := overlappingWildcardSNI(sni, s.wildSNI[entryPoint]); wildcard != "" {
505		return fmt.Errorf("ambiguous wildcard tls sni %q overlaps %q on entrypoint %q", sni, wildcard, entryPoint)
506	}
507	if host := matchingHTTPHost(sni, s.httpHosts[entryPoint]); host != "" {
508		return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q for host %q", sni, entryPoint, host)
509	}
510	putNested(s.wildSNI, entryPoint, sni, routerName)
511	return nil
512}
513
514func (s *routerValidationState) validateExactSNI(entryPoint string, sni string, routerName string) error {
515	if s.exactSNI[entryPoint][sni] != "" {
516		return fmt.Errorf("duplicate exact tls sni %q on entrypoint %q", sni, entryPoint)
517	}
518	if s.httpHosts[entryPoint][sni] != "" {
519		return fmt.Errorf("http host conflicts with tls passthrough sni %q on entrypoint %q", sni, entryPoint)
520	}
521	putNested(s.exactSNI, entryPoint, sni, routerName)
522	return nil
523}
524
525func validateRouterEntryPointProtocol(router TrafficRouter, entryPoint EntryPoint) error {
526	compatible := false
527	switch router.Protocol {
528	case RouterProtocolHTTP:
529		compatible = entryPoint.Protocol == EntryPointProtocolHTTP || entryPoint.Protocol == EntryPointProtocolTLSMux || entryPoint.Protocol == EntryPointProtocolSmartTCP
530	case RouterProtocolTLSPassthrough:
531		compatible = entryPoint.Protocol == EntryPointProtocolTLSMux || entryPoint.Protocol == EntryPointProtocolSmartTCP
532	case RouterProtocolTCP:
533		compatible = entryPoint.Protocol == EntryPointProtocolTCP || (entryPoint.Protocol == EntryPointProtocolSmartTCP && entryPoint.RawFallback == router.Name)
534	case RouterProtocolUDP:
535		compatible = entryPoint.Protocol == EntryPointProtocolUDP
536	}
537	if compatible {
538		return nil
539	}
540	return fmt.Errorf("router %q protocol %s is incompatible with entrypoint %q protocol %s", router.Name, router.Protocol, entryPoint.Name, entryPoint.Protocol)
541}
542
543func validateRouterServiceRef(router TrafficRouter, ref TrafficServiceRef, service TrafficService) error {
544	switch router.Protocol {
545	case RouterProtocolHTTP:
546		if ref.Kind == TrafficServiceRefRoute || ref.Kind == TrafficServiceRefExternalRoute {
547			return nil
548		}
549	case RouterProtocolTCP, RouterProtocolUDP, RouterProtocolTLSPassthrough:
550		return validateL4RouterService(router, ref, service)
551	}
552	if ref.Kind == TrafficServiceRefStatic {
553		return fmt.Errorf("static traffic service ref %q is unsupported in traffic graph validation", router.Service)
554	}
555	return fmt.Errorf("router %q protocol %s does not support service ref kind %s", router.Name, router.Protocol, ref.Kind)
556}
557
558func validateL4RouterService(router TrafficRouter, ref TrafficServiceRef, service TrafficService) error {
559	want, ok := l4BackendProtocol(router.Protocol)
560	if !ok {
561		return nil
562	}
563	if ref.Kind != TrafficServiceRefNetworkService && ref.Kind != TrafficServiceRefService {
564		if ref.Kind == TrafficServiceRefStatic {
565			return fmt.Errorf("static traffic service ref %q is unsupported in traffic graph validation", router.Service)
566		}
567		return fmt.Errorf("l4 router %q requires network_service or service service ref", router.Name)
568	}
569	if len(service.Backends) != 1 {
570		return fmt.Errorf("l4 router service must have exactly one backend for router %q", router.Name)
571	}
572	backend := service.Backends[0]
573	if backend.Protocol != want {
574		return fmt.Errorf("backend protocol %s does not match router protocol %s for router %q (expected backend protocol %s)", backend.Protocol, router.Protocol, router.Name, want)
575	}
576	return nil
577}
578
579func l4BackendProtocol(protocol RouterProtocol) (NetworkProtocol, bool) {
580	switch protocol {
581	case RouterProtocolTCP:
582		return NetworkProtocolTCP, true
583	case RouterProtocolUDP:
584		return NetworkProtocolUDP, true
585	case RouterProtocolTLSPassthrough:
586		return NetworkProtocolTCP, true
587	default:
588		return "", false
589	}
590}
591
592func validateRawFallbackEntrypoint(entryPoint EntryPoint) error {
593	if entryPoint.Protocol == EntryPointProtocolSmartTCP {
594		if entryPoint.RawFallback == "" && len(entryPoint.RawFallbackTrustedCIDRs) > 0 {
595			return fmt.Errorf("raw_fallback_trusted_cidrs on entrypoint %q requires raw_fallback", entryPoint.Name)
596		}
597		if entryPoint.RawFallback == "" && entryPoint.AllowPublicRawFallback {
598			return fmt.Errorf("allow_public_raw_fallback on entrypoint %q requires raw_fallback", entryPoint.Name)
599		}
600		return nil
601	}
602	if entryPoint.RawFallback != "" {
603		return fmt.Errorf("raw_fallback is only supported on smart_tcp entrypoint %q", entryPoint.Name)
604	}
605	if len(entryPoint.RawFallbackTrustedCIDRs) > 0 {
606		return fmt.Errorf("raw_fallback_trusted_cidrs is only supported on smart_tcp entrypoint %q", entryPoint.Name)
607	}
608	if entryPoint.AllowPublicRawFallback {
609		return fmt.Errorf("allow_public_raw_fallback is only supported on smart_tcp entrypoint %q", entryPoint.Name)
610	}
611	return nil
612}
613
614func validateTrustedCIDRs(entryPoint EntryPoint) error {
615	for _, cidr := range entryPoint.TrustedCIDRs {
616		if _, _, err := net.ParseCIDR(strings.TrimSpace(cidr)); err != nil {
617			return fmt.Errorf("invalid trusted_cidrs entry %q for entrypoint %q: %w", cidr, entryPoint.Name, err)
618		}
619	}
620	for _, cidr := range entryPoint.RawFallbackTrustedCIDRs {
621		if _, _, err := net.ParseCIDR(strings.TrimSpace(cidr)); err != nil {
622			return fmt.Errorf("invalid raw_fallback_trusted_cidrs entry %q for entrypoint %q: %w", cidr, entryPoint.Name, err)
623		}
624	}
625	if entryPoint.RawFallback != "" && len(entryPoint.RawFallbackTrustedCIDRs) == 0 && !entryPoint.AllowPublicRawFallback {
626		return fmt.Errorf("raw fallback %q on entrypoint %q requires raw_fallback_trusted_cidrs or allow_public_raw_fallback", entryPoint.RawFallback, entryPoint.Name)
627	}
628	return nil
629}
630
631func validateRawFallbackRouters(entryPoints map[string]EntryPoint, routers map[string]TrafficRouter) error {
632	for _, entryPoint := range entryPoints {
633		if entryPoint.RawFallback == "" {
634			continue
635		}
636		router, ok := routers[entryPoint.RawFallback]
637		if !ok {
638			return fmt.Errorf("raw fallback %q on entrypoint %q references unknown router", entryPoint.RawFallback, entryPoint.Name)
639		}
640		if router.EntryPoint != entryPoint.Name {
641			return fmt.Errorf("raw fallback %q on entrypoint %q references router on entrypoint %q", entryPoint.RawFallback, entryPoint.Name, router.EntryPoint)
642		}
643		if router.Protocol != RouterProtocolTCP {
644			return fmt.Errorf("raw fallback %q on entrypoint %q must reference tcp router", entryPoint.RawFallback, entryPoint.Name)
645		}
646	}
647	return nil
648}
649
650func validateEntryPointProtocol(protocol EntryPointProtocol) error {
651	switch protocol {
652	case EntryPointProtocolTLSMux, EntryPointProtocolSmartTCP, EntryPointProtocolTCP, EntryPointProtocolUDP:
653		return nil
654	default:
655		return fmt.Errorf("unsupported traffic entrypoint protocol %q", protocol)
656	}
657}
658
659func validateRouterProtocol(protocol RouterProtocol) error {
660	switch protocol {
661	case RouterProtocolHTTP, RouterProtocolTCP, RouterProtocolUDP, RouterProtocolTLSPassthrough:
662		return nil
663	default:
664		return fmt.Errorf("%q", protocol)
665	}
666}
667
668func validateNetworkProtocol(protocol NetworkProtocol) error {
669	switch protocol {
670	case NetworkProtocolTCP, NetworkProtocolUDP:
671		return nil
672	default:
673		return fmt.Errorf("%q", protocol)
674	}
675}
676
677type listenAddress struct {
678	key      string
679	port     int
680	wildcard bool
681}
682
683type listenAddressSet struct {
684	byKey map[string]string
685	all   []registeredListenAddress
686}
687
688type registeredListenAddress struct {
689	addr listenAddress
690	name string
691}
692
693func newListenAddressSet() listenAddressSet {
694	return listenAddressSet{byKey: map[string]string{}}
695}
696
697func (s *listenAddressSet) add(addr listenAddress, name string, protocol string) error {
698	if other, exists := s.byKey[addr.key]; exists {
699		return fmt.Errorf("duplicate %s entrypoint address %q used by %q and %q", protocol, addr.key, other, name)
700	}
701	for _, existing := range s.all {
702		if existing.addr.port == addr.port && (existing.addr.wildcard || addr.wildcard) {
703			return fmt.Errorf("conflicting %s entrypoint address %q used by %q and %q", protocol, addr.key, existing.name, name)
704		}
705	}
706	s.byKey[addr.key] = name
707	s.all = append(s.all, registeredListenAddress{addr: addr, name: name})
708	return nil
709}
710
711func parseListenAddress(address string) (listenAddress, error) {
712	host, portValue, err := net.SplitHostPort(address)
713	if err != nil {
714		return listenAddress{}, err
715	}
716	port, err := strconv.Atoi(portValue)
717	if err != nil || port < 1 || port > 65535 {
718		return listenAddress{}, fmt.Errorf("invalid port %q", portValue)
719	}
720	return listenAddress{
721		key:      net.JoinHostPort(host, strconv.Itoa(port)),
722		port:     port,
723		wildcard: isWildcardListenHost(host),
724	}, nil
725}
726
727func isWildcardListenHost(host string) bool {
728	return host == "" || host == "0.0.0.0" || host == "::"
729}
730
731func normalizeHost(host string) string {
732	return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")
733}
734
735func matchingWildcardSNI(host string, wildcards map[string]string) string {
736	for wildcard := range wildcards {
737		if hostMatchesWildcard(host, wildcard) {
738			return wildcard
739		}
740	}
741	return ""
742}
743
744func matchingHTTPHost(wildcard string, hosts map[string]string) string {
745	for host := range hosts {
746		if hostMatchesWildcard(host, wildcard) {
747			return host
748		}
749	}
750	return ""
751}
752
753func overlappingWildcardSNI(wildcard string, wildcards map[string]string) string {
754	for existing := range wildcards {
755		if wildcardSNIOverlap(wildcard, existing) {
756			return existing
757		}
758	}
759	return ""
760}
761
762func wildcardSNIOverlap(left string, right string) bool {
763	leftSuffix, leftOK := strings.CutPrefix(left, "*.")
764	rightSuffix, rightOK := strings.CutPrefix(right, "*.")
765	if !leftOK || !rightOK {
766		return false
767	}
768	return leftSuffix == rightSuffix || strings.HasSuffix(leftSuffix, "."+rightSuffix) || strings.HasSuffix(rightSuffix, "."+leftSuffix)
769}
770
771func hostMatchesWildcard(host string, wildcard string) bool {
772	suffix, ok := strings.CutPrefix(wildcard, "*.")
773	if !ok || host == suffix {
774		return false
775	}
776	return strings.HasSuffix(host, "."+suffix)
777}
778
779func putNested(values map[string]map[string]string, key string, nestedKey string, value string) {
780	if values[key] == nil {
781		values[key] = map[string]string{}
782	}
783	values[key][nestedKey] = value
784}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close