From a5677d25580247a74e14cd38a8bd6f303114e588 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 28 Apr 2026 18:16:55 +0300 Subject: [PATCH 01/13] feat: initial tailscale backend --- go.mod | 59 ++++++++++- go.sum | 105 ++++++++++++++++++++ internal/bootstrap/app_bootstrap.go | 55 ++++++++-- internal/bootstrap/router_bootstrap.go | 2 +- internal/bootstrap/service_bootstrap.go | 24 +++++ internal/config/config.go | 19 ++++ internal/middleware/context_middleware.go | 57 ++++++++--- internal/service/tailscale_service.go | 116 ++++++++++++++++++++++ 8 files changed, 409 insertions(+), 28 deletions(-) create mode 100644 internal/service/tailscale_service.go diff --git a/go.mod b/go.mod index 0b7cfafa..3d709e9c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tinyauthapp/tinyauth -go 1.26.0 +go 1.26.1 require ( charm.land/huh/v2 v2.0.3 @@ -30,13 +30,29 @@ require ( charm.land/bubbletea/v2 v2.0.2 // indirect charm.land/lipgloss/v2 v2.0.1 // indirect dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/boombuler/barcode v1.0.2 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect @@ -54,18 +70,24 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/creachadair/msync v0.7.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gaissmai/bart v0.26.1 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -73,9 +95,16 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect @@ -83,49 +112,69 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect + github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/safchain/ethtool v0.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect + github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect modernc.org/libc v1.72.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect + tailscale.com v1.96.5 // indirect ) diff --git a/go.sum b/go.sum index 2e064d74..01046ecc 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= @@ -20,14 +22,44 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= +github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -75,18 +107,24 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho= +github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= @@ -101,8 +139,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo= +github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= @@ -111,6 +153,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -130,8 +174,15 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -148,8 +199,12 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -162,8 +217,12 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -184,10 +243,16 @@ github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjc github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -205,6 +270,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -215,8 +282,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -224,6 +295,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -236,6 +309,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -253,6 +328,18 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw= +github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -261,6 +348,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE= github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= @@ -269,6 +358,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -287,6 +378,10 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= @@ -299,8 +394,10 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= @@ -312,6 +409,10 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= @@ -329,6 +430,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= @@ -359,3 +462,5 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA= +tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY= diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 3879c05e..3a570c69 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/gin-gonic/gin" "github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/repository" @@ -204,7 +205,20 @@ func (app *BootstrapApp) Setup() error { go app.heartbeatRoutine() } - // If we have an socket path, bind to it + // Start listeners and monitor for errors + err = app.setupListeners(router) + + if err != nil { + return fmt.Errorf("server error: %w", err) + } + + return nil +} + +func (app *BootstrapApp) setupListeners(router *gin.Engine) error { + errChan := make(chan error, 1) + + // First check socket if app.config.Server.SocketPath != "" { if _, err := os.Stat(app.config.Server.SocketPath); err == nil { tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath) @@ -215,21 +229,44 @@ func (app *BootstrapApp) Setup() error { } tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath) - if err := router.RunUnix(app.config.Server.SocketPath); err != nil { - tlog.App.Fatal().Err(err).Msg("Failed to start server") - } - return nil + go func() { + err := router.RunUnix(app.config.Server.SocketPath) + if err != nil { + errChan <- fmt.Errorf("failed to start server on unix socket: %w", err) + } + }() } - // Start server + // Then normal TCP listener address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port) tlog.App.Info().Msgf("Starting server on %s", address) - if err := router.Run(address); err != nil { - tlog.App.Fatal().Err(err).Msg("Failed to start server") + + go func() { + err := router.Run(address) + if err != nil { + errChan <- fmt.Errorf("failed to start server on TCP: %w", err) + } + }() + + // Finally tailscale listener if configured + if app.services.tailscaleService.IsConnfigured() { + tailscaleListener, err := app.services.tailscaleService.CreateListener() + if err != nil { + return fmt.Errorf("failed to create tailscale listener: %w", err) + } + + tlog.App.Info().Msgf("Starting server on Tailscale interface with hostname %s", app.services.tailscaleService.GetHostname()) + + go func() { + err := router.RunListener(tailscaleListener) + if err != nil { + errChan <- fmt.Errorf("failed to start server on Tailscale interface: %w", err) + } + }() } - return nil + return <-errChan } func (app *BootstrapApp) heartbeatRoutine() { diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 91d36ac2..f30e28d3 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -31,7 +31,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) { contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ CookieDomain: app.context.cookieDomain, - }, app.services.authService, app.services.oauthBrokerService) + }, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService) err := contextMiddleware.Init() diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index 9c5806b9..546512c5 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -1,6 +1,8 @@ package bootstrap import ( + "fmt" + "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils/tlog" @@ -13,6 +15,7 @@ type Services struct { ldapService *service.LdapService oauthBrokerService *service.OAuthBrokerService oidcService *service.OIDCService + tailscaleService *service.TailscaleService } func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) { @@ -68,6 +71,27 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er services.oauthBrokerService = oauthBrokerService + tailscaleHostname := app.config.Tailscale.Hostname + + if tailscaleHostname == "" { + tailscaleHostname = fmt.Sprintf("tinyauth-%s", app.context.uuid) + } + + tailscaleService := service.NewTailscaleService(service.TailscaleServiceConfig{ + Dir: app.config.Tailscale.Dir, + Hostname: tailscaleHostname, + AuthKey: app.config.Tailscale.AuthKey, + }) + + err = tailscaleService.Init() + + if err != nil { + tlog.App.Warn().Err(err).Msg("Failed to setup Tailscale service, starting without it") + tailscaleService.Destroy() + } else { + services.tailscaleService = tailscaleService + } + authService := service.NewAuthService(service.AuthServiceConfig{ Users: app.context.users, OauthWhitelist: app.config.OAuth.Whitelist, diff --git a/internal/config/config.go b/internal/config/config.go index 1bf64af4..66f391d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,6 +59,9 @@ func NewDefaultConfiguration() *Config { Experimental: ExperimentalConfig{ ConfigFile: "", }, + Tailscale: TailscaleConfig{ + Dir: "./state", + }, } } @@ -91,6 +94,7 @@ type Config struct { Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"` Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"` Log LogConfig `description:"Logging configuration." yaml:"log"` + Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"` } type DatabaseConfig struct { @@ -209,6 +213,13 @@ type ExperimentalConfig struct { ConfigFile string `description:"Path to config file." yaml:"-"` } +type TailscaleConfig struct { + Dir string `description:"Tailscale state directory." yaml:"dir"` + Hostname string `description:"Tailscale hostname." yaml:"hostname"` + AuthKey string `description:"Tailscale auth key." yaml:"authKey"` + Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"` +} + // Config loader options const DefaultNamePrefix = "TINYAUTH_" @@ -269,6 +280,13 @@ type UserSearch struct { Type string // local, ldap or unknown } +type TailscaleWhoisResponse struct { + UserID string + LoginName string + DisplayName string + NodeName string +} + type UserContext struct { Username string Name string @@ -284,6 +302,7 @@ type UserContext struct { OAuthSub string LdapGroups string Attributes UserAttributes + Tailscale *TailscaleWhoisResponse } // API responses and queries diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 651d9d85..e187afd0 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -37,16 +37,18 @@ type ContextMiddlewareConfig struct { } type ContextMiddleware struct { - config ContextMiddlewareConfig - auth *service.AuthService - broker *service.OAuthBrokerService + config ContextMiddlewareConfig + auth *service.AuthService + broker *service.OAuthBrokerService + tailscale *service.TailscaleService } -func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService) *ContextMiddleware { +func NewContextMiddleware(config ContextMiddlewareConfig, auth *service.AuthService, broker *service.OAuthBrokerService, tailscale *service.TailscaleService) *ContextMiddleware { return &ContextMiddleware{ - config: config, - auth: auth, - broker: broker, + config: config, + auth: auth, + broker: broker, + tailscale: tailscale, } } @@ -69,7 +71,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { } if cookie.TotpPending { - c.Set("context", &config.UserContext{ + ctx := m.addTailscaleContext(c, config.UserContext{ Username: cookie.Username, Name: cookie.Name, Email: cookie.Email, @@ -77,6 +79,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { TotpPending: true, TotpEnabled: true, }) + c.Set("context", &ctx) c.Next() return } @@ -119,7 +122,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { } m.auth.RefreshSessionCookie(c) - c.Set("context", &config.UserContext{ + ctx := m.addTailscaleContext(c, config.UserContext{ Username: cookie.Username, Name: cookie.Name, Email: cookie.Email, @@ -128,6 +131,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { LdapGroups: strings.Join(ldapGroups, ","), Attributes: localAttributes, }) + c.Set("context", &ctx) c.Next() return default: @@ -146,7 +150,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { } m.auth.RefreshSessionCookie(c) - c.Set("context", &config.UserContext{ + ctx := m.addTailscaleContext(c, config.UserContext{ Username: cookie.Username, Name: cookie.Name, Email: cookie.Email, @@ -157,6 +161,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { IsLoggedIn: true, OAuth: true, }) + c.Set("context", &ctx) c.Next() return } @@ -166,7 +171,8 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { if basic == nil { tlog.App.Debug().Msg("No basic auth provided") - c.Next() + ctx := m.addTailscaleContext(c, config.UserContext{}) + c.Set("context", &ctx) return } @@ -218,7 +224,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { email = user.Attributes.Email } - c.Set("context", &config.UserContext{ + ctx := m.addTailscaleContext(c, config.UserContext{ Username: user.Username, Name: name, Email: email, @@ -227,6 +233,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { IsBasicAuth: true, Attributes: user.Attributes, }) + c.Set("context", &ctx) c.Next() return case "ldap": @@ -240,7 +247,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } - c.Set("context", &config.UserContext{ + ctx := m.addTailscaleContext(c, config.UserContext{ Username: basic.Username, Name: utils.Capitalize(basic.Username), Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain), @@ -249,10 +256,14 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { LdapGroups: strings.Join(ldapUser.Groups, ","), IsBasicAuth: true, }) + c.Set("context", &ctx) c.Next() return } + // unreachable but just in case + ctx := m.addTailscaleContext(c, config.UserContext{}) + c.Set("context", &ctx) c.Next() } } @@ -265,3 +276,23 @@ func (m *ContextMiddleware) isIgnorePath(path string) bool { } return false } + +func (m *ContextMiddleware) addTailscaleContext(c *gin.Context, ctx config.UserContext) config.UserContext { + if !m.tailscale.IsConnfigured() { + return ctx + } + + ip := c.Request.RemoteAddr + + whois, err := m.tailscale.Whois(c, ip) + + if err != nil { + tlog.App.Warn().Err(err).Msg("Error performing Tailscale whois") + return ctx + } + + tlog.App.Trace().Interface("whois", whois).Msg("Tailscale whois result") + + ctx.Tailscale = &whois + return ctx +} diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go new file mode 100644 index 00000000..e5c0a907 --- /dev/null +++ b/internal/service/tailscale_service.go @@ -0,0 +1,116 @@ +package service + +import ( + "context" + "errors" + "net" + + "github.com/tinyauthapp/tinyauth/internal/config" + "github.com/tinyauthapp/tinyauth/internal/utils/tlog" + "tailscale.com/client/local" + "tailscale.com/tsnet" +) + +type TailscaleServiceConfig struct { + Dir string + Hostname string + AuthKey string + Ephemeral bool +} + +type TailscaleService struct { + config TailscaleServiceConfig + srv *tsnet.Server + lc *local.Client + ln *net.Listener +} + +func NewTailscaleService(config TailscaleServiceConfig) *TailscaleService { + return &TailscaleService{ + config: config, + } +} + +func (ts *TailscaleService) Init() error { + srv := new(tsnet.Server) + + // node options + srv.Dir = ts.config.Dir + srv.Hostname = ts.config.Hostname + srv.AuthKey = ts.config.AuthKey + srv.Ephemeral = ts.config.Ephemeral + + // redirect logs to zerolog + srv.Logf = tlog.App.Printf + srv.UserLogf = tlog.App.Printf + + err := srv.Start() + + if err != nil { + return err + } + + ts.srv = srv + + lc, err := srv.LocalClient() + + if err != nil { + return err + } + + ts.lc = lc + return nil +} + +func (ts *TailscaleService) Destroy() error { + if ts.ln != nil { + (*ts.ln).Close() + } + if ts.srv != nil { + return ts.srv.Close() + } + ts.ln = nil + ts.lc = nil + ts.srv = nil + return nil +} + +func (ts *TailscaleService) Whois(ctx context.Context, addr string) (config.TailscaleWhoisResponse, error) { + who, err := ts.lc.WhoIs(ctx, addr) + + if err != nil { + if errors.Is(err, local.ErrPeerNotFound) { + return config.TailscaleWhoisResponse{}, nil + } + return config.TailscaleWhoisResponse{}, err + } + + res := config.TailscaleWhoisResponse{ + UserID: who.UserProfile.ID.String(), + LoginName: who.UserProfile.LoginName, + DisplayName: who.UserProfile.DisplayName, + NodeName: who.Node.Name, + } + + return res, nil +} + +func (ts *TailscaleService) CreateListener() (net.Listener, error) { + if ts.ln != nil { + return *ts.ln, nil + } + ln, err := ts.srv.ListenTLS("tcp", ":443") + if err != nil { + return nil, err + } + ts.ln = &ln + return ln, nil +} + +func (ts *TailscaleService) IsConnfigured() bool { + return ts.srv != nil +} + +func (ts *TailscaleService) GetHostname() string { + return ts.srv.Hostname +} From 3971710e87b24db4b87243fc6e2d891de2f60414 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 28 Apr 2026 19:11:36 +0300 Subject: [PATCH 02/13] feat: initial wip frontend --- air.toml | 2 +- frontend/src/pages/login-page.tsx | 70 ++++++++++++++++++++- frontend/src/schemas/user-context-schema.ts | 1 + internal/controller/context_controller.go | 25 +++++--- internal/controller/user_controller.go | 48 ++++++++++++++ internal/middleware/context_middleware.go | 14 +++++ internal/service/auth_service.go | 10 +++ 7 files changed, 158 insertions(+), 12 deletions(-) diff --git a/air.toml b/air.toml index 5de0449c..d1ed6df9 100644 --- a/air.toml +++ b/air.toml @@ -2,7 +2,7 @@ root = "/tinyauth" tmp_dir = "tmp" [build] -pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data", "echo 'backend running' > internal/assets/dist/index.html"] +pre_cmd = ["mkdir -p internal/assets/dist", "mkdir -p /data"] # "echo 'backend running' > internal/assets/dist/index.html" cmd = "CGO_ENABLED=0 go build -gcflags=\"all=-N -l\" -o tmp/tinyauth ./cmd/tinyauth" bin = "tmp/tinyauth" full_bin = "dlv --listen :4000 --headless=true --api-version=2 --accept-multiclient --log=true exec tmp/tinyauth --continue --check-go-version=false" diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index c39a0fb6..288054dd 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -36,12 +36,13 @@ const iconMap: Record = { }; export const LoginPage = () => { - const { isLoggedIn } = useUserContext(); + const { isLoggedIn, tailscaleNodeName } = useUserContext(); const { providers, title, oauthAutoRedirect } = useAppContext(); const { search } = useLocation(); const { t } = useTranslation(); const [showRedirectButton, setShowRedirectButton] = useState(false); + const [useTailscale, setUseTailscale] = useState(tailscaleNodeName !== ""); const hasAutoRedirectedRef = useRef(false); @@ -148,6 +149,32 @@ export const LoginPage = () => { }, }); + const { mutate: tailscaleMutate, isPending: tailscaleIsPending } = + useMutation({ + mutationFn: () => axios.post("/api/user/tailscale"), + mutationKey: ["tailscale"], + onSuccess: () => { + toast.success("Logged in", { + description: t("Tailscale session confirmed"), + }); + + redirectTimer.current = window.setTimeout(() => { + if (oidcParams.isOidc) { + window.location.replace(`/authorize?${oidcParams.compiled}`); + return; + } + window.location.replace( + `/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`, + ); + }, 500); + }, + onError: () => { + toast.error("Failed to login", { + description: "Failed to authenticate with Tailscale.", + }); + }, + }); + useEffect(() => { if ( !isLoggedIn && @@ -228,6 +255,47 @@ export const LoginPage = () => { ); } + + if (useTailscale) { + return ( + + + + + Tinyauth x Tailscale + + + +
+ We detected that you are accessing Tinyauth from an authorized + Tailscale device. Would you like to continue with your Tailscale + credentials? +
+
+ Machine Name: {tailscaleNodeName} +
+
+ + + + +
+ ); + } + return ( diff --git a/frontend/src/schemas/user-context-schema.ts b/frontend/src/schemas/user-context-schema.ts index e7e057ac..0c4b24c6 100644 --- a/frontend/src/schemas/user-context-schema.ts +++ b/frontend/src/schemas/user-context-schema.ts @@ -9,6 +9,7 @@ export const userContextSchema = z.object({ oauth: z.boolean(), totpPending: z.boolean(), oauthName: z.string(), + tailscaleNodeName: z.string(), }); export type UserContextSchema = z.infer; diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index da53303b..4febb48c 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -11,16 +11,17 @@ import ( ) type UserContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - IsLoggedIn bool `json:"isLoggedIn"` - Username string `json:"username"` - Name string `json:"name"` - Email string `json:"email"` - Provider string `json:"provider"` - OAuth bool `json:"oauth"` - TotpPending bool `json:"totpPending"` - OAuthName string `json:"oauthName"` + Status int `json:"status"` + Message string `json:"message"` + IsLoggedIn bool `json:"isLoggedIn"` + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + Provider string `json:"provider"` + OAuth bool `json:"oauth"` + TotpPending bool `json:"totpPending"` + OAuthName string `json:"oauthName"` + TailscaleNodeName string `json:"tailscaleNodeName"` } type AppContextResponse struct { @@ -91,6 +92,10 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { OAuthName: context.OAuthName, } + if context.Tailscale != nil { + userContext.TailscaleNodeName = context.Tailscale.NodeName + } + if err != nil { tlog.App.Debug().Err(err).Msg("No user context found in request") userContext.Status = 401 diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 187b33b9..a0d665cd 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -46,6 +46,7 @@ func (controller *UserController) SetupRoutes() { userGroup.POST("/login", controller.loginHandler) userGroup.POST("/logout", controller.logoutHandler) userGroup.POST("/totp", controller.totpHandler) + userGroup.POST("/tailscale", controller.tailscaleHandler) } func (controller *UserController) loginHandler(c *gin.Context) { @@ -309,3 +310,50 @@ func (controller *UserController) totpHandler(c *gin.Context) { "message": "Login successful", }) } + +func (controller *UserController) tailscaleHandler(c *gin.Context) { + context, err := utils.GetContext(c) + + if err != nil { + tlog.App.Error().Err(err).Msg("Failed to get user context") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + if context.Tailscale == nil { + tlog.App.Warn().Msg("Tailscale session requested but Tailscale device not found") + c.JSON(404, gin.H{ + "status": 404, + "message": "Not Found", + }) + return + } + + sessionCookie := repository.Session{ + Username: context.Tailscale.LoginName, + Name: context.Tailscale.DisplayName, + Email: context.Tailscale.LoginName, + Provider: "tailscale", + } + + tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie") + + err = controller.auth.CreateSessionCookie(c, &sessionCookie) + + if err != nil { + tlog.App.Error().Err(err).Msg("Failed to create session cookie") + c.JSON(500, gin.H{ + "status": 500, + "message": "Internal Server Error", + }) + return + } + + c.JSON(200, gin.H{ + "status": 200, + "message": "Login successful", + }) +} diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index e187afd0..35c8cec5 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -63,6 +63,8 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } + tlog.App.Trace().Interface("cookies", c.Request.Cookies()).Msg("cookies") + cookie, err := m.auth.GetSessionCookie(c) if err != nil { @@ -134,6 +136,18 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { c.Set("context", &ctx) c.Next() return + case "tailscale": + m.auth.RefreshSessionCookie(c) + ctx := m.addTailscaleContext(c, config.UserContext{ + Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, + Provider: cookie.Provider, + IsLoggedIn: true, + }) + c.Set("context", &ctx) + c.Next() + return default: _, exists := m.broker.GetService(cookie.Provider) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 1d0d74d3..d8ef347a 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -327,6 +327,16 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Se return err } + if data.Provider == "tailscale" { + // TODO: use domain from tailscale to set cookie, this is mostly a hack for now + tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", c.Request.Host)) + if err != nil { + return err + } + c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", tsCookieDomain), auth.config.SecureCookie, true) + return nil + } + c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) return nil From 25017a76c959997c1d3bcffdc225869781c2ca2c Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 10 May 2026 17:50:01 +0300 Subject: [PATCH 03/13] feat: add basic login functionality back after main merge --- frontend/src/pages/login-page.tsx | 2 +- go.mod | 13 +- go.sum | 156 ++++++++++++++++-- internal/bootstrap/app_bootstrap.go | 51 +++++- internal/bootstrap/router_bootstrap.go | 2 +- internal/bootstrap/service_bootstrap.go | 10 +- internal/controller/context_controller.go | 42 ++--- internal/controller/proxy_controller_test.go | 2 +- internal/controller/user_controller.go | 35 ++-- internal/controller/user_controller_test.go | 2 +- internal/middleware/context_middleware.go | 83 +++++++--- .../middleware/context_middleware_test.go | 4 +- internal/model/config.go | 104 ++---------- internal/model/context.go | 46 +++++- internal/service/auth_service.go | 14 +- internal/service/tailscale_service.go | 99 +++++------ 16 files changed, 434 insertions(+), 231 deletions(-) diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index 288054dd..c8c9f23d 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -262,7 +262,7 @@ export const LoginPage = () => { - Tinyauth x Tailscale + Tinyauth · Tailscale diff --git a/go.mod b/go.mod index 2f762f1f..26525468 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 modernc.org/sqlite v1.50.0 + tailscale.com v1.96.5 ) require ( @@ -73,7 +74,6 @@ require ( github.com/coder/websocket v1.8.12 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/creachadair/msync v0.7.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect @@ -120,7 +120,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -159,9 +158,9 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.52.0 // indirect @@ -169,17 +168,14 @@ require ( golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect - golang.org/x/time v0.12.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect - golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect + gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect @@ -187,7 +183,6 @@ require ( modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect - tailscale.com v1.96.5 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect diff --git a/go.sum b/go.sum index 01046ecc..cfc5abbb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= @@ -10,6 +12,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= +filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= @@ -20,8 +24,6 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -32,6 +34,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= @@ -52,6 +56,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEd github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= @@ -60,6 +66,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -101,6 +109,8 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2 github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= +github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= +github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -115,8 +125,14 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA= +github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs= github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho= github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008= +github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= +github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -125,8 +141,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -135,6 +157,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -149,6 +173,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= @@ -162,6 +188,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -170,11 +204,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= @@ -183,12 +218,18 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= +github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -197,6 +238,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -205,6 +247,10 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk= +github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -217,6 +263,12 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -225,6 +277,10 @@ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uq github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -235,6 +291,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -243,12 +301,18 @@ github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjc github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= +github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -268,7 +332,6 @@ github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFL github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -276,20 +339,26 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -297,6 +366,10 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -317,9 +390,12 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -332,20 +408,36 @@ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw= github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ= github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE= github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -356,8 +448,6 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -378,6 +468,10 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= @@ -388,6 +482,10 @@ golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= @@ -405,8 +503,8 @@ golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= @@ -420,11 +518,15 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -432,6 +534,22 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLaf0O3aOenC0Z4ZafIWSpjUzsQaNho= +honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= @@ -454,13 +572,23 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= -modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA= tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY= diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 6e3ad038..1a932a28 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -34,6 +34,7 @@ type Services struct { ldapService *service.LdapService oauthBrokerService *service.OAuthBrokerService oidcService *service.OIDCService + tailscaleService *service.TailscaleService } type BootstrapApp struct { @@ -250,6 +251,7 @@ func (app *BootstrapApp) Setup() error { runUnix := app.config.Server.SocketPath != "" runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled + runTailscale := app.services.tailscaleService != nil if runUnix { errChanLen++ @@ -259,6 +261,10 @@ func (app *BootstrapApp) Setup() error { errChanLen++ } + if runTailscale { + errChanLen++ + } + errChan := make(chan error, errChanLen) if app.config.Server.ConcurrentListenersEnabled { @@ -283,6 +289,15 @@ func (app *BootstrapApp) Setup() error { }) } + // serve to tailscale + if runTailscale { + app.wg.Go(func() { + if err := app.serveTailscale(); err != nil { + errChan <- err + } + }) + } + // monitor cancellation and server errors for { select { @@ -369,7 +384,41 @@ func (app *BootstrapApp) serveUnix() error { return fmt.Errorf("failed to start unix socket listener: %w", err) } - return <-errChan + return nil +} + +func (app *BootstrapApp) serveTailscale() error { + app.log.App.Info().Msgf("Starting Tailscale server on %s", app.services.tailscaleService.GetHostname()) + + listener, err := app.services.tailscaleService.CreateListener() + + if err != nil { + return fmt.Errorf("failed to create tailscale listener: %w", err) + } + + server := &http.Server{ + Handler: app.router.Handler(), + } + + shutdown := func() { + server.Shutdown(app.ctx) + listener.Close() + } + + go func() { + <-app.ctx.Done() + app.log.App.Debug().Msg("Shutting down Tailscale listener") + shutdown() + }() + + err = server.Serve(listener) + + if err != nil && !errors.Is(err, http.ErrServerClosed) { + shutdown() + return fmt.Errorf("failed to start tailscale listener: %w", err) + } + + return nil } func (app *BootstrapApp) heartbeatRoutine() { diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 12a48bc0..7f02af47 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -24,7 +24,7 @@ func (app *BootstrapApp) setupRouter() error { } } - contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService) + contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService) engine.Use(contextMiddleware.Middleware()) uiMiddleware, err := middleware.NewUIMiddleware() diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index cea23ab8..e6fed4ff 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -46,13 +46,21 @@ func (app *BootstrapApp) setupServices() error { labelProvider = dockerService } + tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, &app.wg) + + if err != nil { + app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it") + } else { + app.services.tailscaleService = tailscaleService + } + accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps) app.services.accessControlService = accessControlsService oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx) app.services.oauthBrokerService = oauthBrokerService - authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService) + authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService) app.services.authService = authService oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg) diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index 8d9f5fa2..a63cf3c7 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -11,16 +11,17 @@ import ( ) type UserContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - IsLoggedIn bool `json:"isLoggedIn"` - Username string `json:"username"` - Name string `json:"name"` - Email string `json:"email"` - Provider string `json:"provider"` - OAuth bool `json:"oauth"` - TOTPPending bool `json:"totpPending"` - OAuthName string `json:"oauthName"` + Status int `json:"status"` + Message string `json:"message"` + IsLoggedIn bool `json:"isLoggedIn"` + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + Provider string `json:"provider"` + OAuth bool `json:"oauth"` + TOTPPending bool `json:"totpPending"` + OAuthName string `json:"oauthName"` + TailscaleNodeName string `json:"tailscaleNodeName,omitempty"` } type AppContextResponse struct { @@ -79,16 +80,17 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { } userContext := UserContextResponse{ - Status: 200, - Message: "Success", - IsLoggedIn: context.Authenticated, - Username: context.GetUsername(), - Name: context.GetName(), - Email: context.GetEmail(), - Provider: context.GetProviderID(), - OAuth: context.IsOAuth(), - TOTPPending: context.TOTPPending(), - OAuthName: context.OAuthName(), + Status: 200, + Message: "Success", + IsLoggedIn: context.Authenticated, + Username: context.GetUsername(), + Name: context.GetName(), + Email: context.GetEmail(), + Provider: context.GetProviderID(), + OAuth: context.IsOAuth(), + TOTPPending: context.TOTPPending(), + OAuthName: context.OAuthName(), + TailscaleNodeName: context.TailscaleNodeName(), } c.JSON(200, userContext) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index 12c3c9f1..199df5a6 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -390,7 +390,7 @@ func TestProxyController(t *testing.T) { ctx := context.TODO() broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker) + authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker, nil) aclsService := service.NewAccessControlsService(log, nil, acls) for _, test := range tests { diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index e86934c2..72ab6e04 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -394,39 +394,37 @@ func (controller *UserController) totpHandler(c *gin.Context) { } func (controller *UserController) tailscaleHandler(c *gin.Context) { - context, err := utils.GetContext(c) + context, err := new(model.UserContext).NewFromGin(c) if err != nil { - tlog.App.Error().Err(err).Msg("Failed to get user context") - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", + controller.log.App.Error().Err(err).Msg("Failed to create user context from request") + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", }) return } if context.Tailscale == nil { - tlog.App.Warn().Msg("Tailscale session requested but Tailscale device not found") - c.JSON(404, gin.H{ - "status": 404, - "message": "Not Found", + controller.log.App.Warn().Msg("Tailscale login attempt without Tailscale context") + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", }) return } sessionCookie := repository.Session{ - Username: context.Tailscale.LoginName, - Name: context.Tailscale.DisplayName, - Email: context.Tailscale.LoginName, + Username: context.Tailscale.Username, + Name: context.Tailscale.Name, + Email: context.Tailscale.Email, Provider: "tailscale", } - tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie") - - err = controller.auth.CreateSessionCookie(c, &sessionCookie) + cookie, err := controller.auth.CreateSession(c, sessionCookie) if err != nil { - tlog.App.Error().Err(err).Msg("Failed to create session cookie") + controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification") c.JSON(500, gin.H{ "status": 500, "message": "Internal Server Error", @@ -434,6 +432,11 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) { return } + http.SetCookie(c.Writer, cookie) + + controller.log.App.Info().Str("username", context.GetUsername()).Msg("Tailscale login successful, login complete") + controller.log.AuditLoginSuccess(context.GetUsername(), "tailscale", c.ClientIP()) + c.JSON(200, gin.H{ "status": 200, "message": "Login successful", diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index 10858175..3f07b634 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -420,7 +420,7 @@ func TestUserController(t *testing.T) { wg := &sync.WaitGroup{} broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker) + authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker, nil) beforeEach := func() { // Clear failed login attempts before each test diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 00ec95a0..cdc4c273 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -36,10 +36,11 @@ var ( ) type ContextMiddleware struct { - log *logger.Logger - runtime model.RuntimeConfig - auth *service.AuthService - broker *service.OAuthBrokerService + log *logger.Logger + runtime model.RuntimeConfig + auth *service.AuthService + broker *service.OAuthBrokerService + tailscale *service.TailscaleService } func NewContextMiddleware( @@ -47,12 +48,14 @@ func NewContextMiddleware( runtime model.RuntimeConfig, auth *service.AuthService, broker *service.OAuthBrokerService, + tailscale *service.TailscaleService, ) *ContextMiddleware { return &ContextMiddleware{ - log: log, - runtime: runtime, - auth: auth, - broker: broker, + log: log, + runtime: runtime, + auth: auth, + broker: broker, + tailscale: tailscale, } } @@ -66,7 +69,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { uuid, err := c.Cookie(m.runtime.SessionCookieName) if err == nil { - userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid) + userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.RemoteIP()) if err == nil { if cookie != nil { @@ -102,14 +105,27 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } - // unreachable but just in case - ctx := m.addTailscaleContext(c, config.UserContext{}) - c.Set("context", &ctx) + // Lastly check if we have a tailscale session to add + if m.tailscale != nil { + tailscaleContext, err := m.tailscaleWhois(c.Request.Context(), c.RemoteIP()) + + if err != nil { + m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.RemoteIP(), err) + } + + if tailscaleContext != nil { + c.Set("context", &model.UserContext{ + Authenticated: false, + Tailscale: tailscaleContext, + }) + } + } + c.Next() } } -func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model.UserContext, *http.Cookie, error) { +func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip string) (*model.UserContext, *http.Cookie, error) { session, err := m.auth.GetSession(ctx, uuid) if err != nil { @@ -144,6 +160,19 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model if userContext.Local.Attributes.Email == "" { userContext.Local.Attributes.Email = utils.CompileUserEmail(user.Username, m.runtime.CookieDomain) } + // Ensures that the seesion is still coming from Tailscale + case model.ProviderTailscale: + tailscaleContext, err := m.tailscaleWhois(ctx, ip) + + if err != nil { + return nil, nil, fmt.Errorf("error performing tailscale whois: %w", err) + } + + if tailscaleContext == nil { + return nil, nil, fmt.Errorf("tailscale whois returned no result for IP: %s", ip) + } + + userContext.Tailscale = tailscaleContext case model.ProviderLDAP: search, err := m.auth.SearchUser(userContext.LDAP.Username) @@ -261,22 +290,28 @@ func (m *ContextMiddleware) isIgnorePath(path string) bool { return false } -func (m *ContextMiddleware) addTailscaleContext(c *gin.Context, ctx config.UserContext) config.UserContext { - if !m.tailscale.IsConnfigured() { - return ctx +func (m *ContextMiddleware) tailscaleWhois(ctx context.Context, ip string) (*model.TailscaleContext, error) { + if m.tailscale == nil { + return nil, nil } - ip := c.Request.RemoteAddr - - whois, err := m.tailscale.Whois(c, ip) + whois, err := m.tailscale.Whois(ctx, ip) if err != nil { - tlog.App.Warn().Err(err).Msg("Error performing Tailscale whois") - return ctx + m.log.App.Error().Err(err).Msgf("Error performing Tailscale whois for IP %s: %v", ip, err) + return nil, err } - tlog.App.Trace().Interface("whois", whois).Msg("Tailscale whois result") + if whois == nil { + return nil, nil + } - ctx.Tailscale = &whois - return ctx + return &model.TailscaleContext{ + BaseContext: model.BaseContext{ + Username: whois.NodeName, + Email: whois.LoginName, + Name: whois.DisplayName, + }, + UserID: whois.UserID, + }, nil } diff --git a/internal/middleware/context_middleware_test.go b/internal/middleware/context_middleware_test.go index 03f9f553..bdb937ab 100644 --- a/internal/middleware/context_middleware_test.go +++ b/internal/middleware/context_middleware_test.go @@ -260,9 +260,9 @@ func TestContextMiddleware(t *testing.T) { queries := repository.New(app.GetDB()) broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker) + authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker, nil) - contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker) + contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil) for _, test := range tests { authService.ClearRateLimitsTestingOnly() diff --git a/internal/model/config.go b/internal/model/config.go index 4333b5b2..2804b17a 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -69,20 +69,21 @@ func NewDefaultConfiguration() *Config { } type Config struct { - AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"` - Database DatabaseConfig `description:"Database configuration." yaml:"database"` - Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"` - Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"` - Server ServerConfig `description:"Server configuration." yaml:"server"` - Auth AuthConfig `description:"Authentication configuration." yaml:"auth"` - Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"` - OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"` - OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"` - UI UIConfig `description:"UI customization." yaml:"ui"` - Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"` - Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"` - Log LogConfig `description:"Logging configuration." yaml:"log"` - Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"` + AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"` + Database DatabaseConfig `description:"Database configuration." yaml:"database"` + Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"` + Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"` + Server ServerConfig `description:"Server configuration." yaml:"server"` + Auth AuthConfig `description:"Authentication configuration." yaml:"auth"` + Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"` + OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"` + OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"` + UI UIConfig `description:"UI customization." yaml:"ui"` + LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"` + Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"` + Log LogConfig `description:"Logging configuration." yaml:"log"` + Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"` + LabelProvider string `description:"Label provider to use (docker, kubernetes, auto)." yaml:"labelProvider"` } type DatabaseConfig struct { @@ -204,7 +205,6 @@ type ExperimentalConfig struct { ConfigFile string `description:"Path to config file." yaml:"-"` } -<<<<<<< HEAD:internal/config/config.go type TailscaleConfig struct { Dir string `description:"Tailscale state directory." yaml:"dir"` Hostname string `description:"Tailscale hostname." yaml:"hostname"` @@ -212,22 +212,8 @@ type TailscaleConfig struct { Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"` } -// Config loader options - -const DefaultNamePrefix = "TINYAUTH_" - // OAuth/OIDC config -type Claims struct { - Sub string `json:"sub"` - Name string `json:"name"` - Email string `json:"email"` - PreferredUsername string `json:"preferred_username"` - Groups any `json:"groups"` -} - -======= ->>>>>>> main:internal/model/config.go type OAuthServiceConfig struct { ClientID string `description:"OAuth client ID." yaml:"clientId"` ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"` @@ -250,31 +236,6 @@ type OIDCClientConfig struct { Name string `description:"Client name in UI." yaml:"name"` } -<<<<<<< HEAD:internal/config/config.go -var OverrideProviders = map[string]string{ - "google": "Google", - "github": "GitHub", -} - -// User/session related stuff - -type User struct { - Username string - Password string - TotpSecret string - Attributes UserAttributes -} - -type LdapUser struct { - DN string - Groups []string -} - -type UserSearch struct { - Username string - Type string // local, ldap or unknown -} - type TailscaleWhoisResponse struct { UserID string LoginName string @@ -282,41 +243,6 @@ type TailscaleWhoisResponse struct { NodeName string } -type UserContext struct { - Username string - Name string - Email string - IsLoggedIn bool - IsBasicAuth bool - OAuth bool - Provider string - TotpPending bool - OAuthGroups string - TotpEnabled bool - OAuthName string - OAuthSub string - LdapGroups string - Attributes UserAttributes - Tailscale *TailscaleWhoisResponse -} - -// API responses and queries - -type UnauthorizedQuery struct { - Username string `url:"username"` - Resource string `url:"resource"` - GroupErr bool `url:"groupErr"` - IP string `url:"ip"` -} - -type RedirectQuery struct { - RedirectURI string `url:"redirect_uri"` -} - -======= ->>>>>>> main:internal/model/config.go -// ACLs - type Apps struct { Apps map[string]App `description:"App ACLs configuration." yaml:"apps"` } diff --git a/internal/model/context.go b/internal/model/context.go index b9e31bef..8f244ab8 100644 --- a/internal/model/context.go +++ b/internal/model/context.go @@ -19,6 +19,7 @@ const ( ProviderBasicAuth ProviderOAuth ProviderLDAP + ProviderTailscale ) type UserContext struct { @@ -27,6 +28,7 @@ type UserContext struct { Local *LocalContext OAuth *OAuthContext LDAP *LDAPContext + Tailscale *TailscaleContext } type BaseContext struct { @@ -54,6 +56,11 @@ type LDAPContext struct { Groups []string } +type TailscaleContext struct { + BaseContext + UserID string +} + func (c *UserContext) IsAuthenticated() bool { return c.Authenticated } @@ -74,6 +81,10 @@ func (c *UserContext) IsBasicAuth() bool { return c.Provider == ProviderBasicAuth && c.Local != nil } +func (c *UserContext) IsTailscale() bool { + return c.Provider == ProviderTailscale && c.Tailscale != nil +} + func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) { userContextValue, exists := ginctx.Get("context") @@ -87,7 +98,7 @@ func (c *UserContext) NewFromGin(ginctx *gin.Context) (*UserContext, error) { return nil, errors.New("invalid user context type") } - if userContext.LDAP == nil && userContext.Local == nil && userContext.OAuth == nil { + if userContext.LDAP == nil && userContext.Local == nil && userContext.OAuth == nil && userContext.Tailscale == nil { return nil, errors.New("incomplete user context") } @@ -121,6 +132,15 @@ func (c *UserContext) NewFromSession(session *repository.Session) (*UserContext, Email: session.Email, }, } + case "tailscale": + c.Provider = ProviderTailscale + c.Tailscale = &TailscaleContext{ + BaseContext: BaseContext{ + Username: session.Username, + Name: session.Name, + Email: session.Email, + }, + } // By default we assume an unknown name which is oauth default: c.Provider = ProviderOAuth @@ -167,6 +187,11 @@ func (c *UserContext) GetUsername() string { return "" } return c.OAuth.Username + case ProviderTailscale: + if c.Tailscale == nil { + return "" + } + return c.Tailscale.Username default: return "" } @@ -194,6 +219,11 @@ func (c *UserContext) GetEmail() string { return "" } return c.OAuth.Email + case ProviderTailscale: + if c.Tailscale == nil { + return "" + } + return c.Tailscale.Email default: return "" } @@ -221,6 +251,11 @@ func (c *UserContext) GetName() string { return "" } return c.OAuth.Name + case ProviderTailscale: + if c.Tailscale == nil { + return "" + } + return c.Tailscale.Name default: return "" } @@ -234,6 +269,8 @@ func (c *UserContext) GetProviderID() string { return "ldap" case ProviderOAuth: return c.OAuth.ID + case ProviderTailscale: + return "tailscale" default: return "unknown" } @@ -252,3 +289,10 @@ func (c *UserContext) OAuthName() string { } return "" } + +func (c *UserContext) TailscaleNodeName() string { + if c.Tailscale != nil { + return c.Tailscale.Username + } + return "" +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index c44cbb50..4b191c89 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -81,6 +81,7 @@ type AuthService struct { ldap *LdapService queries *repository.Queries oauthBroker *OAuthBrokerService + tailscale *TailscaleService loginAttempts map[string]*LoginAttempt ldapGroupsCache map[string]*LdapGroupsCache @@ -102,6 +103,7 @@ func NewAuthService( ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService, + tailscale *TailscaleService, ) *AuthService { service := &AuthService{ log: log, @@ -114,6 +116,7 @@ func NewAuthService( ldap: ldap, queries: queries, oauthBroker: oauthBroker, + tailscale: tailscale, } wg.Go(service.CleanupOAuthSessionsRoutine) @@ -326,11 +329,18 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess } if data.Provider == "tailscale" { - // TODO: use domain from tailscale to set cookie, this is mostly a hack for now - tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", c.Request.Host)) + if auth.tailscale == nil { + return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") + } + + auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname") + + tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname())) + if err != nil { return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err) } + return &http.Cookie{ Name: auth.runtime.SessionCookieName, Value: session.UUID, diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go index e5c0a907..1d4dc4eb 100644 --- a/internal/service/tailscale_service.go +++ b/internal/service/tailscale_service.go @@ -3,96 +3,96 @@ package service import ( "context" "errors" + "fmt" "net" + "strings" + "sync" - "github.com/tinyauthapp/tinyauth/internal/config" - "github.com/tinyauthapp/tinyauth/internal/utils/tlog" + "github.com/tinyauthapp/tinyauth/internal/model" + "github.com/tinyauthapp/tinyauth/internal/utils/logger" "tailscale.com/client/local" "tailscale.com/tsnet" ) -type TailscaleServiceConfig struct { - Dir string - Hostname string - AuthKey string - Ephemeral bool -} - type TailscaleService struct { - config TailscaleServiceConfig - srv *tsnet.Server - lc *local.Client - ln *net.Listener + log *logger.Logger + wg *sync.WaitGroup + config model.Config + ctx context.Context + + srv *tsnet.Server + lc *local.Client + ln *net.Listener } -func NewTailscaleService(config TailscaleServiceConfig) *TailscaleService { - return &TailscaleService{ - config: config, - } -} - -func (ts *TailscaleService) Init() error { +func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) { srv := new(tsnet.Server) // node options - srv.Dir = ts.config.Dir - srv.Hostname = ts.config.Hostname - srv.AuthKey = ts.config.AuthKey - srv.Ephemeral = ts.config.Ephemeral + srv.Dir = config.Tailscale.Dir + srv.Hostname = config.Tailscale.Hostname + srv.AuthKey = config.Tailscale.AuthKey + srv.Ephemeral = config.Tailscale.Ephemeral // redirect logs to zerolog - srv.Logf = tlog.App.Printf - srv.UserLogf = tlog.App.Printf + srv.Logf = log.App.Printf + srv.UserLogf = log.App.Printf err := srv.Start() if err != nil { - return err + return nil, fmt.Errorf("failed to start tailscale server: %w", err) } - ts.srv = srv - lc, err := srv.LocalClient() if err != nil { - return err + return nil, fmt.Errorf("failed to get tailscale local client: %w", err) } - ts.lc = lc - return nil + service := &TailscaleService{ + log: log, + wg: wg, + config: config, + ctx: ctx, + srv: srv, + lc: lc, + } + + wg.Go(service.watchAndClose) + + return service, nil } -func (ts *TailscaleService) Destroy() error { +func (ts *TailscaleService) watchAndClose() { + <-ts.ctx.Done() + ts.log.App.Debug().Msg("Shutting down Tailscale service") if ts.ln != nil { (*ts.ln).Close() } if ts.srv != nil { - return ts.srv.Close() + ts.srv.Close() } - ts.ln = nil - ts.lc = nil - ts.srv = nil - return nil } -func (ts *TailscaleService) Whois(ctx context.Context, addr string) (config.TailscaleWhoisResponse, error) { +func (ts *TailscaleService) Whois(ctx context.Context, addr string) (*model.TailscaleWhoisResponse, error) { who, err := ts.lc.WhoIs(ctx, addr) if err != nil { if errors.Is(err, local.ErrPeerNotFound) { - return config.TailscaleWhoisResponse{}, nil + return nil, nil } - return config.TailscaleWhoisResponse{}, err + return nil, fmt.Errorf("failed to get client whois: %w", err) } - res := config.TailscaleWhoisResponse{ + res := model.TailscaleWhoisResponse{ UserID: who.UserProfile.ID.String(), LoginName: who.UserProfile.LoginName, DisplayName: who.UserProfile.DisplayName, NodeName: who.Node.Name, } - return res, nil + return &res, nil } func (ts *TailscaleService) CreateListener() (net.Listener, error) { @@ -107,10 +107,13 @@ func (ts *TailscaleService) CreateListener() (net.Listener, error) { return ln, nil } -func (ts *TailscaleService) IsConnfigured() bool { - return ts.srv != nil -} - func (ts *TailscaleService) GetHostname() string { - return ts.srv.Hostname + status, err := ts.lc.Status(ts.ctx) + + if err != nil { + ts.log.App.Error().Err(err).Msg("Failed to get Tailscale status") + return "" + } + + return strings.TrimSuffix(status.Self.DNSName, ".") } From 32595e351d420657a13e2bd38e9eb27244598e89 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 10 May 2026 19:17:22 +0300 Subject: [PATCH 04/13] refactor: rework backend to frontend context --- frontend/src/App.tsx | 4 +- frontend/src/components/layout/layout.tsx | 18 +- frontend/src/pages/authorize-page.tsx | 4 +- frontend/src/pages/continue-page.tsx | 17 +- frontend/src/pages/forgot-password-page.tsx | 6 +- frontend/src/pages/login-page.tsx | 34 ++-- frontend/src/pages/logout-page.tsx | 96 +++++++---- frontend/src/pages/totp-page.tsx | 4 +- frontend/src/schemas/app-context-schema.ts | 25 ++- frontend/src/schemas/user-context-schema.ts | 30 +++- internal/bootstrap/app_bootstrap.go | 8 + internal/controller/context_controller.go | 158 +++++++++++------- .../controller/context_controller_test.go | 45 +++-- internal/middleware/context_middleware.go | 10 +- internal/model/runtime.go | 1 + internal/service/tailscale_service.go | 28 +++- 16 files changed, 326 insertions(+), 162 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0559b26f..f258eb98 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,9 +2,9 @@ import { Navigate } from "react-router"; import { useUserContext } from "./context/user-context"; export const App = () => { - const { isLoggedIn } = useUserContext(); + const { auth } = useUserContext(); - if (isLoggedIn) { + if (auth.authenticated) { return ; } diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx index a71a1aa9..d59aadf3 100644 --- a/frontend/src/components/layout/layout.tsx +++ b/frontend/src/components/layout/layout.tsx @@ -6,17 +6,17 @@ import { DomainWarning } from "../domain-warning/domain-warning"; import { ThemeToggle } from "../theme-toggle/theme-toggle"; const BaseLayout = ({ children }: { children: React.ReactNode }) => { - const { backgroundImage, title } = useAppContext(); + const { ui } = useAppContext(); useEffect(() => { - document.title = title; - }, [title]); + document.title = ui.title; + }, [ui.title]); return (
{ }; export const Layout = () => { - const { appUrl, warningsEnabled } = useAppContext(); + const { app, ui } = useAppContext(); const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => { return window.sessionStorage.getItem("ignoreDomainWarning") === "true"; }); @@ -42,11 +42,15 @@ export const Layout = () => { setIgnoreDomainWarning(true); }, [setIgnoreDomainWarning]); - if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) { + if ( + !ignoreDomainWarning && + ui.warningsEnabled && + !app.trustedDomains.includes(currentUrl) + ) { return ( handleIgnore()} /> diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index f2b7c11d..91f8f9c9 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -77,7 +77,7 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => { }; export const AuthorizePage = () => { - const { isLoggedIn } = useUserContext(); + const { auth } = useUserContext(); const { search } = useLocation(); const { t } = useTranslation(); const navigate = useNavigate(); @@ -127,7 +127,7 @@ export const AuthorizePage = () => { ); } - if (!isLoggedIn) { + if (!auth.authenticated) { return ; } diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index b7cdd743..82846c64 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRedirectUri } from "@/lib/hooks/redirect-uri"; export const ContinuePage = () => { - const { cookieDomain, warningsEnabled } = useAppContext(); - const { isLoggedIn } = useUserContext(); + const { app, ui } = useAppContext(); + const { auth } = useUserContext(); const { search } = useLocation(); const { t } = useTranslation(); const navigate = useNavigate(); @@ -29,17 +29,18 @@ export const ContinuePage = () => { const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri( redirectUri, - cookieDomain, + app.cookieDomain, ); const urlHref = url?.href; const hasValidRedirect = valid && allowedProto; - const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled; + const showUntrustedWarning = + hasValidRedirect && !trusted && ui.warningsEnabled; const showInsecureWarning = - hasValidRedirect && httpsDowngrade && warningsEnabled; + hasValidRedirect && httpsDowngrade && ui.warningsEnabled; const shouldAutoRedirect = - isLoggedIn && + auth.authenticated && hasValidRedirect && !showUntrustedWarning && !showInsecureWarning; @@ -77,7 +78,7 @@ export const ContinuePage = () => { }; }, [shouldAutoRedirect, redirectToTarget]); - if (!isLoggedIn) { + if (!auth.authenticated) { return ( { components={{ code: , }} - values={{ cookieDomain }} + values={{ cookieDomain: app.cookieDomain }} shouldUnescape={true} /> diff --git a/frontend/src/pages/forgot-password-page.tsx b/frontend/src/pages/forgot-password-page.tsx index 7d47d02f..6438e353 100644 --- a/frontend/src/pages/forgot-password-page.tsx +++ b/frontend/src/pages/forgot-password-page.tsx @@ -13,7 +13,7 @@ import Markdown from "react-markdown"; import { useLocation } from "react-router"; export const ForgotPasswordPage = () => { - const { forgotPasswordMessage } = useAppContext(); + const { ui } = useAppContext(); const { t } = useTranslation(); const { search } = useLocation(); const searchParams = new URLSearchParams(search); @@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => { - {forgotPasswordMessage !== "" - ? forgotPasswordMessage + {ui.forgotPasswordMessage !== "" + ? ui.forgotPasswordMessage : t("forgotPasswordMessage")} diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index c8c9f23d..91ad6411 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -36,13 +36,13 @@ const iconMap: Record = { }; export const LoginPage = () => { - const { isLoggedIn, tailscaleNodeName } = useUserContext(); - const { providers, title, oauthAutoRedirect } = useAppContext(); + const { auth, tailscale } = useUserContext(); + const { ui, oauth, auth: cauth } = useAppContext(); const { search } = useLocation(); const { t } = useTranslation(); const [showRedirectButton, setShowRedirectButton] = useState(false); - const [useTailscale, setUseTailscale] = useState(tailscaleNodeName !== ""); + const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== ""); const hasAutoRedirectedRef = useRef(false); @@ -56,15 +56,15 @@ export const LoginPage = () => { const oidcParams = useOIDCParams(searchParams); const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( - providers.find((provider) => provider.id === oauthAutoRedirect) !== + cauth.providers.find((provider) => provider.id === oauth.autoRedirect) !== undefined && redirectUri !== undefined, ); - const oauthProviders = providers.filter( + const oauthProviders = cauth.providers.filter( (provider) => provider.id !== "local" && provider.id !== "ldap", ); const userAuthConfigured = - providers.find( + cauth.providers.find( (provider) => provider.id === "local" || provider.id === "ldap", ) !== undefined; @@ -177,19 +177,19 @@ export const LoginPage = () => { useEffect(() => { if ( - !isLoggedIn && + !auth.authenticated && isOauthAutoRedirect && !hasAutoRedirectedRef.current && redirectUri !== undefined ) { hasAutoRedirectedRef.current = true; - oauthMutate(oauthAutoRedirect); + oauthMutate(oauth.autoRedirect); } }, [ - isLoggedIn, + auth.authenticated, oauthMutate, hasAutoRedirectedRef, - oauthAutoRedirect, + oauth.autoRedirect, isOauthAutoRedirect, redirectUri, ]); @@ -206,11 +206,11 @@ export const LoginPage = () => { }; }, [redirectTimer, redirectButtonTimer]); - if (isLoggedIn && oidcParams.isOidc) { + if (auth.authenticated && oidcParams.isOidc) { return ; } - if (isLoggedIn && redirectUri !== undefined) { + if (auth.authenticated && redirectUri !== undefined) { return ( { ); } - if (isLoggedIn) { + if (auth.authenticated) { return ; } @@ -272,7 +272,7 @@ export const LoginPage = () => { credentials?
- Machine Name: {tailscaleNodeName} + Machine Name: {tailscale.nodeName}
@@ -299,8 +299,8 @@ export const LoginPage = () => { return ( - {title} - {providers.length > 0 && ( + {ui.title} + {cauth.providers.length > 0 && ( {oauthProviders.length !== 0 ? t("loginTitle") @@ -338,7 +338,7 @@ export const LoginPage = () => { })()} /> )} - {providers.length == 0 && ( + {cauth.providers.length == 0 && (
             {t("failedToFetchProvidersTitle")}
           
diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index f60bd656..7f500d7e 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -13,9 +13,11 @@ import { useEffect, useRef } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Navigate } from "react-router"; import { toast } from "sonner"; +import { type UseMutationResult } from "@tanstack/react-query"; +import { type AxiosResponse } from "axios"; export const LogoutPage = () => { - const { provider, username, isLoggedIn, email, oauthName } = useUserContext(); + const { auth, oauth, tailscale } = useUserContext(); const { t } = useTranslation(); const redirectTimer = useRef(null); @@ -47,42 +49,74 @@ export const LogoutPage = () => { }; }, [redirectTimer]); - if (!isLoggedIn) { + if (!auth.authenticated) { return ; } + if (oauth.active) { + return ( + + , + }} + values={{ + username: auth.email, + provider: oauth.displayName, + }} + shouldUnescape={true} + /> + + ); + } + + if (auth.providerId === "tailscale") { + return ( + + You are currently logged in with the Tailscale integration identified by + the {tailscale.nodeName} node. Click the button below to + log out. + + ); + } + + return ( + + , + }} + values={{ + username: auth.username, + }} + shouldUnescape={true} + /> + + ); +}; + +interface LogoutLayoutProps { + children: React.ReactNode; + logoutMutation: UseMutationResult< + //eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-empty-object-type + AxiosResponse, + Error, + void, + unknown + >; +} + +function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) { + const { t } = useTranslation(); return ( {t("logoutTitle")} - - {provider !== "local" && provider !== "ldap" ? ( - , - }} - values={{ - username: email, - provider: oauthName, - }} - shouldUnescape={true} - /> - ) : ( - , - }} - values={{ - username, - }} - shouldUnescape={true} - /> - )} - + {children} diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index 7f500d7e..bd1704c0 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -75,9 +75,17 @@ export const LogoutPage = () => { if (auth.providerId === "tailscale") { return ( - You are currently logged in with the Tailscale integration identified by - the {tailscale.nodeName} node. Click the button below to - log out. + , + }} + values={{ + deviceName: tailscale.nodeName, + }} + shouldUnescape={true} + /> ); } diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 630413a4..b92dfe5c 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -396,7 +396,7 @@ func (app *BootstrapApp) serveUnix() error { } func (app *BootstrapApp) serveTailscale() error { - app.log.App.Info().Msgf("Starting Tailscale server on %s", app.services.tailscaleService.GetHostname()) + app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname())) listener, err := app.services.tailscaleService.CreateListener() From 35cd3b9ce5f4d203ca6b1a49d588bf92b6fc38d0 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 11 May 2026 16:32:30 +0300 Subject: [PATCH 10/13] refactor: simplify listener logic --- internal/bootstrap/app_bootstrap.go | 179 ++++--------------------- internal/bootstrap/router_bootstrap.go | 128 ++++++++++++++++++ 2 files changed, 151 insertions(+), 156 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index b92dfe5c..3f104a61 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "net" "net/http" "net/url" "os" @@ -38,16 +37,17 @@ type Services struct { } type BootstrapApp struct { - config model.Config - runtime model.RuntimeConfig - services Services - log *logger.Logger - ctx context.Context - cancel context.CancelFunc - queries *repository.Queries - router *gin.Engine - db *sql.DB - wg sync.WaitGroup + config model.Config + runtime model.RuntimeConfig + services Services + log *logger.Logger + ctx context.Context + cancel context.CancelFunc + queries *repository.Queries + router *gin.Engine + db *sql.DB + wg sync.WaitGroup + listeners []Listener } func NewBootstrapApp(config model.Config) *BootstrapApp { @@ -254,56 +254,32 @@ func (app *BootstrapApp) Setup() error { app.wg.Go(app.heartbeatRoutine) } - // create err channel to listen for server errors - errChanLen := 0 - + // setup listeners runUnix := app.config.Server.SocketPath != "" runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled runTailscale := app.services.tailscaleService != nil - if runUnix { - errChanLen++ + if runHTTP { + app.listeners = append(app.listeners, ListenerHTTP) } - if runHTTP { - errChanLen++ + if runUnix { + app.listeners = append(app.listeners, ListenerUnix) } if runTailscale { - errChanLen++ + app.listeners = append(app.listeners, ListenerTailscale) } - errChan := make(chan error, errChanLen) - if app.config.Server.ConcurrentListenersEnabled { app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners") } - // serve unix - if runUnix { - app.wg.Go(func() { - if err := app.serveUnix(); err != nil { - errChan <- err - } - }) - } - - // serve to http - if runHTTP { - app.wg.Go(func() { - if err := app.serveHTTP(); err != nil { - errChan <- err - } - }) - } + // run listeners + lec, err := app.runListeners() - // serve to tailscale - if runTailscale { - app.wg.Go(func() { - if err := app.serveTailscale(); err != nil { - errChan <- err - } - }) + if err != nil { + return fmt.Errorf("failed to run listeners: %w", err) } // monitor cancellation and server errors @@ -312,123 +288,14 @@ func (app *BootstrapApp) Setup() error { case <-app.ctx.Done(): app.log.App.Info().Msg("Oh, it's time for me to go, bye!") return nil - case err := <-errChan: + case err := <-lec: if err != nil { - return fmt.Errorf("server error: %w", err) + return fmt.Errorf("listener error: %w", err) } } } } -func (app *BootstrapApp) serveHTTP() error { - address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port) - - app.log.App.Info().Msgf("Starting server on %s", address) - - server := &http.Server{ - Addr: address, - Handler: app.router.Handler(), - } - - go func() { - <-app.ctx.Done() - app.log.App.Debug().Msg("Shutting down http listener") - server.Shutdown(app.ctx) - }() - - err := server.ListenAndServe() - - if err != nil && !errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("failed to start http listener: %w", err) - } - - return nil -} - -func (app *BootstrapApp) serveUnix() error { - if app.config.Server.SocketPath == "" { - return nil - } - - _, err := os.Stat(app.config.Server.SocketPath) - - if err == nil { - app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath) - err := os.Remove(app.config.Server.SocketPath) - - if err != nil { - return fmt.Errorf("failed to remove existing socket file: %w", err) - } - } - - app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath) - - listener, err := net.Listen("unix", app.config.Server.SocketPath) - - if err != nil { - return fmt.Errorf("failed to create unix socket listener: %w", err) - } - - server := &http.Server{ - Handler: app.router.Handler(), - } - - shutdown := func() { - server.Shutdown(app.ctx) - listener.Close() - os.Remove(app.config.Server.SocketPath) - } - - go func() { - <-app.ctx.Done() - app.log.App.Debug().Msg("Shutting down unix socket listener") - shutdown() - }() - - err = server.Serve(listener) - - if err != nil && !errors.Is(err, http.ErrServerClosed) { - shutdown() - return fmt.Errorf("failed to start unix socket listener: %w", err) - } - - return nil -} - -func (app *BootstrapApp) serveTailscale() error { - app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname())) - - listener, err := app.services.tailscaleService.CreateListener() - - if err != nil { - return fmt.Errorf("failed to create tailscale listener: %w", err) - } - - server := &http.Server{ - Handler: app.router.Handler(), - } - - shutdown := func() { - server.Shutdown(app.ctx) - listener.Close() - } - - go func() { - <-app.ctx.Done() - app.log.App.Debug().Msg("Shutting down Tailscale listener") - shutdown() - }() - - err = server.Serve(listener) - - if err != nil && !errors.Is(err, http.ErrServerClosed) { - shutdown() - return fmt.Errorf("failed to start tailscale listener: %w", err) - } - - return nil -} - func (app *BootstrapApp) heartbeatRoutine() { ticker := time.NewTicker(time.Duration(12) * time.Hour) defer ticker.Stop() diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 7f02af47..02e409ad 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -1,7 +1,11 @@ package bootstrap import ( + "errors" "fmt" + "net" + "net/http" + "os" "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/middleware" @@ -9,6 +13,14 @@ import ( "github.com/gin-gonic/gin" ) +type Listener int + +const ( + ListenerHTTP Listener = iota + ListenerUnix + ListenerTailscale +) + func (app *BootstrapApp) setupRouter() error { // we don't want gin debug mode gin.SetMode(gin.ReleaseMode) @@ -53,3 +65,119 @@ func (app *BootstrapApp) setupRouter() error { app.router = engine return nil } + +func (app *BootstrapApp) runListeners() (chan error, error) { + // lec -> listener error channel + lec := make(chan error, len(app.listeners)) + + for _, listenerType := range app.listeners { + listenerFunc, err := app.listenerFromType(listenerType) + + if err != nil { + return nil, fmt.Errorf("failed to get listener function: %w", err) + } + + app.wg.Go(func() { + lec <- listenerFunc() + }) + } + + return lec, nil +} + +func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) { + switch listenerType { + case ListenerHTTP: + return app.serveHTTP, nil + case ListenerUnix: + return app.serveUnix, nil + case ListenerTailscale: + return app.serveTailscale, nil + default: + return nil, fmt.Errorf("invalid listener type: %d", listenerType) + } +} + +func (app *BootstrapApp) serveHTTP() error { + address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port) + + app.log.App.Info().Msgf("Starting server on %s", address) + + listener, err := net.Listen("tcp", address) + + if err != nil { + return fmt.Errorf("failed to create tcp listener: %w", err) + } + + server := &http.Server{ + Addr: address, + Handler: app.router.Handler(), + } + + return app.serve(listener, server, "http") +} + +func (app *BootstrapApp) serveUnix() error { + _, err := os.Stat(app.config.Server.SocketPath) + + if err == nil { + app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath) + err := os.Remove(app.config.Server.SocketPath) + + if err != nil { + return fmt.Errorf("failed to remove existing socket file: %w", err) + } + } + + app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath) + + listener, err := net.Listen("unix", app.config.Server.SocketPath) + + if err != nil { + return fmt.Errorf("failed to create unix socket listener: %w", err) + } + + server := &http.Server{ + Handler: app.router.Handler(), + } + + return app.serve(listener, server, "unix socket") +} + +func (app *BootstrapApp) serveTailscale() error { + app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname())) + + listener, err := app.services.tailscaleService.CreateListener() + + if err != nil { + return fmt.Errorf("failed to create tailscale listener: %w", err) + } + + server := &http.Server{ + Handler: app.router.Handler(), + } + + return app.serve(listener, server, "tailscale") +} + +func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error { + shutdown := func() { + server.Shutdown(app.ctx) + listener.Close() + } + + go func() { + <-app.ctx.Done() + app.log.App.Debug().Msgf("Shutting down %s listener", name) + shutdown() + }() + + err := server.Serve(listener) + + if err != nil && !errors.Is(err, http.ErrServerClosed) { + shutdown() + return fmt.Errorf("failed to start %s listener: %w", name, err) + } + + return nil +} From b8b0e45ec40ff2881b7485f559256ab1a8e486fd Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 11 May 2026 18:47:37 +0300 Subject: [PATCH 11/13] feat: add listener policy calculator --- internal/bootstrap/app_bootstrap.go | 16 +---------- internal/bootstrap/router_bootstrap.go | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 3f104a61..526f4351 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -255,21 +255,7 @@ func (app *BootstrapApp) Setup() error { } // setup listeners - runUnix := app.config.Server.SocketPath != "" - runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled - runTailscale := app.services.tailscaleService != nil - - if runHTTP { - app.listeners = append(app.listeners, ListenerHTTP) - } - - if runUnix { - app.listeners = append(app.listeners, ListenerUnix) - } - - if runTailscale { - app.listeners = append(app.listeners, ListenerTailscale) - } + app.listeners = app.calculateListenerPolicy() if app.config.Server.ConcurrentListenersEnabled { app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners") diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 02e409ad..0bff3b4e 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -85,6 +85,43 @@ func (app *BootstrapApp) runListeners() (chan error, error) { return lec, nil } +// The way we calculate listeners is as follows: +// If concurrent listeners are disabled, we pick the first available listener, so: +// 1. If tailscale is enabled, we use tailscale +// 2. If socket path is configured, we use unix socket +// 3. Finally if none is configured we use http +// If concurrent listeners are enabled, we add all available listeners in the following order +func (app *BootstrapApp) calculateListenerPolicy() []Listener { + l := []Listener{} + + if !app.config.Server.ConcurrentListenersEnabled { + if app.config.Tailscale.Enabled { + l = append(l, ListenerTailscale) + return l + } + + if app.config.Server.SocketPath != "" { + l = append(l, ListenerUnix) + return l + } + + l = append(l, ListenerHTTP) + return l + } + + if app.config.Server.SocketPath != "" { + l = append(l, ListenerUnix) + } + + if app.config.Tailscale.Enabled { + l = append(l, ListenerTailscale) + } + + l = append(l, ListenerHTTP) + + return l +} + func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) { switch listenerType { case ListenerHTTP: From c298d0b1587ab20cdd3e14d880dca7610e2f6014 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 11 May 2026 18:48:46 +0300 Subject: [PATCH 12/13] i18n: use friendlier tone in tailscale i18n --- frontend/src/lib/i18n/locales/en-US.json | 2 +- frontend/src/lib/i18n/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index 49b96659..a71696e2 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -86,7 +86,7 @@ "addressScopeName": "Address", "addressScopeDescription": "Allows the app to access your address.", "loginTailscaleTitle": "Continue with Tailscale", - "loginTailscaleDescription": "We detected that you are accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", + "loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", "loginTailscaleDeviceName": "Device name:", "loginTailscaleSubmit": "Continue with Tailscale", "loginTailscaleOtherMethod": "Login with another method", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 49b96659..a71696e2 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -86,7 +86,7 @@ "addressScopeName": "Address", "addressScopeDescription": "Allows the app to access your address.", "loginTailscaleTitle": "Continue with Tailscale", - "loginTailscaleDescription": "We detected that you are accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", + "loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", "loginTailscaleDeviceName": "Device name:", "loginTailscaleSubmit": "Continue with Tailscale", "loginTailscaleOtherMethod": "Login with another method", From b6df4b9f0abd6e7f69421bc5e65bd6b16da0438b Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 11 May 2026 19:10:50 +0300 Subject: [PATCH 13/13] fix: coderabbit comments --- internal/bootstrap/router_bootstrap.go | 10 +++++++++- internal/controller/user_controller.go | 2 +- internal/middleware/context_middleware.go | 1 + internal/model/constants.go | 2 ++ internal/service/auth_service.go | 8 ++++---- internal/service/tailscale_service.go | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 0bff3b4e..f072134f 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -1,14 +1,17 @@ package bootstrap import ( + "context" "errors" "fmt" "net" "net/http" "os" + "time" "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/middleware" + "github.com/tinyauthapp/tinyauth/internal/model" "github.com/gin-gonic/gin" ) @@ -199,7 +202,12 @@ func (app *BootstrapApp) serveTailscale() error { func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error { shutdown := func() { - server.Shutdown(app.ctx) + ctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second) + defer cancel() + err := server.Shutdown(ctx) + if err != nil { + app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name) + } listener.Close() } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 72ab6e04..e235e571 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -424,7 +424,7 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) { cookie, err := controller.auth.CreateSession(c, sessionCookie) if err != nil { - controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification") + controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login") c.JSON(500, gin.H{ "status": 500, "message": "Internal Server Error", diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index f8f9ca8e..f5b7fc63 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -116,6 +116,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { if tailscaleContext != nil { c.Set("context", &model.UserContext{ Authenticated: false, + Provider: model.ProviderTailscale, Tailscale: tailscaleContext, }) } diff --git a/internal/model/constants.go b/internal/model/constants.go index d9e85e57..d5885dcf 100644 --- a/internal/model/constants.go +++ b/internal/model/constants.go @@ -21,3 +21,5 @@ const SessionCookieName = "tinyauth-session" const CSRFCookieName = "tinyauth-csrf" const RedirectCookieName = "tinyauth-redirect" const OAuthSessionCookieName = "tinyauth-oauth" + +const GracefulShutdownTimeout = 5 // seconds diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 4b191c89..944d7307 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -292,6 +292,10 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool { } func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) { + if data.Provider == "tailscale" && auth.tailscale == nil { + return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") + } + uuid, err := uuid.NewRandom() if err != nil { @@ -329,10 +333,6 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess } if data.Provider == "tailscale" { - if auth.tailscale == nil { - return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") - } - auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname") tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname())) diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go index 0ed74dad..9d341063 100644 --- a/internal/service/tailscale_service.go +++ b/internal/service/tailscale_service.go @@ -64,7 +64,7 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co lc: lc, } - connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed defer cancel() err = service.waitForConn(connectCtx)