diff --git a/model/client.go b/model/client.go index d835124..a15b33a 100644 --- a/model/client.go +++ b/model/client.go @@ -7,6 +7,7 @@ import ( // Client model type Client struct { ID string `json:"id"` + ServerID string `json:"server_id,omitempty"` PrivateKey string `json:"private_key"` PublicKey string `json:"public_key"` PresharedKey string `json:"preshared_key"` diff --git a/model/server.go b/model/server.go index 0aa804f..37e3c3c 100644 --- a/model/server.go +++ b/model/server.go @@ -6,6 +6,8 @@ import ( // Server model type Server struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` KeyPair *ServerKeypair Interface *ServerInterface } @@ -19,6 +21,7 @@ type ServerKeypair struct { // ServerInterface model type ServerInterface struct { + Name string `json:"name,omitempty"` // OS interface name, e.g. wg0 Addresses []string `json:"addresses"` ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int UpdatedAt time.Time `json:"updated_at"` diff --git a/model/setting.go b/model/setting.go index c9e152c..408f8a3 100644 --- a/model/setting.go +++ b/model/setting.go @@ -15,3 +15,14 @@ type GlobalSetting struct { ConfigFilePath string `json:"config_file_path"` UpdatedAt time.Time `json:"updated_at"` } + +// ServerSetting model holds the per-server-interface subset of settings +// that used to live in the single global GlobalSetting. Introduced for the +// multi-server extension; not yet wired into store/handler layers. +type ServerSetting struct { + EndpointAddress string `json:"endpoint_address"` + FirewallMark string `json:"firewall_mark"` + Table string `json:"table"` + ConfigFilePath string `json:"config_file_path"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/store/jsondb/jsondb.go b/store/jsondb/jsondb.go index 1cd0a43..6468d8a 100644 --- a/store/jsondb/jsondb.go +++ b/store/jsondb/jsondb.go @@ -4,9 +4,11 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log" "os" "path" "strconv" + "strings" "time" "github.com/sdomino/scribble" @@ -17,6 +19,10 @@ import ( "github.com/ngoduykhanh/wireguard-ui/util" ) +// legacyDefaultServerID is the synthetic ID assigned to a pre-existing +// single-server installation when it is migrated to the multi-server layout. +const legacyDefaultServerID = "wg0" + type JsonDB struct { conn *scribble.Driver dbPath string @@ -161,6 +167,11 @@ func (o *JsonDB) Init() error { } } + // migrate legacy single-server layout to the new multi-server layout + if err := o.migrateLegacyServer(); err != nil { + return err + } + // init cache for _, i := range results { user := model.User{} @@ -186,6 +197,122 @@ func (o *JsonDB) Init() error { return nil } +// migrateLegacyServer detects a pre-multi-server DB layout (single +// "server" collection, no "servers" registry yet) and migrates it into +// the new per-server layout ("servers/.json", "server_settings/.json", +// "server_hashes/.json"), backfilling ServerID on every existing client. +// It is idempotent: it only runs once, guarded by the absence of the +// "servers" directory, and never deletes or modifies the legacy "server" +// collection - existing store methods keep reading/writing it directly +// until they are switched over to the new layout in a later step. +func (o *JsonDB) migrateLegacyServer() error { + legacyServerPath := path.Join(o.dbPath, "server") + legacyInterfacePath := path.Join(legacyServerPath, "interfaces.json") + newServersPath := path.Join(o.dbPath, "servers") + + if _, err := os.Stat(legacyInterfacePath); os.IsNotExist(err) { + // nothing to migrate (fresh install) + return nil + } + if _, err := os.Stat(newServersPath); err == nil { + // already migrated + return nil + } + + var legacyInterface model.ServerInterface + if err := o.conn.Read("server", "interfaces", &legacyInterface); err != nil { + return fmt.Errorf("migration: cannot read legacy server interface: %v", err) + } + var legacyKeyPair model.ServerKeypair + if err := o.conn.Read("server", "keypair", &legacyKeyPair); err != nil { + return fmt.Errorf("migration: cannot read legacy server keypair: %v", err) + } + var legacyGlobalSettings model.GlobalSetting + if err := o.conn.Read("server", "global_settings", &legacyGlobalSettings); err != nil { + return fmt.Errorf("migration: cannot read legacy global settings: %v", err) + } + var legacyHashes model.ClientServerHashes + hasHashes := true + if err := o.conn.Read("server", "hashes", &legacyHashes); err != nil { + hasHashes = false + } + + serverID := legacyDefaultServerID + ifaceName := legacyDefaultServerID + if legacyGlobalSettings.ConfigFilePath != "" { + base := path.Base(legacyGlobalSettings.ConfigFilePath) + if strings.HasSuffix(base, ".conf") { + ifaceName = strings.TrimSuffix(base, ".conf") + } + } + + legacyInterface.Name = ifaceName + + server := model.Server{ + ID: serverID, + Name: "Default Server", + KeyPair: &legacyKeyPair, + Interface: &legacyInterface, + } + if err := o.conn.Write("servers", serverID, server); err != nil { + return fmt.Errorf("migration: cannot write servers/%s.json: %v", serverID, err) + } + if err := util.ManagePerms(path.Join(newServersPath, serverID+".json")); err != nil { + return err + } + + serverSetting := model.ServerSetting{ + EndpointAddress: legacyGlobalSettings.EndpointAddress, + FirewallMark: legacyGlobalSettings.FirewallMark, + Table: legacyGlobalSettings.Table, + ConfigFilePath: legacyGlobalSettings.ConfigFilePath, + UpdatedAt: time.Now().UTC(), + } + if err := o.conn.Write("server_settings", serverID, serverSetting); err != nil { + return fmt.Errorf("migration: cannot write server_settings/%s.json: %v", serverID, err) + } + if err := util.ManagePerms(path.Join(o.dbPath, "server_settings", serverID+".json")); err != nil { + return err + } + + if hasHashes { + if err := o.conn.Write("server_hashes", serverID, legacyHashes); err != nil { + return fmt.Errorf("migration: cannot write server_hashes/%s.json: %v", serverID, err) + } + if err := util.ManagePerms(path.Join(o.dbPath, "server_hashes", serverID+".json")); err != nil { + return err + } + } + + // backfill ServerID on every existing client + clientRecords, err := o.conn.ReadAll("clients") + if err != nil && err != scribble.ErrMissingCollection { + return fmt.Errorf("migration: cannot read clients: %v", err) + } + for _, rec := range clientRecords { + var client model.Client + if err := json.Unmarshal(rec, &client); err != nil { + return fmt.Errorf("migration: cannot decode client json: %v", err) + } + if client.ServerID != "" { + continue + } + client.ServerID = serverID + if err := o.conn.Write("clients", client.ID, client); err != nil { + return fmt.Errorf("migration: cannot backfill server_id on client %s: %v", client.ID, err) + } + } + + // NOTE: the legacy "server" directory is intentionally left in place + // (not renamed/removed) at this stage. Existing store methods (GetServer, + // GetGlobalSettings, SaveServerInterface, ...) still read/write it + // directly until they are switched over to the new per-server layout; + // removing it now would break the running app. The rename-to-backup + // step happens once those methods are migrated. + log.Printf("Migrated legacy single-server DB to multi-server layout (server id: %s); legacy db/server/ kept in place for now", serverID) + return nil +} + // GetUsers func to get all users from the database func (o *JsonDB) GetUsers() ([]model.User, error) { var users []model.User