fix: multi issues - TUN read loop, SDWAN routing for TenantID=0, WS keepalive 10s
This commit is contained in:
343
internal/store/store.go
Normal file
343
internal/store/store.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID int64
|
||||
Name string
|
||||
Status int
|
||||
Subnet string
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Hash string
|
||||
Scope string
|
||||
Expires *time.Time
|
||||
Status int
|
||||
}
|
||||
|
||||
type NodeCredential struct {
|
||||
NodeID int64
|
||||
NodeName string
|
||||
Secret string
|
||||
VirtualIP string
|
||||
TenantID int64
|
||||
}
|
||||
|
||||
type EnrollToken struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Hash string
|
||||
ExpiresAt int64
|
||||
UsedAt *int64
|
||||
MaxAttempt int
|
||||
Attempts int
|
||||
Status int
|
||||
}
|
||||
|
||||
func Open(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := db.Exec(`PRAGMA journal_mode=WAL;`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := db.Exec(`PRAGMA foreign_keys=ON;`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Store{DB: db}
|
||||
if err := s.migrate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ensureSubnetPool(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS tenants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
subnet TEXT NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
email TEXT,
|
||||
password_hash TEXT,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
key_hash TEXT NOT NULL UNIQUE,
|
||||
scope TEXT,
|
||||
expires_at INTEGER,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
node_name TEXT NOT NULL,
|
||||
node_pubkey TEXT,
|
||||
node_secret_hash TEXT,
|
||||
virtual_ip TEXT,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
last_seen INTEGER,
|
||||
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS enroll_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
used_at INTEGER,
|
||||
max_attempt INTEGER NOT NULL DEFAULT 5,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS peering_policies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
src_tenant_id INTEGER NOT NULL,
|
||||
dst_tenant_id INTEGER NOT NULL,
|
||||
rules TEXT,
|
||||
expires_at INTEGER,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(src_tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY(dst_tenant_id) REFERENCES tenants(id)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_type TEXT,
|
||||
actor_id TEXT,
|
||||
action TEXT,
|
||||
target_type TEXT,
|
||||
target_id TEXT,
|
||||
detail TEXT,
|
||||
ip TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS subnet_pool (
|
||||
subnet TEXT PRIMARY KEY,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
reserved INTEGER NOT NULL DEFAULT 0,
|
||||
tenant_id INTEGER,
|
||||
updated_at INTEGER NOT NULL
|
||||
);`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if _, err := s.DB.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ensureSubnetPool() error {
|
||||
// pool: 10.10.1.0/24 .. 10.10.254.0/24
|
||||
// reserve: 10.10.0.0/24 and 10.10.255.0/24
|
||||
rows, err := s.DB.Query(`SELECT COUNT(1) FROM subnet_pool;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var count int
|
||||
if rows.Next() {
|
||||
_ = rows.Scan(&count)
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
insert := `INSERT INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`
|
||||
// reserved
|
||||
_, _ = s.DB.Exec(insert, "10.10.0.0/24", 0, 1, nil, now)
|
||||
_, _ = s.DB.Exec(insert, "10.10.255.0/24", 0, 1, nil, now)
|
||||
for i := 1; i <= 254; i++ {
|
||||
sn := fmt.Sprintf("10.10.%d.0/24", i)
|
||||
_, _ = s.DB.Exec(insert, sn, 0, 0, nil, now)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) AllocateSubnet() (string, error) {
|
||||
// find first available subnet
|
||||
row := s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 ORDER BY subnet LIMIT 1`)
|
||||
var subnet string
|
||||
if err := row.Scan(&subnet); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if subnet == "" {
|
||||
return "", errors.New("no subnet available")
|
||||
}
|
||||
return subnet, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateTenant(name string) (*Tenant, error) {
|
||||
sn, err := s.AllocateSubnet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
res, err := s.DB.Exec(`INSERT INTO tenants(name,status,subnet,created_at) VALUES(?,?,?,?)`, name, 1, sn, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
_, _ = s.DB.Exec(`UPDATE subnet_pool SET status=1, tenant_id=?, updated_at=? WHERE subnet=?`, id, now, sn)
|
||||
return &Tenant{ID: id, Name: name, Status: 1, Subnet: sn}, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateNodeCredential(tenantID int64, nodeName string) (*NodeCredential, error) {
|
||||
secret := randToken()
|
||||
h := hashTokenString(secret)
|
||||
res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_name,node_secret_hash,status) VALUES(?,?,?,1)`, tenantID, nodeName, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return &NodeCredential{NodeID: id, NodeName: nodeName, Secret: secret, TenantID: tenantID}, nil
|
||||
}
|
||||
|
||||
func (s *Store) VerifyNodeSecret(nodeName, secret string) (*Tenant, error) {
|
||||
h := hashTokenString(secret)
|
||||
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM nodes n JOIN tenants t ON n.tenant_id=t.id WHERE n.node_name=? AND n.node_secret_hash=? AND n.status=1`, nodeName, h)
|
||||
var t Tenant
|
||||
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTenantByToken(token uint64) (*Tenant, error) {
|
||||
h := hashToken(token)
|
||||
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1`, h)
|
||||
var t Tenant
|
||||
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateAPIKey(tenantID int64, scope string, ttl time.Duration) (string, error) {
|
||||
token := randToken()
|
||||
h := hashTokenString(token)
|
||||
now := time.Now().Unix()
|
||||
if ttl > 0 {
|
||||
e := time.Now().Add(ttl)
|
||||
_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, e.Unix(), now)
|
||||
return token, err
|
||||
}
|
||||
_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, nil, now)
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateEnrollToken(tenantID int64, ttl time.Duration, maxAttempt int) (string, error) {
|
||||
code := randToken()
|
||||
h := hashTokenString(code)
|
||||
exp := time.Now().Add(ttl).Unix()
|
||||
now := time.Now().Unix()
|
||||
_, err := s.DB.Exec(`INSERT INTO enroll_tokens(tenant_id,token_hash,expires_at,max_attempt,attempts,status,created_at) VALUES(?,?,?,?,0,1,?)`, tenantID, h, exp, maxAttempt, now)
|
||||
return code, err
|
||||
}
|
||||
|
||||
func (s *Store) ConsumeEnrollToken(code string) (*EnrollToken, error) {
|
||||
h := hashTokenString(code)
|
||||
now := time.Now().Unix()
|
||||
row := s.DB.QueryRow(`SELECT id,tenant_id,expires_at,used_at,max_attempt,attempts,status FROM enroll_tokens WHERE token_hash=?`, h)
|
||||
var et EnrollToken
|
||||
var used sql.NullInt64
|
||||
if err := row.Scan(&et.ID, &et.TenantID, &et.ExpiresAt, &used, &et.MaxAttempt, &et.Attempts, &et.Status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if used.Valid {
|
||||
return nil, errors.New("token already used")
|
||||
}
|
||||
if et.Status != 1 {
|
||||
return nil, errors.New("token disabled")
|
||||
}
|
||||
if et.Attempts >= et.MaxAttempt {
|
||||
return nil, errors.New("token attempts exceeded")
|
||||
}
|
||||
if now > et.ExpiresAt {
|
||||
return nil, errors.New("token expired")
|
||||
}
|
||||
// mark used
|
||||
_, err := s.DB.Exec(`UPDATE enroll_tokens SET used_at=?, attempts=attempts+1 WHERE id=?`, now, et.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
et.UsedAt = &now
|
||||
return &et, nil
|
||||
}
|
||||
|
||||
func (s *Store) IncEnrollAttempt(code string) {
|
||||
h := hashTokenString(code)
|
||||
_, _ = s.DB.Exec(`UPDATE enroll_tokens SET attempts=attempts+1 WHERE token_hash=?`, h)
|
||||
}
|
||||
|
||||
func hashToken(token uint64) string {
|
||||
b := make([]byte, 8)
|
||||
for i := uint(0); i < 8; i++ {
|
||||
b[7-i] = byte(token >> (i * 8))
|
||||
}
|
||||
return hashTokenBytes(b)
|
||||
}
|
||||
|
||||
func hashTokenString(token string) string {
|
||||
return hashTokenBytes([]byte(token))
|
||||
}
|
||||
|
||||
func (s *Store) VerifyAPIKey(token string) (*Tenant, error) {
|
||||
h := hashTokenString(token)
|
||||
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1`, h)
|
||||
var t Tenant
|
||||
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func hashTokenBytes(b []byte) string {
|
||||
h := sha256.Sum256(b)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func randToken() string {
|
||||
b := make([]byte, 24)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// helper to avoid unused import (net)
|
||||
var _ = net.IPv4len
|
||||
Reference in New Issue
Block a user