├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── controller_http.go ├── controller_tcp.go ├── demo ├── deployment.yaml ├── ingress-controller-stable.yaml ├── ingress-controller.yaml ├── ingress.yaml ├── secret.yaml ├── service.yaml └── tcp-config-map.yaml ├── go.mod ├── go.sum └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: docker/setup-qemu-action@v3 14 | 15 | - uses: docker/setup-buildx-action@v3 16 | 17 | - uses: docker/login-action@v3 18 | with: 19 | username: ${{ vars.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | 22 | - uses: docker/build-push-action@v5 23 | with: 24 | push: true 25 | platforms: linux/amd64,linux/arm64 26 | tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ vars.DOCKERHUB_REPO }}:stable 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine as builder 2 | 3 | RUN apk add --update \ 4 | ca-certificates \ 5 | git \ 6 | && rm -rf /var/cache/apk/* 7 | 8 | ENV CGO_ENABLED=0 9 | WORKDIR /go/src/github.com/valentinalexeev/tailscale-ingress-controller 10 | COPY go.mod go.sum ./ 11 | RUN go mod download 12 | 13 | COPY . . 14 | RUN go install . 15 | 16 | FROM scratch 17 | 18 | COPY --from=builder /go/bin/tailscale-ingress-controller /bin/tailscale-ingress-controller 19 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 20 | 21 | CMD ["/bin/tailscale-ingress-controller"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Wilson 4 | Copyright (c) 2023 Valentin A. Alekseev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VARIANT?=latest 2 | VARIANT_SUFFIX= 3 | REGISTRY?=valentinalexeev 4 | 5 | ifneq (${VARIANT},latest) 6 | VARIANT_SUFFIX=-${VARIANT} 7 | endif 8 | 9 | build: 10 | docker build -t ${REGISTRY}/tailscale-ingress-controller:${VARIANT} --push . 11 | 12 | deploy: 13 | kubectl apply -f demo/ingress-controller${VARIANT_SUFFIX}.yaml 14 | 15 | remove: 16 | kubectl delete -f demo/ingress-controller${VARIANT_SUFFIX}.yaml 17 | 18 | redeploy: remove deploy 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailscale Ingress Controller 2 | 3 | This is a [Kubernetes Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) for [Tailscale](https://tailscale.com/). 4 | The controller will [create a Tailscale node](https://tailscale.com/blog/tsnet-virtual-private-services/) for each host present in an Ingress resource and then route all incoming traffic to the correct backend service. 5 | 6 | Try it out by applying the resources in the demo directory: 7 | ``` 8 | git clone https://github.com/valentinalexeev/tailscale-ingress-controller 9 | cd tailscale-ingress-controller/demo 10 | export TS_AUTHKEY= 11 | sed "s/\$TS_AUTHKEY/$TS_AUTHKEY/g" * | kubectl apply -f - 12 | ``` 13 | 14 | If all goes well, you should be able to access the hello world HTTP demo service at `http://demo` on your Tailscale network. 15 | 16 | ## How it works 17 | 18 | The demo manifests create a demo backend deployment and service, a demo ingress resource, a deployment for the ingress controller, and a secret for your Tailscale key. 19 | The controller will create a Tailscale node with the hostname `demo` and proxy traffic from the Tailscale network to the backend Kubernetes service. 20 | 21 | ### Tailscale SSO 22 | As Tailscale provides authentication information as part of the requests the Ingress Controller is able to supply this information to the services. 23 | 24 | The controller proxy server will parse the remote IP address from Tailscale and add `X-Webauth-User` and `X-Webauth-Name` HTTP headers to the request before forwarding it for the Tailscale login name and display name, respectively. 25 | 26 | The services can be configured to use the provided headers as SSO credentials. See sample use case on [How To Seamlessly Authenticate to Grafana using Tailscale](https://tailscale.com/blog/grafana-auth/) 27 | 28 | ### TLS support 29 | Tailscale provides native HTTPS implementation with certificates by Let's Encrypt. 30 | 31 | If the host is also listed in the `tls` section of the Ingress spec (see comment in the example Ingress to try it), then the Tailscale node will proxy requests from port 443 instead of 80 and [automatically generate a certificate for itself](https://tailscale.com/blog/tls-certs/). 32 | 33 | ### Funnel support 34 | The Ingress Controller allows the use of [Tailscale Funnel](https://tailscale.com/kb/1223/tailscale-funnel/) to expose services to the public network. 35 | 36 | Unlike HTTPS support, to enable Funnel for an Ingress point a custom annotation ``tailscale.com/funnel: "true"`` needs to be added to the resource definition. 37 | ```yaml 38 | --- 39 | apiVersion: networking.k8s.io/v1 40 | kind: Ingress 41 | metadata: 42 | name: tailscale-ingress-funnel 43 | labels: 44 | tailscale.com/funnel: "true" 45 | spec: 46 | rules: 47 | - host: demo-funnel 48 | http: 49 | paths: 50 | - path: / 51 | pathType: Prefix 52 | backend: 53 | service: 54 | name: demo-backend 55 | port: 56 | number: 8080 57 | ``` 58 | 59 | Please refer to the Tailscale documentation on additional opt-in actions (nodeAttrs and ACL tag set-up) required to make Funnel enabled for the services. 60 | 61 | ### TCP service support 62 | The TCP support was inspired by the ``ingress-nginx`` and relies on a dedicated ConfigMap with a mapping between virtual Tailscale nodes and kubernetes services. 63 | 64 | To configure ``tailscale-ingress-controller`` to proxy TCP requests the following settings must be done: 65 | * Create a new ConfigMap that will include service mappings. The notation of the config map is the following: 66 | ```yaml 67 | ... 68 | data: 69 | # .: [/]: 70 | # A sample mapping to allow connection to the Clickhouse native port (deployed from a Bitnami Helm chart) 71 | clickhouse.9000: clickhouse/clickhouse-1687979852:9000 72 | ``` 73 | * Deploy controller with an additional environment variable ``TCP_SERVICES_CONFIGMAP`` set to the name of the newly created ConfigMap. 74 | 75 | ## Future Work 76 | - Support Ingress Classes 77 | - High Availability 78 | 79 | ## Authors 80 | - Michael Wilson http://github.com/mewil 81 | - Valentin Alekseev http://github.com/valentinalexeev -------------------------------------------------------------------------------- /controller_http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "time" 19 | 20 | "github.com/bep/debounce" 21 | v1 "k8s.io/api/networking/v1" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/client-go/informers" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/tools/cache" 26 | "tailscale.com/ipn/store/kubestore" 27 | "tailscale.com/tsnet" 28 | ) 29 | 30 | const INGRESS_CLASS_NAME = "tailscale" 31 | 32 | // HttpController state 33 | type HttpController struct { 34 | // Tailscale authentication key 35 | tsAuthKey string 36 | // Mutex for shared hosts map 37 | mu sync.RWMutex 38 | // HTTP proxies for each Ingress host 39 | hosts map[string]*HttpHost 40 | } 41 | 42 | // State of the HTTP proxy 43 | type HttpHost struct { 44 | // Tailscale leg of the proxy 45 | tsServer *tsnet.Server 46 | // HTTP connection to backoffice service 47 | httpServer *http.Server 48 | // Path prefixes to match this host 49 | pathPrefixes []*HttpHostPath 50 | // Path map to direct to this host 51 | pathMap map[string]*HttpHostPath 52 | // Host state 53 | started, deleted bool 54 | // If Tailscale TLS will be requested for the service 55 | useTls bool 56 | // If Tailscale Funnel will be requested for the service 57 | useFunnel bool 58 | // If TIC will log the requests passing 59 | enableLogging bool 60 | // Version of the HTTP setup to track changes 61 | generation int64 62 | } 63 | 64 | // A path associated with the host 65 | type HttpHostPath struct { 66 | // A matching part of the specification 67 | value string 68 | // If it is an exact match 69 | exact bool 70 | // Reference to the backend service 71 | backend *url.URL 72 | } 73 | 74 | // Create a new HTTP controller with a specified Tailscale auth key 75 | func NewHttpController(tsAuthKey string) *HttpController { 76 | return &HttpController{ 77 | tsAuthKey: tsAuthKey, 78 | mu: sync.RWMutex{}, 79 | hosts: make(map[string]*HttpHost), 80 | } 81 | } 82 | 83 | // Find a backend target for the specific host and incoming request 84 | func (c *HttpController) getBackendUrl(host, path string, rawquery string) (*url.URL, error) { 85 | c.mu.RLock() 86 | defer c.mu.RUnlock() 87 | h, ok := c.hosts[host] 88 | if !ok { 89 | return nil, fmt.Errorf("host not found") 90 | } 91 | if _, ok = h.pathMap[path]; ok { 92 | return h.pathMap[path].backend, nil 93 | } 94 | for _, p := range h.pathPrefixes { 95 | if strings.HasPrefix(path, p.value) { 96 | return &url.URL{ 97 | Scheme: p.backend.Scheme, 98 | Host: p.backend.Host, 99 | Path: path, 100 | RawQuery: rawquery, 101 | }, nil 102 | } 103 | } 104 | return nil, fmt.Errorf("path not found") 105 | } 106 | 107 | // Generate a tsnet state folder name at the specific prefix and host 108 | func generateTsDir(prefix, host string) (*string, error) { 109 | confDir, err := os.UserConfigDir() 110 | if err != nil { 111 | return nil, fmt.Errorf("failed to get user config dir: %s", err.Error()) 112 | } 113 | dir := filepath.Join(confDir, prefix, host) 114 | if err = os.MkdirAll(dir, 0755); err != nil { 115 | return nil, fmt.Errorf("failed to create config dir: %s", err.Error()) 116 | } 117 | return &dir, nil 118 | } 119 | 120 | // Turn K8s service name into an actual port number (if necessary) 121 | func resolveTargetAddress(targetAddress, targetPort string) (*string, error) { 122 | var fullTargetAddress string 123 | // check if targetPort is number or service name 124 | if targetPortNumber, err := strconv.Atoi(targetPort); err == nil { 125 | fullTargetAddress = fmt.Sprintf("%s:%d", targetAddress, targetPortNumber) 126 | } else { 127 | // targetPort is a service name, must resolve 128 | _, addrs, err := net.LookupSRV(targetPort, "tcp", targetAddress) 129 | var port int16 130 | if err == nil { 131 | for _, service := range addrs { 132 | // XXX: is there a possibility of multiple answers for the k8s SRV request? 133 | port = int16(service.Port) 134 | break 135 | } 136 | } else { 137 | log.Printf("TIC: Unable to resolve service to port number: %s: %s", targetPort, err.Error()) 138 | return nil, fmt.Errorf("unable to resolve service to port number %s: %s", targetPort, err.Error()) 139 | } 140 | fullTargetAddress = fmt.Sprintf("%s:%d", targetAddress, port) 141 | } 142 | return &fullTargetAddress, nil 143 | } 144 | 145 | // Refresh controller state from the set of Ingress objects 146 | func (c *HttpController) update(payload *update) { 147 | c.mu.Lock() 148 | defer c.mu.Unlock() 149 | for h := range c.hosts { 150 | c.hosts[h].deleted = true 151 | } 152 | for _, ingress := range payload.ingresses { 153 | ingressClassName := "" 154 | if ingress.Spec.IngressClassName != nil { 155 | ingressClassName = *ingress.Spec.IngressClassName 156 | } 157 | 158 | if ingressClassName != INGRESS_CLASS_NAME { 159 | log.Printf("TIC: skipping %s as the ingressClassName %s is not for TIC", ingress.Name, ingressClassName) 160 | continue 161 | } 162 | 163 | tlsHosts := make(map[string]struct{}) 164 | _, useFunnel := ingress.Labels["tailscale.com/funnel"] 165 | _, enableLogging := ingress.Labels["tailscale.com/logging"] 166 | _, enableWebClient := ingress.Labels["tailscale.com/webclient"] 167 | 168 | for _, t := range ingress.Spec.TLS { 169 | for _, h := range t.Hosts { 170 | tlsHosts[h] = struct{}{} 171 | } 172 | } 173 | for _, rule := range ingress.Spec.Rules { 174 | if rule.Host == "" { 175 | log.Println("TIC: ignoring ingress rule without host") 176 | continue 177 | } 178 | if strings.Contains(rule.Host, "*") { 179 | log.Println("TIC: ignoring ingress rule with wildcard host") 180 | continue 181 | } 182 | if rule.HTTP == nil { 183 | log.Println("TIC: ignoring ingress rule without http") 184 | continue 185 | } 186 | existingHost, ok := c.hosts[rule.Host] 187 | if !ok || existingHost.generation < ingress.Generation { 188 | if ok { 189 | // We already have a host with the same name but now the resource configuration 190 | // is updated. We need to re-create the host with any new settings. 191 | log.Printf("TIC: Ingress definition for host %s changed from %d to %d, restarting Tailscale host", 192 | rule.Host, 193 | existingHost.generation, 194 | ingress.Generation, 195 | ) 196 | existingHost.tsServer.Close() 197 | delete(c.hosts, rule.Host) 198 | } 199 | 200 | dir, err := generateTsDir("ts", rule.Host) 201 | 202 | if err != nil { 203 | log.Printf("TIC: unable to create dir for tsnet: %s", err.Error()) 204 | continue 205 | } 206 | 207 | _, useTls := tlsHosts[rule.Host] 208 | 209 | kubeStore, err := kubestore.New(log.Printf, fmt.Sprintf("ts-%s", rule.Host)) 210 | 211 | if err != nil { 212 | log.Printf("TIC: unable to create kubestore: %s", err.Error()) 213 | } 214 | 215 | c.hosts[rule.Host] = &HttpHost{ 216 | tsServer: &tsnet.Server{ 217 | Dir: *dir, 218 | Store: kubeStore, 219 | Hostname: rule.Host, 220 | Ephemeral: true, 221 | AuthKey: c.tsAuthKey, 222 | Logf: nil, 223 | RunWebClient: enableWebClient, 224 | }, 225 | useTls: useTls, 226 | useFunnel: useFunnel, 227 | enableLogging: enableLogging, 228 | generation: ingress.Generation, 229 | } 230 | } 231 | c.hosts[rule.Host].deleted = false 232 | if ingress.Spec.DefaultBackend != nil { 233 | log.Println("TIC: ignoring ingress default backend") 234 | continue 235 | } 236 | 237 | for _, path := range rule.HTTP.Paths { 238 | if _, ok = c.hosts[rule.Host].pathMap[path.Path]; !ok { 239 | c.hosts[rule.Host].pathMap = make(map[string]*HttpHostPath, 0) 240 | } 241 | if path.PathType == nil { 242 | log.Println("TIC: ignoring ingress path without path type") 243 | continue 244 | } 245 | 246 | var fullTargetAddress string 247 | 248 | // port can be given as a service name or as a number 249 | if path.Backend.Service.Port.Name != "" { 250 | resolvedAddress, err := resolveTargetAddress( 251 | fmt.Sprintf("%s.%s.svc.cluster.local", path.Backend.Service.Name, ingress.Namespace), 252 | path.Backend.Service.Port.Name, 253 | ) 254 | 255 | if err != nil { 256 | log.Printf("TIC: Unable to resolve target address: %v", err.Error()) 257 | continue 258 | } 259 | fullTargetAddress = *resolvedAddress 260 | } else { 261 | fullTargetAddress = fmt.Sprintf( 262 | "%s.%s.svc.cluster.local:%d", 263 | path.Backend.Service.Name, 264 | ingress.Namespace, 265 | path.Backend.Service.Port.Number, 266 | ) 267 | } 268 | 269 | p := &HttpHostPath{ 270 | value: path.Path, 271 | exact: *path.PathType == v1.PathTypeExact, 272 | backend: &url.URL{ 273 | Scheme: "http", 274 | Host: fullTargetAddress, 275 | }, 276 | } 277 | 278 | c.hosts[rule.Host].pathMap[p.value] = p 279 | if !p.exact { 280 | appendSorted := func(l []*HttpHostPath, e *HttpHostPath) []*HttpHostPath { 281 | i := sort.Search(len(l), func(i int) bool { 282 | return len(l[i].value) < len(e.value) 283 | }) 284 | if i == len(l) { 285 | return append(l, e) 286 | } 287 | l = append(l, &HttpHostPath{}) 288 | copy(l[i+1:], l[i:]) 289 | l[i] = e 290 | return l 291 | } 292 | c.hosts[rule.Host].pathPrefixes = appendSorted(c.hosts[rule.Host].pathPrefixes, p) 293 | } 294 | } 295 | } 296 | } 297 | for n, h := range c.hosts { 298 | if h.deleted { 299 | log.Println("TIC: deleting host ", n) 300 | if err := h.httpServer.Close(); err != nil { 301 | log.Printf("TIC: failed to close http server: %v", err) 302 | } 303 | if err := h.tsServer.Close(); err != nil { 304 | log.Printf("TIC: failed to close ts server: %v", err) 305 | } 306 | delete(c.hosts, n) 307 | continue 308 | } 309 | if h.started { 310 | log.Printf("TIC: host %s already started", n) 311 | continue 312 | } 313 | 314 | var ln net.Listener 315 | var err error 316 | 317 | if h.useFunnel { 318 | ln, err = h.tsServer.ListenFunnel("tcp", ":443") 319 | } else if h.useTls { 320 | ln, err = h.tsServer.Listen("tcp", ":443") 321 | } else { 322 | ln, err = h.tsServer.Listen("tcp", ":80") 323 | } 324 | if err != nil { 325 | log.Println("TIC: failed to listen: ", err) 326 | continue 327 | } 328 | lc, err := h.tsServer.LocalClient() 329 | if err != nil { 330 | log.Println("TIC: failed to get local client: ", err) 331 | continue 332 | } 333 | if h.useTls { 334 | ln = tls.NewListener(ln, &tls.Config{ 335 | GetCertificate: lc.GetCertificate, 336 | }) 337 | } 338 | 339 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 340 | // Hack since the host will include a tailnet name when using TLS. 341 | rh, _, _ := strings.Cut(r.Host, ".") 342 | backendURL, err := c.getBackendUrl(rh, r.URL.Path, r.URL.RawQuery) 343 | if err != nil { 344 | log.Printf("TIC: upstream server %s not found: %s", rh, err.Error()) 345 | http.Error(w, fmt.Sprintf("upstream server %s not found", rh), http.StatusNotFound) 346 | return 347 | } 348 | // TODO: optional request logging 349 | director := func(req *http.Request) { 350 | req.URL = backendURL 351 | who, err := lc.WhoIs(req.Context(), req.RemoteAddr) 352 | if err != nil { 353 | log.Println("TIC: failed to get the owner of the request") 354 | return 355 | } 356 | if who.UserProfile == nil { 357 | log.Println("TIC: user profile is nil") 358 | return 359 | } 360 | req.Header.Set("X-Webauth-User", who.UserProfile.LoginName) 361 | req.Header.Set("X-Webauth-Name", who.UserProfile.DisplayName) 362 | if h.enableLogging { 363 | log.Printf("TIC: Proxying HTTP request for host %s to [%s]", r.Host, backendURL) 364 | } 365 | } 366 | proxy := &httputil.ReverseProxy{Director: director} 367 | proxy.ServeHTTP(w, r) 368 | }) 369 | 370 | srv := http.Server{Handler: handler} 371 | c.hosts[n].httpServer = &srv 372 | go func() { 373 | log.Printf("TIC: Started HTTP proxy for host [%s]", n) 374 | if err := srv.Serve(ln); err != nil { 375 | log.Println("TIC: failed to serve: ", err) 376 | } 377 | }() 378 | c.hosts[n].started = true 379 | } 380 | } 381 | 382 | // Shutdown HTTP reverse proxies 383 | func (c *HttpController) shutdown() { 384 | c.mu.Lock() 385 | defer c.mu.Unlock() 386 | // shutdown HTTP proxies 387 | for n, h := range c.hosts { 388 | if h.started { 389 | log.Printf("TIC: deleting host %s", n) 390 | if err := h.httpServer.Close(); err != nil { 391 | log.Printf("TIC: failed to close http server: %v", err) 392 | } 393 | if err := h.tsServer.Close(); err != nil { 394 | log.Printf("TIC: failed to close ts server: %v", err) 395 | } 396 | delete(c.hosts, n) 397 | } 398 | } 399 | } 400 | 401 | type update struct { 402 | ingresses []*v1.Ingress 403 | } 404 | 405 | // Listen to updates on the Ingress objects 406 | // @param ctx Go context to operate in 407 | // @param client a K8s client interface 408 | func (c *HttpController) listen(ctx context.Context, client kubernetes.Interface) { 409 | factory := informers.NewSharedInformerFactory(client, time.Minute) 410 | ingressLister := factory.Networking().V1().Ingresses().Lister() 411 | 412 | onChange := func() { 413 | ingresses, err := ingressLister.List(labels.Everything()) 414 | if err != nil { 415 | log.Printf("TIC: failed to list ingresses: %s", err) 416 | return 417 | } 418 | log.Printf("TIC: Ingress items to review=%d", len(ingresses)) 419 | c.update(&update{ingresses}) 420 | } 421 | 422 | debounced := debounce.New(time.Second) 423 | eventHandler := cache.ResourceEventHandlerFuncs{ 424 | AddFunc: func(any) { debounced(onChange) }, 425 | UpdateFunc: func(any, any) { debounced(onChange) }, 426 | DeleteFunc: func(any) { debounced(onChange) }, 427 | } 428 | 429 | go func() { 430 | i := factory.Networking().V1().Ingresses().Informer() 431 | i.AddEventHandler(eventHandler) 432 | i.Run(ctx.Done()) 433 | }() 434 | go func() { 435 | i := factory.Core().V1().Services().Informer() 436 | i.AddEventHandler(eventHandler) 437 | i.Run(ctx.Done()) 438 | }() 439 | } 440 | -------------------------------------------------------------------------------- /controller_tcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/bep/debounce" 14 | "inet.af/tcpproxy" 15 | corev1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/labels" 17 | "k8s.io/client-go/informers" 18 | "k8s.io/client-go/kubernetes" 19 | "k8s.io/client-go/tools/cache" 20 | "tailscale.com/ipn/store/kubestore" 21 | "tailscale.com/tsnet" 22 | ) 23 | 24 | // TcpController state 25 | type TcpController struct { 26 | // Tailscale auth key 27 | tsAuthKey string 28 | // Mutex to control access to shared hosts structure 29 | mu sync.RWMutex 30 | // Map of TcpHost proxies 31 | hosts map[string]*TcpHost 32 | } 33 | 34 | // An individual TCP proxy server 35 | type TcpHost struct { 36 | // Tailscale leg of the proxy 37 | tsServer *tsnet.Server 38 | // Backend service proxy 39 | proxy *tcpproxy.Proxy 40 | // ConfigMap parameters signature to check 41 | // if configuration was updated 42 | signature string 43 | } 44 | 45 | // Create a new controller with a specified tsAuthKey for Tailscale 46 | func NewTcpController(tsAuthKey string) *TcpController { 47 | return &TcpController{ 48 | tsAuthKey: tsAuthKey, 49 | mu: sync.RWMutex{}, 50 | hosts: make(map[string]*TcpHost), 51 | } 52 | } 53 | 54 | // Update controller state with the data from ConfigMap 55 | func (c *TcpController) update(payload *updateConfigMap) { 56 | c.mu.Lock() 57 | defer c.mu.Unlock() 58 | 59 | for _, configMap := range payload.configMaps { 60 | if configMap.Name != os.Getenv("TCP_SERVICES_CONFIGMAP") { 61 | continue 62 | } 63 | 64 | aliveHosts := make(map[string]bool) 65 | 66 | // go through the ConfigMap to re-create services that were changed 67 | for sourceSpec, targetSpec := range configMap.Data { 68 | // tailnet-host-name.port 69 | tailnetHost, tailnetPort, ok := strings.Cut(sourceSpec, ".") 70 | if !ok { 71 | log.Printf("TIC: Invalid tailnet spec [%s], must be . format", sourceSpec) 72 | continue 73 | } 74 | // [namespace/]service:port 75 | targetServiceRef, targetPort, ok := strings.Cut(targetSpec, ":") 76 | if !ok { 77 | log.Printf("TIC: Invalid target spec [%s], must be [/]: format", sourceSpec) 78 | continue 79 | } 80 | 81 | aliveHosts[sourceSpec] = true 82 | 83 | oldHost, ok := c.hosts[sourceSpec] 84 | 85 | if ok { 86 | // there is already a TCP proxy host with this name 87 | if oldHost.signature != fmt.Sprintf("%s: %s", sourceSpec, targetSpec) { 88 | // if host signature does not match — re-create 89 | log.Printf("TIC: Host [%s] was updated, re-creating", sourceSpec) 90 | oldHost.proxy.Close() 91 | oldHost.tsServer.Close() 92 | delete(c.hosts, tailnetHost) 93 | } else { 94 | // skip host if signature is the same 95 | log.Printf("TIC: Host [%s] was not changed, skipping", sourceSpec) 96 | continue 97 | } 98 | } 99 | 100 | // construct target service address 101 | var targetAddress string 102 | var fullTargetAddress *string 103 | 104 | targetNamespace, targetService, found := strings.Cut(targetServiceRef, "/") 105 | if found { 106 | // generate FQDN 107 | targetAddress = fmt.Sprintf("%s.%s.svc.cluster.local", targetService, targetNamespace) 108 | } else { 109 | // assume same namespace 110 | targetAddress = targetServiceRef 111 | } 112 | 113 | fullTargetAddress, err := resolveTargetAddress(targetAddress, targetPort) 114 | 115 | if err != nil { 116 | log.Printf("TIC: unable to resolve target address %v", err) 117 | continue 118 | } 119 | 120 | dir, err := generateTsDir("tsproxy", tailnetHost) 121 | 122 | if err != nil { 123 | log.Printf("TIC: Unable to create dir for tsnet: %s", err.Error()) 124 | continue 125 | } 126 | 127 | kubeStore, err := kubestore.New(log.Printf, fmt.Sprintf("tsproxy-%s", tailnetHost)) 128 | 129 | if err != nil { 130 | log.Printf("TIC: unable to create kubestore: %s", err.Error()) 131 | } 132 | 133 | // initialize tsnet 134 | tsServer := &tsnet.Server{ 135 | Dir: *dir, 136 | Hostname: tailnetHost, 137 | Ephemeral: true, 138 | AuthKey: c.tsAuthKey, 139 | Logf: nil, 140 | Store: kubeStore, 141 | } 142 | 143 | // setup proxy 144 | proxy := &tcpproxy.Proxy{ 145 | ListenFunc: func(net, laddr string) (net.Listener, error) { 146 | return tsServer.Listen(net, laddr) 147 | }, 148 | } 149 | 150 | signature := fmt.Sprintf("%s: %s", sourceSpec, targetSpec) 151 | 152 | c.hosts[sourceSpec] = &TcpHost{ 153 | tsServer, 154 | proxy, 155 | signature, 156 | } 157 | proxy.AddRoute(":"+tailnetPort, tcpproxy.To(*fullTargetAddress)) 158 | 159 | // launch a dedicated goroutine with the proxy 160 | go func() { 161 | log.Printf("TIC: Starting TCP proxy %s:%s -> %s", tailnetHost, tailnetPort, *fullTargetAddress) 162 | proxy.Run() 163 | }() 164 | } 165 | 166 | // remove hosts that are no longer present in the ConfigMap 167 | for idx, host := range c.hosts { 168 | if _, ok := aliveHosts[idx]; !ok { 169 | log.Printf("TIC: host [%s] no longer alive in ConfigMap, removing", idx) 170 | // if host was not found in the alive hosts 171 | host.proxy.Close() 172 | host.tsServer.Close() 173 | delete(c.hosts, idx) 174 | } 175 | } 176 | } 177 | } 178 | 179 | // Shutdown all TCP proxy connections and listeners. 180 | func (c *TcpController) shutdown() { 181 | c.mu.Lock() 182 | defer c.mu.Unlock() 183 | // shutdown TCP proxies 184 | for idx, tcpHost := range c.hosts { 185 | if err := tcpHost.proxy.Close(); err != nil { 186 | log.Printf("Unable to close TCP proxy: %v", err) 187 | } 188 | if err := tcpHost.tsServer.Close(); err != nil { 189 | log.Printf("Unable to close ts server: %v", err) 190 | } 191 | delete(c.hosts, idx) 192 | } 193 | } 194 | 195 | type updateConfigMap struct { 196 | configMaps []*corev1.ConfigMap 197 | } 198 | 199 | func (c *TcpController) listen(ctx context.Context, client kubernetes.Interface) { 200 | factory := informers.NewSharedInformerFactory(client, time.Minute) 201 | configMapLister := factory.Core().V1().ConfigMaps().Lister() 202 | 203 | onConfigMapChange := func() { 204 | configMaps, err := configMapLister.List(labels.Everything()) 205 | if err != nil { 206 | log.Println("failed to list config maps: ", err) 207 | return 208 | } 209 | log.Printf("onChange configmap") 210 | c.update(&updateConfigMap{configMaps}) 211 | } 212 | debounced := debounce.New(time.Second) 213 | 214 | eventHandlerConfig := cache.ResourceEventHandlerFuncs{ 215 | AddFunc: func(any) { debounced(onConfigMapChange) }, 216 | UpdateFunc: func(any, any) { debounced(onConfigMapChange) }, 217 | DeleteFunc: func(any) { debounced(onConfigMapChange) }, 218 | } 219 | 220 | go func() { 221 | i := factory.Core().V1().ConfigMaps().Informer() 222 | i.AddEventHandler(eventHandlerConfig) 223 | i.Run(ctx.Done()) 224 | }() 225 | <-ctx.Done() 226 | } 227 | -------------------------------------------------------------------------------- /demo/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: demo-backend-deployment 6 | labels: 7 | app: demo-backend 8 | spec: 9 | replicas: 2 10 | selector: 11 | matchLabels: 12 | app: demo 13 | role: backend 14 | template: 15 | metadata: 16 | labels: 17 | app: demo 18 | role: backend 19 | spec: 20 | containers: 21 | - name: demo-backend 22 | image: strm/helloworld-http:latest 23 | ports: 24 | - containerPort: 80 25 | -------------------------------------------------------------------------------- /demo/ingress-controller-stable.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: tailscale-ingress-controller 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: tailscale-ingress-controller 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - "secrets" 16 | verbs: 17 | - "get" 18 | - "watch" 19 | - "list" 20 | - "create" 21 | - "update" 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - "services" 26 | - "configmaps" 27 | verbs: 28 | - "get" 29 | - "watch" 30 | - "list" 31 | - apiGroups: 32 | - "extensions" 33 | - "networking.k8s.io" 34 | resources: 35 | - "ingresses" 36 | verbs: 37 | - "get" 38 | - "watch" 39 | - "list" 40 | --- 41 | apiVersion: rbac.authorization.k8s.io/v1 42 | kind: ClusterRoleBinding 43 | metadata: 44 | name: tailscale-ingress-controller-binding 45 | roleRef: 46 | apiGroup: rbac.authorization.k8s.io 47 | kind: ClusterRole 48 | name: tailscale-ingress-controller 49 | subjects: 50 | - kind: ServiceAccount 51 | name: tailscale-ingress-controller 52 | namespace: default 53 | --- 54 | apiVersion: apps/v1 55 | kind: Deployment 56 | metadata: 57 | name: tailscale-ingress-controller 58 | namespace: default 59 | labels: 60 | app: ingress-controller 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app: ingress-controller 66 | template: 67 | metadata: 68 | labels: 69 | app: ingress-controller 70 | spec: 71 | serviceAccountName: tailscale-ingress-controller 72 | containers: 73 | - name: tailscale-ingress-controller 74 | image: valentinalexeev/tailscale-ingress-controller:stable 75 | resources: 76 | requests: 77 | cpu: "0.1" 78 | memory: "64Mi" 79 | limits: 80 | cpu: "0.1" 81 | memory: "128Mi" 82 | env: 83 | - name: TS_AUTHKEY 84 | valueFrom: 85 | secretKeyRef: 86 | name: tailscale-auth 87 | key: key 88 | - name: TCP_SERVICES_CONFIGMAP 89 | value: tcp-services 90 | -------------------------------------------------------------------------------- /demo/ingress-controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: tailscale-ingress-controller 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: tailscale-ingress-controller 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - "secrets" 16 | verbs: 17 | - "get" 18 | - "watch" 19 | - "list" 20 | - "create" 21 | - "update" 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - "services" 26 | - "configmaps" 27 | verbs: 28 | - "get" 29 | - "watch" 30 | - "list" 31 | - apiGroups: 32 | - "extensions" 33 | - "networking.k8s.io" 34 | resources: 35 | - "ingresses" 36 | verbs: 37 | - "get" 38 | - "watch" 39 | - "list" 40 | --- 41 | apiVersion: rbac.authorization.k8s.io/v1 42 | kind: ClusterRoleBinding 43 | metadata: 44 | name: tailscale-ingress-controller-binding 45 | roleRef: 46 | apiGroup: rbac.authorization.k8s.io 47 | kind: ClusterRole 48 | name: tailscale-ingress-controller 49 | subjects: 50 | - kind: ServiceAccount 51 | name: tailscale-ingress-controller 52 | namespace: default 53 | --- 54 | apiVersion: apps/v1 55 | kind: Deployment 56 | metadata: 57 | name: tailscale-ingress-controller 58 | namespace: default 59 | labels: 60 | app: ingress-controller 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app: ingress-controller 66 | template: 67 | metadata: 68 | labels: 69 | app: ingress-controller 70 | spec: 71 | serviceAccountName: tailscale-ingress-controller 72 | containers: 73 | - name: tailscale-ingress-controller 74 | image: valentinalexeev/tailscale-ingress-controller:latest 75 | resources: 76 | requests: 77 | cpu: "0.1" 78 | memory: "64Mi" 79 | limits: 80 | cpu: "0.1" 81 | memory: "128Mi" 82 | env: 83 | - name: TS_AUTHKEY 84 | valueFrom: 85 | secretKeyRef: 86 | name: tailscale-auth 87 | key: key 88 | - name: TCP_SERVICES_CONFIGMAP 89 | value: tcp-services 90 | -------------------------------------------------------------------------------- /demo/ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: tailscale-ingress 6 | spec: 7 | # Uncomment the tls block below to generate a certificate for your Tailscale node 8 | # (Requires going to "Configure HTTPS" in the Tailscale admin panel) 9 | # tls: 10 | # - hosts: 11 | # - demo 12 | rules: 13 | - host: demo 14 | http: 15 | paths: 16 | - path: / 17 | pathType: Prefix 18 | backend: 19 | service: 20 | name: demo-backend 21 | port: 22 | number: 8080 23 | -------------------------------------------------------------------------------- /demo/secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | type: Opaque 5 | metadata: 6 | name: tailscale-auth 7 | stringData: 8 | key: $TS_AUTHKEY 9 | -------------------------------------------------------------------------------- /demo/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: demo-backend 6 | labels: 7 | app: demo-backend 8 | spec: 9 | ports: 10 | - port: 8080 11 | protocol: TCP 12 | targetPort: 80 13 | selector: 14 | app: demo 15 | role: backend 16 | -------------------------------------------------------------------------------- /demo/tcp-config-map.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: tcp-services 6 | namespace: default 7 | data: 8 | clickhouse.9000: clickhouse/clickhouse-1687979852:tcp 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/valentinalexeev/tailscale-ingress-controller 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/bep/debounce v1.2.1 7 | inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 8 | k8s.io/api v0.32.2 9 | k8s.io/apimachinery v0.32.2 10 | k8s.io/client-go v0.32.2 11 | tailscale.com v1.80.2 12 | ) 13 | 14 | require ( 15 | filippo.io/edwards25519 v1.1.0 // indirect 16 | github.com/akutz/memconn v0.1.0 // indirect 17 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 18 | github.com/aws/aws-sdk-go-v2 v1.25.2 // indirect 19 | github.com/aws/aws-sdk-go-v2/config v1.27.4 // indirect 20 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4 // indirect 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect 31 | github.com/aws/smithy-go v1.20.1 // indirect 32 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 33 | github.com/coder/websocket v1.8.12 // indirect 34 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 36 | github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect 37 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect 38 | github.com/emicklei/go-restful/v3 v3.11.3 // indirect 39 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 40 | github.com/gaissmai/bart v0.11.1 // indirect 41 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect 42 | github.com/go-logr/logr v1.4.2 // indirect 43 | github.com/go-ole/go-ole v1.3.0 // indirect 44 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 45 | github.com/go-openapi/jsonreference v0.20.4 // indirect 46 | github.com/go-openapi/swag v0.23.0 // indirect 47 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 50 | github.com/golang/protobuf v1.5.4 // indirect 51 | github.com/google/btree v1.1.2 // indirect 52 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 53 | github.com/google/go-cmp v0.6.0 // indirect 54 | github.com/google/gofuzz v1.2.0 // indirect 55 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect 56 | github.com/google/uuid v1.6.0 // indirect 57 | github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect 58 | github.com/gorilla/securecookie v1.1.2 // indirect 59 | github.com/hdevalence/ed25519consensus v0.2.0 // indirect 60 | github.com/illarion/gonotify/v2 v2.0.3 // indirect 61 | github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 // indirect 62 | github.com/jmespath/go-jmespath v0.4.0 // indirect 63 | github.com/josharian/intern v1.0.0 // indirect 64 | github.com/jsimonetti/rtnetlink v1.4.1 // indirect 65 | github.com/json-iterator/go v1.1.12 // indirect 66 | github.com/klauspost/compress v1.17.11 // indirect 67 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect 68 | github.com/mailru/easyjson v0.7.7 // indirect 69 | github.com/mdlayher/genetlink v1.3.2 // indirect 70 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect 71 | github.com/mdlayher/sdnotify v1.0.0 // indirect 72 | github.com/mdlayher/socket v0.5.0 // indirect 73 | github.com/miekg/dns v1.1.58 // indirect 74 | github.com/mitchellh/go-ps v1.0.0 // indirect 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 76 | github.com/modern-go/reflect2 v1.0.2 // indirect 77 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 78 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 79 | github.com/pkg/errors v0.9.1 // indirect 80 | github.com/prometheus-community/pro-bing v0.4.0 // indirect 81 | github.com/safchain/ethtool v0.3.0 // indirect 82 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect 83 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect 84 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect 85 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect 86 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect 87 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect 88 | github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect 89 | github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect 90 | github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 // indirect 91 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect 92 | github.com/vishvananda/netns v0.0.4 // indirect 93 | github.com/x448/float16 v0.8.4 // indirect 94 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect 95 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 96 | golang.org/x/crypto v0.33.0 // indirect 97 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 98 | golang.org/x/mod v0.22.0 // indirect 99 | golang.org/x/net v0.35.0 // indirect 100 | golang.org/x/oauth2 v0.25.0 // indirect 101 | golang.org/x/sync v0.11.0 // indirect 102 | golang.org/x/sys v0.30.0 // indirect 103 | golang.org/x/term v0.29.0 // indirect 104 | golang.org/x/text v0.22.0 // indirect 105 | golang.org/x/time v0.9.0 // indirect 106 | golang.org/x/tools v0.29.0 // indirect 107 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 108 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 109 | google.golang.org/protobuf v1.35.1 // indirect 110 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 111 | gopkg.in/inf.v0 v0.9.1 // indirect 112 | gopkg.in/yaml.v3 v3.0.1 // indirect 113 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect 114 | k8s.io/klog/v2 v2.130.1 // indirect 115 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 116 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 117 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 118 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 119 | sigs.k8s.io/yaml v1.4.0 // indirect 120 | ) 121 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= 4 | filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= 5 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 6 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= 8 | github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= 9 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 10 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 11 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 13 | github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= 14 | github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= 15 | github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= 16 | github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= 17 | github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g= 18 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI= 19 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0= 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= 25 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= 26 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= 29 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk= 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0= 32 | github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1 h1:MeYuN4Ld4FWVJb9ZiOJkon7/foj0Zm2GTDorSaInHj4= 33 | github.com/aws/aws-sdk-go-v2/service/ssm v1.49.1/go.mod h1:TM0pqkfTRMVtsMlPnOivUmrZSIANsLbq9FTm4oJPcPQ= 34 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ= 35 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8= 36 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw= 37 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= 38 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew= 39 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= 40 | github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= 41 | github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 42 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 43 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 44 | github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= 45 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 46 | github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= 47 | github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 48 | github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= 49 | github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 50 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= 51 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= 52 | github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= 53 | github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 54 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 57 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 | github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 h1:vrC07UZcgPzu/OjWsmQKMGg3LoPSz9jh/pQXIrHjUj4= 59 | github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= 60 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= 61 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= 62 | github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= 63 | github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= 64 | github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= 65 | github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= 66 | github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE= 67 | github.com/emicklei/go-restful/v3 v3.11.3/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 68 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 69 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 70 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 71 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 72 | github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= 73 | github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= 74 | github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= 75 | github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= 76 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84= 77 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 78 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 79 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 80 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 81 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 82 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 83 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 84 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 85 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 86 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 87 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 88 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 89 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 90 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= 91 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= 92 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 93 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 94 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 95 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 97 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 98 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 99 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 100 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= 101 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= 102 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 103 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 104 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 105 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 106 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 107 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 108 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= 109 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= 110 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 111 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 112 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 113 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 114 | github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M= 115 | github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= 116 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 117 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 118 | github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= 119 | github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= 120 | github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= 121 | github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= 122 | github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 h1:V3plQrMHRWOB5zMm3yNqvBxDQVW1+/wHBSok5uPdmVs= 123 | github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= 124 | github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= 125 | github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= 126 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 127 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 128 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 129 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 130 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 131 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 132 | github.com/jsimonetti/rtnetlink v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE= 133 | github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w= 134 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 135 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 136 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 137 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 138 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 139 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 140 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= 141 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= 142 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 143 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 144 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 145 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 146 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 147 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 148 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 149 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 150 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 151 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 152 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= 153 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= 154 | github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= 155 | github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= 156 | github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= 157 | github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 158 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 159 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 160 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 161 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 162 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 163 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 164 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 165 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 166 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 167 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 168 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 169 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 170 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 171 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 172 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 173 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 174 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 175 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 176 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 177 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 178 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 179 | github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= 180 | github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= 181 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 182 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 183 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 184 | github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= 185 | github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= 186 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 187 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 188 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 189 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 190 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 191 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 192 | github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= 193 | github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= 194 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 195 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 196 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 197 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 198 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 199 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 200 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= 201 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= 202 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= 203 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= 204 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= 205 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= 206 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= 207 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= 208 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= 209 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= 210 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= 211 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= 212 | github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= 213 | github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= 214 | github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= 215 | github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= 216 | github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= 217 | github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= 218 | github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw= 219 | github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= 220 | github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= 221 | github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= 222 | github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= 223 | github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= 224 | github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= 225 | github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= 226 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= 227 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= 228 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 229 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 230 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 231 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 232 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 233 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 234 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 235 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= 236 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= 237 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 238 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 239 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 240 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 241 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 242 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 243 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 244 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 245 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 246 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= 247 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 248 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= 249 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 250 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 251 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 252 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 253 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 254 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 255 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 256 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 257 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 258 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 259 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 260 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 261 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 262 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 263 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 264 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 265 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 267 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 268 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 269 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 270 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 274 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 275 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 276 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 277 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 278 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 279 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 280 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 281 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 282 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 283 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 284 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 285 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 286 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 287 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 288 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 289 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 290 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 291 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 292 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 293 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 294 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 295 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 296 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 297 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 298 | golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= 299 | golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= 300 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 301 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 302 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 303 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 304 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 305 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 306 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 307 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 308 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 309 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 310 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 311 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 312 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 313 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 314 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= 315 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= 316 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 317 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 318 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 319 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 320 | inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 h1:zomTWJvjwLbKRgGameQtpK6DNFUbZ2oNJuWhgUkGp3M= 321 | inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= 322 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 323 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 324 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 325 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 326 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 327 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 328 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 329 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 330 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 331 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 332 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 333 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 334 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 335 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 336 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 337 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 338 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 339 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 340 | software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= 341 | software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 342 | tailscale.com v1.80.2 h1:MA/AvAyWakq01E1MS6SwKEp2VEFE5CoXAAguwrnbF5g= 343 | tailscale.com v1.80.2/go.mod h1:HTOFVeo5RY0qBl5Uy+LXHwgp0PLXgVSfgqWI34gSrPA= 344 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | func main() { 15 | config, err := rest.InClusterConfig() 16 | if err != nil { 17 | log.Fatal("TIS: failed to get kubernetes config:", err) 18 | } 19 | client, err := kubernetes.NewForConfig(config) 20 | if err != nil { 21 | log.Fatal("TIS: failed to create kubernetes client", err) 22 | } 23 | 24 | tsAuthKey := os.Getenv("TS_AUTHKEY") 25 | if tsAuthKey == "" { 26 | log.Fatal("TIS: missing TS_AUTHKEY") 27 | } 28 | 29 | cHttp := NewHttpController(tsAuthKey) 30 | cTcp := NewTcpController(tsAuthKey) 31 | 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | s := make(chan os.Signal, 1) 34 | signal.Notify(s, syscall.SIGINT, syscall.SIGTERM) 35 | go func() { 36 | <-s 37 | cHttp.shutdown() 38 | cTcp.shutdown() 39 | log.Println("shutting down") 40 | cancel() 41 | os.Exit(0) 42 | }() 43 | 44 | go cHttp.listen(ctx, client) 45 | go cTcp.listen(ctx, client) 46 | <-s 47 | } 48 | --------------------------------------------------------------------------------