Files
tinyauth/internal/service/tailscale_service.go
T

151 lines
3.1 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"tailscale.com/client/local"
"tailscale.com/tsnet"
)
type TailscaleService struct {
log *logger.Logger
wg *sync.WaitGroup
config model.Config
ctx context.Context
srv *tsnet.Server
lc *local.Client
ln *net.Listener
}
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) {
if !config.Tailscale.Enabled {
return nil, nil
}
srv := new(tsnet.Server)
// node options
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 = log.App.Printf
srv.UserLogf = log.App.Printf
err := srv.Start()
if err != nil {
return nil, fmt.Errorf("failed to start tailscale server: %w", err)
}
lc, err := srv.LocalClient()
if err != nil {
return nil, fmt.Errorf("failed to get tailscale local client: %w", err)
}
service := &TailscaleService{
log: log,
wg: wg,
config: config,
ctx: ctx,
srv: srv,
lc: lc,
}
connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
err = service.waitForConn(connectCtx)
if err != nil {
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
}
wg.Go(service.watchAndClose)
return service, nil
}
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 {
ts.srv.Close()
}
}
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 nil, nil
}
return nil, fmt.Errorf("failed to get client whois: %w", err)
}
res := model.TailscaleWhoisResponse{
UserID: who.UserProfile.ID.String(),
LoginName: who.UserProfile.LoginName,
DisplayName: who.UserProfile.DisplayName,
NodeName: strings.TrimSuffix(who.Node.Name, "."),
Tags: who.Node.Tags,
}
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) GetHostname() string {
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, ".")
}
func (ts *TailscaleService) waitForConn(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return fmt.Errorf("timed out waiting for tailscale connection")
default:
ip4, _ := ts.srv.TailscaleIPs()
if !ip4.IsValid() {
time.Sleep(1 * time.Second)
continue
}
return nil
}
}
}