mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
516 lines
15 KiB
Go
516 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/bufbuild/connect-go"
|
|
"github.com/jsiebens/ionscale/internal/domain"
|
|
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
"net/netip"
|
|
"strings"
|
|
"tailscale.com/util/dnsname"
|
|
"time"
|
|
)
|
|
|
|
func (s *Service) machineToApi(m *domain.Machine) *api.Machine {
|
|
var lastSeen *timestamppb.Timestamp
|
|
|
|
var name = m.Name
|
|
if m.NameIdx != 0 {
|
|
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
|
}
|
|
|
|
online := s.sessionManager.HasSession(m.TailnetID, m.ID)
|
|
if m.LastSeen != nil {
|
|
lastSeen = timestamppb.New(*m.LastSeen)
|
|
}
|
|
|
|
var endpoints []string
|
|
for _, e := range m.Endpoints {
|
|
endpoints = append(endpoints, e.String())
|
|
}
|
|
|
|
return &api.Machine{
|
|
Id: m.ID,
|
|
Name: name,
|
|
Ipv4: m.IPv4.String(),
|
|
Ipv6: m.IPv6.String(),
|
|
Ephemeral: m.Ephemeral,
|
|
Tags: m.Tags,
|
|
LastSeen: lastSeen,
|
|
CreatedAt: timestamppb.New(m.CreatedAt),
|
|
ExpiresAt: timestamppb.New(m.ExpiresAt),
|
|
KeyExpiryDisabled: m.KeyExpiryDisabled,
|
|
Connected: online,
|
|
Os: m.HostInfo.OS,
|
|
ClientVersion: m.HostInfo.IPNVersion,
|
|
Tailnet: &api.Ref{
|
|
Id: m.Tailnet.ID,
|
|
Name: m.Tailnet.Name,
|
|
},
|
|
User: &api.Ref{
|
|
Id: m.User.ID,
|
|
Name: m.User.Name,
|
|
},
|
|
ClientConnectivity: &api.ClientConnectivity{
|
|
Endpoints: endpoints,
|
|
},
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
Authorized: m.Authorized,
|
|
}
|
|
}
|
|
|
|
func (s *Service) ListMachines(ctx context.Context, req *connect.Request[api.ListMachinesRequest]) (*connect.Response[api.ListMachinesResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
if tailnet == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet not found"))
|
|
}
|
|
|
|
machines, err := s.repository.ListMachineByTailnet(ctx, tailnet.ID)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
response := &api.ListMachinesResponse{}
|
|
for _, m := range machines {
|
|
response.Machines = append(response.Machines, s.machineToApi(&m))
|
|
}
|
|
|
|
return connect.NewResponse(response), nil
|
|
}
|
|
|
|
func (s *Service) GetMachine(ctx context.Context, req *connect.Request[api.GetMachineRequest]) (*connect.Response[api.GetMachineResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
return connect.NewResponse(&api.GetMachineResponse{Machine: s.machineToApi(m)}), nil
|
|
}
|
|
|
|
func (s *Service) DeleteMachine(ctx context.Context, req *connect.Request[api.DeleteMachineRequest]) (*connect.Response[api.DeleteMachineResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if _, err := s.repository.DeleteMachine(ctx, req.Msg.MachineId); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.DeleteMachineResponse{}), nil
|
|
}
|
|
|
|
func (s *Service) ExpireMachine(ctx context.Context, req *connect.Request[api.ExpireMachineRequest]) (*connect.Response[api.ExpireMachineResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
timestamp := time.Unix(123, 0)
|
|
m.ExpiresAt = timestamp
|
|
m.KeyExpiryDisabled = false
|
|
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.ExpireMachineResponse{}), nil
|
|
}
|
|
|
|
func (s *Service) SetMachineName(ctx context.Context, req *connect.Request[api.SetMachineNameRequest]) (*connect.Response[api.SetMachineNameResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if req.Msg.UseOsHostname {
|
|
sanitizeHostname := dnsname.SanitizeHostname(m.HostInfo.Hostname)
|
|
nameIdx, err := s.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
m.Name = sanitizeHostname
|
|
m.NameIdx = nameIdx
|
|
m.UseOSHostname = true
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
|
|
}
|
|
|
|
if strings.TrimSpace(req.Msg.Name) == "" {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("machine name is required when not using os hostname"))
|
|
}
|
|
|
|
sanitizeHostname := dnsname.SanitizeHostname(req.Msg.Name)
|
|
|
|
if sanitizeHostname == m.Name {
|
|
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
|
|
}
|
|
|
|
nameIdx, err := s.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if nameIdx > 0 {
|
|
return nil, connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("machine name already in use"))
|
|
}
|
|
|
|
m.Name = sanitizeHostname
|
|
m.NameIdx = 0
|
|
m.UseOSHostname = false
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
|
|
}
|
|
|
|
func (s *Service) AuthorizeMachine(ctx context.Context, req *connect.Request[api.AuthorizeMachineRequest]) (*connect.Response[api.AuthorizeMachineResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if !m.Authorized {
|
|
m.Authorized = true
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.AuthorizeMachineResponse{}), nil
|
|
}
|
|
|
|
func (s *Service) GetMachineRoutes(ctx context.Context, req *connect.Request[api.GetMachineRoutesRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
response := api.GetMachineRoutesResponse{
|
|
MachineId: m.ID,
|
|
Routes: &api.MachineRoutes{
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
},
|
|
}
|
|
|
|
return connect.NewResponse(&response), nil
|
|
}
|
|
|
|
func (s *Service) EnableMachineRoutes(ctx context.Context, req *connect.Request[api.EnableMachineRoutesRequest]) (*connect.Response[api.EnableMachineRoutesResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
var allowIPs = domain.NewAllowIPsSet(m.AllowIPs)
|
|
var autoAllowIPs = domain.NewAllowIPsSet(m.AutoAllowIPs)
|
|
|
|
if req.Msg.Replace {
|
|
allowIPs = domain.NewAllowIPsSet([]netip.Prefix{})
|
|
autoAllowIPs = domain.NewAllowIPsSet([]netip.Prefix{})
|
|
}
|
|
|
|
for _, r := range req.Msg.Routes {
|
|
prefix, err := netip.ParsePrefix(r)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
allowIPs.Add(prefix)
|
|
}
|
|
|
|
m.AllowIPs = allowIPs.Items()
|
|
m.AutoAllowIPs = autoAllowIPs.Items()
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
response := api.EnableMachineRoutesResponse{
|
|
MachineId: m.ID,
|
|
Routes: &api.MachineRoutes{
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
},
|
|
}
|
|
|
|
return connect.NewResponse(&response), nil
|
|
}
|
|
|
|
func (s *Service) DisableMachineRoutes(ctx context.Context, req *connect.Request[api.DisableMachineRoutesRequest]) (*connect.Response[api.DisableMachineRoutesResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
|
autoAllowIPs := domain.NewAllowIPsSet(m.AutoAllowIPs)
|
|
|
|
for _, r := range req.Msg.Routes {
|
|
prefix, err := netip.ParsePrefix(r)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
allowIPs.Remove(prefix)
|
|
autoAllowIPs.Remove(prefix)
|
|
}
|
|
|
|
m.AllowIPs = allowIPs.Items()
|
|
m.AutoAllowIPs = autoAllowIPs.Items()
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
response := api.DisableMachineRoutesResponse{
|
|
MachineId: m.ID,
|
|
Routes: &api.MachineRoutes{
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
},
|
|
}
|
|
|
|
return connect.NewResponse(&response), nil
|
|
}
|
|
|
|
func (s *Service) EnableExitNode(ctx context.Context, req *connect.Request[api.EnableExitNodeRequest]) (*connect.Response[api.EnableExitNodeResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if !m.IsAdvertisedExitNode() {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("machine is not a valid exit node"))
|
|
}
|
|
|
|
prefix4 := netip.MustParsePrefix("0.0.0.0/0")
|
|
prefix6 := netip.MustParsePrefix("::/0")
|
|
|
|
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
|
allowIPs.Add(prefix4, prefix6)
|
|
|
|
m.AllowIPs = allowIPs.Items()
|
|
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
response := api.EnableExitNodeResponse{
|
|
MachineId: m.ID,
|
|
Routes: &api.MachineRoutes{
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
},
|
|
}
|
|
|
|
return connect.NewResponse(&response), nil
|
|
}
|
|
|
|
func (s *Service) DisableExitNode(ctx context.Context, req *connect.Request[api.DisableExitNodeRequest]) (*connect.Response[api.DisableExitNodeResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if !m.IsAdvertisedExitNode() {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("machine is not a valid exit node"))
|
|
}
|
|
|
|
prefix4 := netip.MustParsePrefix("0.0.0.0/0")
|
|
prefix6 := netip.MustParsePrefix("::/0")
|
|
|
|
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
|
allowIPs.Remove(prefix4, prefix6)
|
|
|
|
autoAllowIPs := domain.NewAllowIPsSet(m.AutoAllowIPs)
|
|
autoAllowIPs.Remove(prefix4, prefix6)
|
|
|
|
m.AllowIPs = allowIPs.Items()
|
|
m.AutoAllowIPs = autoAllowIPs.Items()
|
|
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
response := api.DisableExitNodeResponse{
|
|
MachineId: m.ID,
|
|
Routes: &api.MachineRoutes{
|
|
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
|
EnabledRoutes: m.AllowedPrefixes(),
|
|
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
|
EnabledExitNode: m.IsAllowedExitNode(),
|
|
},
|
|
}
|
|
|
|
return connect.NewResponse(&response), nil
|
|
}
|
|
|
|
func (s *Service) SetMachineKeyExpiry(ctx context.Context, req *connect.Request[api.SetMachineKeyExpiryRequest]) (*connect.Response[api.SetMachineKeyExpiryResponse], error) {
|
|
principal := CurrentPrincipal(ctx)
|
|
|
|
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
|
if err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
if m == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
|
|
}
|
|
|
|
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
m.KeyExpiryDisabled = req.Msg.Disabled
|
|
|
|
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
|
return nil, logError(err)
|
|
}
|
|
|
|
s.sessionManager.NotifyAll(m.TailnetID)
|
|
|
|
return connect.NewResponse(&api.SetMachineKeyExpiryResponse{}), nil
|
|
}
|