This commit is contained in:
2026-04-10 01:46:57 +09:00
commit ddd6ecccda
15 changed files with 1370 additions and 0 deletions

130
internal/services/config.go Normal file
View File

@@ -0,0 +1,130 @@
package services
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
)
type Server struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
}
type Config struct {
Servers []Server `json:"servers"`
SelectedServerID string `json:"selectedServerId"`
}
type ConfigService struct {
path string
config Config
}
func NewConfigService() (*ConfigService, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("get config dir: %w", err)
}
dir := filepath.Join(configDir, "tailscale-vpn")
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, fmt.Errorf("create config dir: %w", err)
}
path := filepath.Join(dir, "settings.json")
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
if err := os.WriteFile(path, []byte(`{"servers":[]}`), 0o600); err != nil {
return nil, fmt.Errorf("create config file: %w", err)
}
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config file: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &ConfigService{path: path, config: cfg}, nil
}
func (s *ConfigService) GetServers() []Server {
return s.config.Servers
}
func (s *ConfigService) GetServer(id string) (Server, bool) {
for _, srv := range s.config.Servers {
if srv.ID == id {
return srv, true
}
}
return Server{}, false
}
func (s *ConfigService) AddServer(name, host string) error {
srv := Server{
ID: host,
Name: name,
Host: host,
}
s.config.Servers = append(s.config.Servers, srv)
return s.Save()
}
func (s *ConfigService) RemoveServer(id string) error {
s.config.Servers = slices.DeleteFunc(s.config.Servers, func(srv Server) bool {
return srv.ID == id
})
return s.Save()
}
func (s *ConfigService) GetSelectedServer() (Server, bool) {
for _, srv := range s.config.Servers {
if srv.ID == s.config.SelectedServerID {
return srv, true
}
}
return Server{}, false
}
func (s *ConfigService) SetSelectedServer(id string) error {
for _, srv := range s.config.Servers {
if srv.ID == id {
s.config.SelectedServerID = id
return s.Save()
}
}
return fmt.Errorf("server not found")
}
func (s *ConfigService) UpdateServer(id, name, host string) error {
for i, srv := range s.config.Servers {
if srv.ID == id {
s.config.Servers[i].Name = name
s.config.Servers[i].Host = host
s.config.Servers[i].ID = host
return s.Save()
}
}
return fmt.Errorf("server not found")
}
func (s *ConfigService) Save() error {
data, err := json.MarshalIndent(s.config, "", " ")
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := os.WriteFile(s.path, data, 0o600); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}

View File

@@ -0,0 +1,11 @@
package services
type ServicesStore struct {
VPN *VPNService
Config *ConfigService
}
func NewServicesStore() (*ServicesStore, error) {
cfg, _ := NewConfigService()
return &ServicesStore{VPN: NewVPNService(cfg), Config: cfg}, nil
}

View File

@@ -0,0 +1,66 @@
package services
import (
"bytes"
"context"
"log/slog"
"os/exec"
"strings"
)
type VPNService struct{
Config *ConfigService
}
func NewVPNService(cfg *ConfigService) *VPNService {
return &VPNService{
Config: cfg,
}
}
func (v *VPNService) CheckStatus(ctx context.Context) (bool, error) {
cmd := exec.CommandContext(ctx, "tailscale", "exit-node", "list")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
slog.ErrorContext(ctx, "failed to check VPN status", "error", err)
return false, err
}
output := out.String()
return strings.Contains(output, "selected"), nil
}
func (v *VPNService) TurnOn(ctx context.Context) error {
selectedServer, _ := v.Config.GetSelectedServer()
cmd := exec.CommandContext(ctx, "sudo", "tailscale", "set", "--exit-node="+selectedServer.Host)
if err := cmd.Run(); err != nil {
slog.ErrorContext(ctx, "failed to turn on VPN", "error", err)
return err
}
return nil
}
func (v *VPNService) TurnOff(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "sudo", "tailscale", "set", "--exit-node=")
if err := cmd.Run(); err != nil {
slog.ErrorContext(ctx, "failed to turn off VPN", "error", err)
return err
}
return nil
}
func (v *VPNService) ExitNodeList(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "tailscale", "exit-node", "list")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
slog.ErrorContext(ctx, "failed to get exit node list", "error", err)
return "", err
}
return out.String(), nil
}