···11MIT License
2233-Copyright (c) 2025 xe
33+Copyright (c) 2025 Caleb Doxsey and Xe Iaso
4455Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
66
+2
README.md
···11# Hythlodaeus
2233A simple ingress controller for Kubernetes clusters that works with Anubis.
44+55+This is a fork of [simple ingress controller](https://github.com/calebdoxsey/kubernetes-simple-ingress-controller).
···11+package server
22+33+import (
44+ "context"
55+ "sync"
66+)
77+88+// An Event is used to communicate that something has happened.
99+type Event struct {
1010+ once sync.Once
1111+ C chan struct{}
1212+}
1313+1414+// NewEvent creates a new Event.
1515+func NewEvent() *Event {
1616+ return &Event{
1717+ C: make(chan struct{}),
1818+ }
1919+}
2020+2121+// Set sets the event by closing the C channel. After the first time, calls to set are a no-op.
2222+func (e *Event) Set() {
2323+ e.once.Do(func() {
2424+ close(e.C)
2525+ })
2626+}
2727+2828+// Wait waits for the event to get set.
2929+func (e *Event) Wait(ctx context.Context) {
3030+ select {
3131+ case <-ctx.Done():
3232+ case <-e.C:
3333+ }
3434+}
+170
server/route.go
···11+package server
22+33+import (
44+ "crypto/tls"
55+ "errors"
66+ "fmt"
77+ "log/slog"
88+ "net/url"
99+ "regexp"
1010+ "strings"
1111+1212+ "git.xeserv.us/Techaro/hythlodaeus/watcher"
1313+ networking "k8s.io/api/networking/v1"
1414+ "k8s.io/apimachinery/pkg/util/intstr"
1515+)
1616+1717+const BackendProtocolAnnotation = "kubernetes-simple-ingress-controller/backend-protocol"
1818+1919+// A RoutingTable contains the information needed to route a request.
2020+type RoutingTable struct {
2121+ certificatesByHost map[string]map[string]*tls.Certificate
2222+ backendsByHost map[string][]routingTableBackend
2323+}
2424+2525+type routingTableBackend struct {
2626+ pathRE *regexp.Regexp
2727+ url *url.URL
2828+}
2929+3030+func newRoutingTableBackend(scheme, path, serviceName, serviceNamespace string, servicePort int) (routingTableBackend, error) {
3131+ rtb := routingTableBackend{
3232+ url: &url.URL{
3333+ Scheme: scheme,
3434+ Host: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, serviceNamespace),
3535+ },
3636+ }
3737+3838+ if servicePort != 0 {
3939+ rtb.url.Host += fmt.Sprintf(":%d", servicePort)
4040+ }
4141+4242+ var err error
4343+ if path != "" {
4444+ rtb.pathRE, err = regexp.Compile(path)
4545+ }
4646+ return rtb, err
4747+}
4848+4949+func (rtb routingTableBackend) matches(path string) bool {
5050+ if rtb.pathRE == nil {
5151+ return true
5252+ }
5353+ return rtb.pathRE.MatchString(path)
5454+}
5555+5656+// NewRoutingTable creates a new RoutingTable.
5757+func NewRoutingTable(payload *watcher.Payload) *RoutingTable {
5858+ rt := &RoutingTable{
5959+ certificatesByHost: make(map[string]map[string]*tls.Certificate),
6060+ backendsByHost: make(map[string][]routingTableBackend),
6161+ }
6262+ rt.init(payload)
6363+ return rt
6464+}
6565+6666+func (rt *RoutingTable) init(payload *watcher.Payload) {
6767+ if payload == nil {
6868+ return
6969+ }
7070+ for _, ingressPayload := range payload.Ingresses {
7171+ for _, rule := range ingressPayload.Ingress.Spec.Rules {
7272+ m, ok := rt.certificatesByHost[rule.Host]
7373+ if !ok {
7474+ m = make(map[string]*tls.Certificate)
7575+ rt.certificatesByHost[rule.Host] = m
7676+ }
7777+ for _, t := range ingressPayload.Ingress.Spec.TLS {
7878+ for _, h := range t.Hosts {
7979+ cert, ok := payload.TLSCertificates[t.SecretName]
8080+ if ok {
8181+ m[h] = cert
8282+ }
8383+ }
8484+ }
8585+ rt.addBackend(ingressPayload, rule)
8686+ }
8787+ }
8888+}
8989+9090+func (rt *RoutingTable) addBackend(ingressPayload watcher.IngressPayload, rule networking.IngressRule) {
9191+ scheme, ok := ingressPayload.Ingress.Annotations[BackendProtocolAnnotation]
9292+ if !ok {
9393+ scheme = "http"
9494+ }
9595+ scheme = strings.ToLower(scheme)
9696+9797+ if rule.HTTP == nil {
9898+ if ingressPayload.Ingress.Spec.DefaultBackend != nil {
9999+ backend := ingressPayload.Ingress.Spec.DefaultBackend
100100+ rtb, err := newRoutingTableBackend(scheme, "", backend.Service.Name, ingressPayload.Ingress.Namespace,
101101+ rt.getServicePort(ingressPayload, backend.Service.Name, intstr.FromInt(int(backend.Service.Port.Number))))
102102+ if err != nil {
103103+ // this shouldn't happen
104104+ slog.Error("[unexpected] can't add routing table backend", "err", err)
105105+ return
106106+ }
107107+ rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb)
108108+ }
109109+ } else {
110110+ for _, path := range rule.HTTP.Paths {
111111+ backend := path.Backend
112112+ rtb, err := newRoutingTableBackend(scheme, path.Path, backend.Service.Name, ingressPayload.Ingress.Namespace,
113113+ rt.getServicePort(ingressPayload, backend.Service.Name, intstr.FromInt(int(backend.Service.Port.Number))))
114114+ if err != nil {
115115+ slog.Error("can't add routing table backend", "err", err)
116116+ continue
117117+ }
118118+ rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb)
119119+ }
120120+ }
121121+}
122122+123123+func (rt *RoutingTable) getServicePort(ingressPayload watcher.IngressPayload, serviceName string, servicePort intstr.IntOrString) int {
124124+ if servicePort.Type == intstr.Int && servicePort.IntVal != 0 {
125125+ return servicePort.IntValue()
126126+ }
127127+ if m, ok := ingressPayload.ServicePorts[serviceName]; ok {
128128+ return m[servicePort.String()]
129129+ }
130130+ return 80
131131+}
132132+133133+func (rt *RoutingTable) matches(sni string, certHost string) bool {
134134+ for strings.HasPrefix(certHost, "*.") {
135135+ if idx := strings.IndexByte(sni, '.'); idx >= 0 {
136136+ sni = sni[idx+1:]
137137+ } else {
138138+ return false
139139+ }
140140+ certHost = certHost[2:]
141141+ }
142142+ return sni == certHost
143143+}
144144+145145+// GetCertificate gets a certificate.
146146+func (rt *RoutingTable) GetCertificate(sni string) (*tls.Certificate, error) {
147147+ hostCerts, ok := rt.certificatesByHost[sni]
148148+ if ok {
149149+ for h, cert := range hostCerts {
150150+ if rt.matches(sni, h) {
151151+ return cert, nil
152152+ }
153153+ }
154154+ }
155155+ return nil, fmt.Errorf("certificate not found for %s", sni)
156156+}
157157+158158+// GetBackend gets the backend for the given host and path.
159159+func (rt *RoutingTable) GetBackend(host, path string) (*url.URL, error) {
160160+ if idx := strings.IndexByte(host, ':'); idx > 0 {
161161+ host = host[:idx]
162162+ }
163163+ backends := rt.backendsByHost[host]
164164+ for _, backend := range backends {
165165+ if backend.matches(path) {
166166+ return backend.url, nil
167167+ }
168168+ }
169169+ return nil, errors.New("backend not found")
170170+}