mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-06-10 05:20:14 +00:00
feat: tailscale integration (#847)
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -35,20 +34,22 @@ type Services struct {
|
||||
ldapService *service.LdapService
|
||||
oauthBrokerService *service.OAuthBrokerService
|
||||
oidcService *service.OIDCService
|
||||
tailscaleService *service.TailscaleService
|
||||
policyEngine *service.PolicyEngine
|
||||
}
|
||||
|
||||
type BootstrapApp struct {
|
||||
config model.Config
|
||||
runtime model.RuntimeConfig
|
||||
services Services
|
||||
log *logger.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
queries repository.Store
|
||||
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.Store
|
||||
router *gin.Engine
|
||||
db *sql.DB
|
||||
wg sync.WaitGroup
|
||||
listeners []Listener
|
||||
}
|
||||
|
||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||
@@ -68,6 +69,8 @@ func (app *BootstrapApp) Setup() error {
|
||||
log.Init()
|
||||
app.log = log
|
||||
|
||||
app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
|
||||
|
||||
// get app url
|
||||
if app.config.AppURL == "" {
|
||||
return errors.New("app url cannot be empty, perhaps config loading failed")
|
||||
@@ -80,6 +83,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
|
||||
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
|
||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.AppURL)
|
||||
|
||||
// validate session config
|
||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||
@@ -231,6 +235,11 @@ func (app *BootstrapApp) Setup() error {
|
||||
|
||||
app.runtime.ConfiguredProviders = configuredProviders
|
||||
|
||||
// throw in tailscale if it's configured just before setting up the controllers
|
||||
if app.services.tailscaleService != nil {
|
||||
app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname())
|
||||
}
|
||||
|
||||
// setup router
|
||||
err = app.setupRouter()
|
||||
|
||||
@@ -248,42 +257,18 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.wg.Go(app.heartbeatRoutine)
|
||||
}
|
||||
|
||||
// create err channel to listen for server errors
|
||||
errChanLen := 0
|
||||
|
||||
runUnix := app.config.Server.SocketPath != ""
|
||||
runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled
|
||||
|
||||
if runUnix {
|
||||
errChanLen++
|
||||
}
|
||||
|
||||
if runHTTP {
|
||||
errChanLen++
|
||||
}
|
||||
|
||||
errChan := make(chan error, errChanLen)
|
||||
// setup listeners
|
||||
app.listeners = app.calculateListenerPolicy()
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
// run listeners
|
||||
lec, err := app.runListeners()
|
||||
|
||||
// serve to http
|
||||
if runHTTP {
|
||||
app.wg.Go(func() {
|
||||
if err := app.serveHTTP(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run listeners: %w", err)
|
||||
}
|
||||
|
||||
// monitor cancellation and server errors
|
||||
@@ -292,89 +277,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) heartbeatRoutine() {
|
||||
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -24,7 +39,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()
|
||||
@@ -53,3 +68,161 @@ 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
|
||||
}
|
||||
|
||||
// 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:
|
||||
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() {
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/service"
|
||||
@@ -22,6 +23,14 @@ func (app *BootstrapApp) setupServices() error {
|
||||
return fmt.Errorf("failed to initialize label provider: %w", err)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
app.services.tailscaleService = tailscaleService
|
||||
|
||||
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||
app.services.accessControlService = accessControlsService
|
||||
|
||||
@@ -34,7 +43,7 @@ func (app *BootstrapApp) setupServices() error {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user