feat: add basic login functionality back after main merge

This commit is contained in:
Stavros
2026-05-10 17:50:01 +03:00
parent 312671c795
commit 25017a76c9
16 changed files with 434 additions and 231 deletions
+50 -1
View File
@@ -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() {
+1 -1
View File
@@ -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()
+9 -1
View File
@@ -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)
+22 -20
View File
@@ -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)
+1 -1
View File
@@ -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 {
+19 -16
View File
@@ -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",
+1 -1
View File
@@ -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
+59 -24
View File
@@ -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
}
@@ -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()
+15 -89
View File
@@ -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"`
}
+45 -1
View File
@@ -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 ""
}
+12 -2
View File
@@ -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,
+51 -48
View File
@@ -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, ".")
}