feat: initial tailscale backend

This commit is contained in:
Stavros
2026-04-28 18:16:55 +03:00
parent d73cc628fb
commit a5677d2558
8 changed files with 409 additions and 28 deletions
+46 -9
View File
@@ -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() {
+1 -1
View File
@@ -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()
+24
View File
@@ -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,
+19
View File
@@ -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
+44 -13
View File
@@ -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
}
+116
View File
@@ -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
}