You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
591 lines
17 KiB
591 lines
17 KiB
//go:build !(js && wasm) |
|
|
|
package database |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"time" |
|
|
|
"github.com/dgraph-io/badger/v4" |
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/log" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"next.orly.dev/pkg/wireguard" |
|
) |
|
|
|
// Key prefixes for WireGuard data |
|
const ( |
|
wgServerKeyPrefix = "wg:server:key" // Server's WireGuard private key |
|
wgSubnetSeedPrefix = "wg:subnet:seed" // Seed for deterministic subnet generation |
|
wgPeerPrefix = "wg:peer:" // Peer data by Nostr pubkey hex |
|
wgSequenceKey = "wg:seq" // Badger sequence key for subnet allocation |
|
wgRevokedPrefix = "wg:revoked:" // Revoked keypairs by Nostr pubkey hex |
|
wgAccessLogPrefix = "wg:accesslog:" // Access log for obsolete addresses |
|
) |
|
|
|
// WireGuardPeer stores WireGuard peer information in the database. |
|
type WireGuardPeer struct { |
|
NostrPubkey []byte `json:"nostr_pubkey"` // User's Nostr pubkey (32 bytes) |
|
WGPrivateKey []byte `json:"wg_private_key"` // WireGuard private key (32 bytes) |
|
WGPublicKey []byte `json:"wg_public_key"` // WireGuard public key (32 bytes) |
|
Sequence uint32 `json:"sequence"` // Sequence number for subnet derivation |
|
CreatedAt int64 `json:"created_at"` // Unix timestamp |
|
} |
|
|
|
// WireGuardRevokedKey stores a revoked/old WireGuard keypair for audit purposes. |
|
type WireGuardRevokedKey struct { |
|
NostrPubkey []byte `json:"nostr_pubkey"` // User's Nostr pubkey (32 bytes) |
|
WGPublicKey []byte `json:"wg_public_key"` // Revoked WireGuard public key (32 bytes) |
|
Sequence uint32 `json:"sequence"` // Sequence number (subnet) |
|
CreatedAt int64 `json:"created_at"` // When the key was originally created |
|
RevokedAt int64 `json:"revoked_at"` // When the key was revoked |
|
AccessCount int `json:"access_count"` // Number of access attempts since revocation |
|
LastAccessAt int64 `json:"last_access_at"` // Last access attempt timestamp (0 if never) |
|
} |
|
|
|
// WireGuardAccessLog records an access attempt to an obsolete address. |
|
type WireGuardAccessLog struct { |
|
NostrPubkey []byte `json:"nostr_pubkey"` // User's Nostr pubkey |
|
WGPublicKey []byte `json:"wg_public_key"` // The obsolete public key used |
|
Sequence uint32 `json:"sequence"` // Subnet sequence |
|
Timestamp int64 `json:"timestamp"` // When the access occurred |
|
RemoteAddr string `json:"remote_addr"` // Remote IP address |
|
} |
|
|
|
// ServerIP returns the derived server IP for this peer's subnet. |
|
func (p *WireGuardPeer) ServerIP(pool *wireguard.SubnetPool) string { |
|
subnet := pool.SubnetForSequence(p.Sequence) |
|
return subnet.ServerIP.String() |
|
} |
|
|
|
// ClientIP returns the derived client IP for this peer's subnet. |
|
func (p *WireGuardPeer) ClientIP(pool *wireguard.SubnetPool) string { |
|
subnet := pool.SubnetForSequence(p.Sequence) |
|
return subnet.ClientIP.String() |
|
} |
|
|
|
// GetWireGuardServerKey retrieves the WireGuard server private key. |
|
func (d *D) GetWireGuardServerKey() (key []byte, err error) { |
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(wgServerKeyPrefix)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
return err |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
return item.Value(func(val []byte) error { |
|
key = make([]byte, len(val)) |
|
copy(key, val) |
|
return nil |
|
}) |
|
}) |
|
return |
|
} |
|
|
|
// SetWireGuardServerKey stores the WireGuard server private key. |
|
func (d *D) SetWireGuardServerKey(key []byte) error { |
|
if len(key) != 32 { |
|
return fmt.Errorf("invalid key length: %d (expected 32)", len(key)) |
|
} |
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
return txn.Set([]byte(wgServerKeyPrefix), key) |
|
}) |
|
} |
|
|
|
// GetOrCreateWireGuardServerKey retrieves or creates the WireGuard server key. |
|
func (d *D) GetOrCreateWireGuardServerKey() (key []byte, err error) { |
|
// Try to get existing key |
|
if key, err = d.GetWireGuardServerKey(); err == nil && len(key) == 32 { |
|
return key, nil |
|
} |
|
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) { |
|
return nil, err |
|
} |
|
|
|
// Generate new keypair |
|
privateKey, publicKey, err := wireguard.GenerateKeyPair() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err) |
|
} |
|
|
|
// Store the private key |
|
if err = d.SetWireGuardServerKey(privateKey); chk.E(err) { |
|
return nil, err |
|
} |
|
|
|
log.I.F("generated new WireGuard server key (pubkey=%s...)", hex.Enc(publicKey[:8])) |
|
return privateKey, nil |
|
} |
|
|
|
// GetSubnetSeed retrieves the subnet pool seed. |
|
func (d *D) GetSubnetSeed() (seed []byte, err error) { |
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(wgSubnetSeedPrefix)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
return err |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
return item.Value(func(val []byte) error { |
|
seed = make([]byte, len(val)) |
|
copy(seed, val) |
|
return nil |
|
}) |
|
}) |
|
return |
|
} |
|
|
|
// SetSubnetSeed stores the subnet pool seed. |
|
func (d *D) SetSubnetSeed(seed []byte) error { |
|
if len(seed) != 32 { |
|
return fmt.Errorf("invalid seed length: %d (expected 32)", len(seed)) |
|
} |
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
return txn.Set([]byte(wgSubnetSeedPrefix), seed) |
|
}) |
|
} |
|
|
|
// GetOrCreateSubnetPool creates or restores a subnet pool from the database. |
|
func (d *D) GetOrCreateSubnetPool(baseNetwork string) (*wireguard.SubnetPool, error) { |
|
// Try to get existing seed |
|
seed, err := d.GetSubnetSeed() |
|
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) { |
|
return nil, err |
|
} |
|
|
|
var pool *wireguard.SubnetPool |
|
|
|
if len(seed) == 32 { |
|
// Restore pool with existing seed |
|
pool, err = wireguard.NewSubnetPoolWithSeed(baseNetwork, seed) |
|
if err != nil { |
|
return nil, err |
|
} |
|
log.D.F("restored subnet pool with existing seed") |
|
} else { |
|
// Create new pool with random seed |
|
pool, err = wireguard.NewSubnetPool(baseNetwork) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Store the new seed |
|
if err = d.SetSubnetSeed(pool.Seed()); err != nil { |
|
return nil, fmt.Errorf("failed to store subnet seed: %w", err) |
|
} |
|
log.I.F("generated new subnet pool seed") |
|
} |
|
|
|
// Restore existing allocations from database |
|
peers, err := d.GetAllWireGuardPeers() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to load existing peers: %w", err) |
|
} |
|
|
|
for _, peer := range peers { |
|
pool.RestoreAllocation(hex.Enc(peer.NostrPubkey), peer.Sequence) |
|
} |
|
|
|
if len(peers) > 0 { |
|
log.D.F("restored %d subnet allocations", len(peers)) |
|
} |
|
|
|
return pool, nil |
|
} |
|
|
|
// GetWireGuardPeer retrieves a WireGuard peer by Nostr pubkey. |
|
func (d *D) GetWireGuardPeer(nostrPubkey []byte) (peer *WireGuardPeer, err error) { |
|
key := append([]byte(wgPeerPrefix), []byte(hex.Enc(nostrPubkey))...) |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get(key) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
return err |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
return item.Value(func(val []byte) error { |
|
peer = &WireGuardPeer{} |
|
return json.Unmarshal(val, peer) |
|
}) |
|
}) |
|
return |
|
} |
|
|
|
// GetOrCreateWireGuardPeer retrieves or creates a WireGuard peer. |
|
// The pool is used for subnet derivation from the sequence number. |
|
func (d *D) GetOrCreateWireGuardPeer(nostrPubkey []byte, pool *wireguard.SubnetPool) (peer *WireGuardPeer, err error) { |
|
// Try to get existing peer |
|
if peer, err = d.GetWireGuardPeer(nostrPubkey); err == nil { |
|
return peer, nil |
|
} |
|
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) { |
|
return nil, err |
|
} |
|
|
|
// Generate new WireGuard keypair |
|
privateKey, publicKey, err := wireguard.GenerateKeyPair() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err) |
|
} |
|
|
|
// Get next sequence number from Badger's sequence |
|
seq64, err := d.GetNextWGSequence() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to allocate sequence: %w", err) |
|
} |
|
seq := uint32(seq64) |
|
|
|
// Register allocation with pool for in-memory tracking |
|
pubkeyHex := hex.Enc(nostrPubkey) |
|
pool.RestoreAllocation(pubkeyHex, seq) |
|
|
|
peer = &WireGuardPeer{ |
|
NostrPubkey: nostrPubkey, |
|
WGPrivateKey: privateKey, |
|
WGPublicKey: publicKey, |
|
Sequence: seq, |
|
CreatedAt: time.Now().Unix(), |
|
} |
|
|
|
// Store peer data |
|
if err = d.setWireGuardPeer(peer); err != nil { |
|
return nil, err |
|
} |
|
|
|
subnet := pool.SubnetForSequence(seq) |
|
log.I.F("created WireGuard peer: nostr=%s... -> subnet %s/%s (seq=%d)", |
|
hex.Enc(nostrPubkey[:8]), subnet.ServerIP, subnet.ClientIP, seq) |
|
|
|
return peer, nil |
|
} |
|
|
|
// RegenerateWireGuardPeer generates a new keypair for an existing peer. |
|
// The sequence number (and thus subnet) is preserved. |
|
// The old keypair is archived for audit purposes. |
|
func (d *D) RegenerateWireGuardPeer(nostrPubkey []byte, pool *wireguard.SubnetPool) (peer *WireGuardPeer, err error) { |
|
// Get existing peer to preserve sequence |
|
existing, err := d.GetWireGuardPeer(nostrPubkey) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Archive the old keypair for audit purposes |
|
if err = d.ArchiveRevokedKey(existing); err != nil { |
|
log.W.F("failed to archive revoked key: %v", err) |
|
// Continue anyway - this is audit logging, not critical |
|
} |
|
|
|
// Generate new WireGuard keypair |
|
privateKey, publicKey, err := wireguard.GenerateKeyPair() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err) |
|
} |
|
|
|
peer = &WireGuardPeer{ |
|
NostrPubkey: nostrPubkey, |
|
WGPrivateKey: privateKey, |
|
WGPublicKey: publicKey, |
|
Sequence: existing.Sequence, // Keep same sequence (same subnet) |
|
CreatedAt: time.Now().Unix(), |
|
} |
|
|
|
// Store updated peer data |
|
if err = d.setWireGuardPeer(peer); err != nil { |
|
return nil, err |
|
} |
|
|
|
subnet := pool.SubnetForSequence(peer.Sequence) |
|
log.I.F("regenerated WireGuard peer: nostr=%s... -> subnet %s/%s (old key archived)", |
|
hex.Enc(nostrPubkey[:8]), subnet.ServerIP, subnet.ClientIP) |
|
|
|
return peer, nil |
|
} |
|
|
|
// DeleteWireGuardPeer removes a WireGuard peer from the database. |
|
// Note: The sequence number is not recycled to prevent subnet reuse. |
|
func (d *D) DeleteWireGuardPeer(nostrPubkey []byte) error { |
|
peerKey := append([]byte(wgPeerPrefix), []byte(hex.Enc(nostrPubkey))...) |
|
|
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
if err := txn.Delete(peerKey); err != nil && !errors.Is(err, badger.ErrKeyNotFound) { |
|
return err |
|
} |
|
return nil |
|
}) |
|
} |
|
|
|
// GetAllWireGuardPeers returns all WireGuard peers. |
|
func (d *D) GetAllWireGuardPeers() (peers []*WireGuardPeer, err error) { |
|
prefix := []byte(wgPeerPrefix) |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
err := item.Value(func(val []byte) error { |
|
peer := &WireGuardPeer{} |
|
if err := json.Unmarshal(val, peer); err != nil { |
|
return err |
|
} |
|
peers = append(peers, peer) |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
return |
|
} |
|
|
|
// setWireGuardPeer stores a WireGuard peer in the database. |
|
func (d *D) setWireGuardPeer(peer *WireGuardPeer) error { |
|
data, err := json.Marshal(peer) |
|
if err != nil { |
|
return fmt.Errorf("failed to marshal peer: %w", err) |
|
} |
|
|
|
peerKey := append([]byte(wgPeerPrefix), []byte(hex.Enc(peer.NostrPubkey))...) |
|
|
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
return txn.Set(peerKey, data) |
|
}) |
|
} |
|
|
|
// GetNextWGSequence retrieves and increments the sequence counter using Badger's Sequence. |
|
func (d *D) GetNextWGSequence() (seq uint64, err error) { |
|
// Get a sequence with bandwidth 1 (allocate 1 number at a time) |
|
badgerSeq, err := d.DB.GetSequence([]byte(wgSequenceKey), 1) |
|
if err != nil { |
|
return 0, fmt.Errorf("failed to get sequence: %w", err) |
|
} |
|
defer badgerSeq.Release() |
|
|
|
seq, err = badgerSeq.Next() |
|
if err != nil { |
|
return 0, fmt.Errorf("failed to get next sequence number: %w", err) |
|
} |
|
return seq, nil |
|
} |
|
|
|
// ArchiveRevokedKey stores a revoked keypair for audit purposes. |
|
func (d *D) ArchiveRevokedKey(peer *WireGuardPeer) error { |
|
revoked := &WireGuardRevokedKey{ |
|
NostrPubkey: peer.NostrPubkey, |
|
WGPublicKey: peer.WGPublicKey, |
|
Sequence: peer.Sequence, |
|
CreatedAt: peer.CreatedAt, |
|
RevokedAt: time.Now().Unix(), |
|
AccessCount: 0, |
|
LastAccessAt: 0, |
|
} |
|
|
|
data, err := json.Marshal(revoked) |
|
if err != nil { |
|
return fmt.Errorf("failed to marshal revoked key: %w", err) |
|
} |
|
|
|
// Key: wg:revoked:<pubkey-hex>:<revoked-timestamp> |
|
keyStr := fmt.Sprintf("%s%s:%d", wgRevokedPrefix, hex.Enc(peer.NostrPubkey), revoked.RevokedAt) |
|
|
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
return txn.Set([]byte(keyStr), data) |
|
}) |
|
} |
|
|
|
// GetRevokedKeys returns all revoked keys for a user. |
|
func (d *D) GetRevokedKeys(nostrPubkey []byte) (keys []*WireGuardRevokedKey, err error) { |
|
prefix := []byte(wgRevokedPrefix + hex.Enc(nostrPubkey) + ":") |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
err := item.Value(func(val []byte) error { |
|
key := &WireGuardRevokedKey{} |
|
if err := json.Unmarshal(val, key); err != nil { |
|
return err |
|
} |
|
keys = append(keys, key) |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
return |
|
} |
|
|
|
// GetAllRevokedKeys returns all revoked keys across all users (admin view). |
|
func (d *D) GetAllRevokedKeys() (keys []*WireGuardRevokedKey, err error) { |
|
prefix := []byte(wgRevokedPrefix) |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
err := item.Value(func(val []byte) error { |
|
key := &WireGuardRevokedKey{} |
|
if err := json.Unmarshal(val, key); err != nil { |
|
return err |
|
} |
|
keys = append(keys, key) |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
return |
|
} |
|
|
|
// LogObsoleteAccess records an access attempt to an obsolete WireGuard address. |
|
func (d *D) LogObsoleteAccess(nostrPubkey, wgPubkey []byte, sequence uint32, remoteAddr string) error { |
|
now := time.Now().Unix() |
|
|
|
logEntry := &WireGuardAccessLog{ |
|
NostrPubkey: nostrPubkey, |
|
WGPublicKey: wgPubkey, |
|
Sequence: sequence, |
|
Timestamp: now, |
|
RemoteAddr: remoteAddr, |
|
} |
|
|
|
data, err := json.Marshal(logEntry) |
|
if err != nil { |
|
return fmt.Errorf("failed to marshal access log: %w", err) |
|
} |
|
|
|
// Key: wg:accesslog:<pubkey-hex>:<timestamp> |
|
keyStr := fmt.Sprintf("%s%s:%d", wgAccessLogPrefix, hex.Enc(nostrPubkey), now) |
|
|
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
return txn.Set([]byte(keyStr), data) |
|
}) |
|
} |
|
|
|
// GetAccessLogs returns access logs for a user. |
|
func (d *D) GetAccessLogs(nostrPubkey []byte) (logs []*WireGuardAccessLog, err error) { |
|
prefix := []byte(wgAccessLogPrefix + hex.Enc(nostrPubkey) + ":") |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
err := item.Value(func(val []byte) error { |
|
logEntry := &WireGuardAccessLog{} |
|
if err := json.Unmarshal(val, logEntry); err != nil { |
|
return err |
|
} |
|
logs = append(logs, logEntry) |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
return |
|
} |
|
|
|
// GetAllAccessLogs returns all access logs (admin view). |
|
func (d *D) GetAllAccessLogs() (logs []*WireGuardAccessLog, err error) { |
|
prefix := []byte(wgAccessLogPrefix) |
|
|
|
err = d.DB.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
err := item.Value(func(val []byte) error { |
|
logEntry := &WireGuardAccessLog{} |
|
if err := json.Unmarshal(val, logEntry); err != nil { |
|
return err |
|
} |
|
logs = append(logs, logEntry) |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
return |
|
} |
|
|
|
// IncrementRevokedKeyAccess updates the access count for a revoked key. |
|
func (d *D) IncrementRevokedKeyAccess(nostrPubkey, wgPubkey []byte) error { |
|
// Find and update the matching revoked key |
|
prefix := []byte(wgRevokedPrefix + hex.Enc(nostrPubkey) + ":") |
|
wgPubkeyHex := hex.Enc(wgPubkey) |
|
now := time.Now().Unix() |
|
|
|
return d.DB.Update(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = prefix |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { |
|
item := it.Item() |
|
key := item.KeyCopy(nil) |
|
|
|
err := item.Value(func(val []byte) error { |
|
revoked := &WireGuardRevokedKey{} |
|
if err := json.Unmarshal(val, revoked); err != nil { |
|
return err |
|
} |
|
|
|
// Check if this is the matching revoked key |
|
if hex.Enc(revoked.WGPublicKey) == wgPubkeyHex { |
|
revoked.AccessCount++ |
|
revoked.LastAccessAt = now |
|
|
|
data, err := json.Marshal(revoked) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
} |
|
return nil |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
}
|
|
|