main bac21c6

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

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