This commit is contained in:
jdl
2024-12-08 09:45:29 +01:00
parent 55a9bf9dc3
commit 03ff1aac80
60 changed files with 3165 additions and 2 deletions

260
hub/api/api.go Normal file
View File

@@ -0,0 +1,260 @@
package api
import (
"crypto/rand"
"database/sql"
"embed"
"errors"
"log"
"sync"
"time"
"vppn/hub/api/db"
"vppn/m"
"git.crumpington.com/lib/go/idgen"
"git.crumpington.com/lib/go/sqliteutil"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/sign"
)
//go:embed migrations
var migrations embed.FS
type API struct {
db *sql.DB
lock sync.Mutex
peerIntents map[string]PeerCreateArgs
}
func New(dbPath string) (*API, error) {
sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal=WAL")
if err != nil {
return nil, err
}
if err := sqliteutil.Migrate(sqlDB, migrations); err != nil {
return nil, err
}
a := &API{
db: sqlDB,
peerIntents: map[string]PeerCreateArgs{},
}
return a, a.ensurePassword()
}
func (a *API) ensurePassword() error {
_, err := db.Config_Get(a.db, 1)
if err == nil {
return nil
}
if !errors.Is(err, sql.ErrNoRows) {
return err
}
pwd := idgen.NewToken()
log.Printf("Setting password: %s", pwd)
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return err
}
conf := &Config{
ConfigID: 1,
VPNNetwork: []byte{10, 1, 1, 0},
Password: hashed,
}
return db.Config_Insert(a.db, conf)
}
func (a *API) Config_Get() *Config {
conf, err := db.Config_Get(a.db, 1)
if err != nil {
panic(err)
}
return conf
}
func (a *API) Config_Update(conf *Config) error {
return db.Config_Update(a.db, conf)
}
func (a *API) Config_UpdatePassword(pwdHash []byte) error {
return db.Config_UpdatePassword(a.db, pwdHash)
}
func (a *API) Session_Delete(sessionID string) error {
return db.Session_Delete(a.db, sessionID)
}
func (a *API) Session_Get(sessionID string) (*Session, error) {
if sessionID == "" {
return a.session_CreatePub()
}
session, err := db.Session_Get(a.db, sessionID)
if err != nil {
return a.session_CreatePub()
}
if timeSince(session.LastSeenAt) > 86400*21 {
return a.session_CreatePub()
}
if timeSince(session.LastSeenAt) > 86400*7 {
session.LastSeenAt = time.Now().Unix()
if err := db.Session_UpdateLastSeenAt(a.db, session.SessionID); err != nil {
log.Printf("Failed to update session: %v", err)
}
}
return session, nil
}
func (a *API) session_CreatePub() (*Session, error) {
s := &Session{
SessionID: idgen.NewToken(),
CSRF: idgen.NewToken(),
SignedIn: false,
CreatedAt: time.Now().Unix(),
LastSeenAt: time.Now().Unix(),
}
err := db.Session_Insert(a.db, s)
return s, err
}
func (a *API) Session_DeleteBefore(timestamp int64) error {
return db.Session_DeleteBefore(a.db, timestamp)
}
func (a *API) Session_SignIn(s *Session, pwd string) error {
conf := a.Config_Get()
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized
}
return db.Session_SetSignedIn(a.db, s.SessionID)
}
type PeerCreateArgs struct {
Name string
IP []byte
Port uint16
Mediator bool
}
// Create the intention to add a peer. The returned code is used to complete
// the peer creation. The code is valid for 5 minutes.
func (a *API) Peer_CreateIntent(args PeerCreateArgs) string {
a.lock.Lock()
defer a.lock.Unlock()
code := idgen.NewToken()
a.peerIntents[code] = args
go func() {
time.Sleep(5 * time.Minute)
a.lock.Lock()
defer a.lock.Unlock()
delete(a.peerIntents, code)
}()
return code
}
func (a *API) Peer_Create(creationCode string) (*m.PeerConfig, error) {
a.lock.Lock()
defer a.lock.Unlock()
args, ok := a.peerIntents[creationCode]
if !ok {
return nil, ErrNotAuthorized
}
delete(a.peerIntents, creationCode)
encPubKey, encPrivKey, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
// Get peer IP.
peerIP := byte(0)
for i := byte(1); i < 255; i++ {
exists, err := db.Peer_Exists(a.db, i)
if err != nil {
return nil, err
}
if !exists {
peerIP = i
break
}
}
if peerIP == 0 {
return nil, ErrNoIPAvailable
}
peer := &Peer{
PeerIP: peerIP,
APIKey: idgen.NewToken(),
Name: args.Name,
IP: args.IP,
Port: args.Port,
Mediator: args.Mediator,
EncPubKey: encPubKey[:],
SignPubKey: signPubKey[:],
}
if err := db.Peer_Insert(a.db, peer); err != nil {
return nil, err
}
conf := a.Config_Get()
return &m.PeerConfig{
PeerIP: peer.PeerIP,
HubAddress: conf.HubAddress,
APIKey: peer.APIKey,
Network: conf.VPNNetwork,
IP: peer.IP,
Port: peer.Port,
Mediator: peer.Mediator,
EncPubKey: encPubKey[:],
EncPrivKey: encPrivKey[:],
SignPubKey: signPubKey[:],
SignPrivKey: signPrivKey[:],
}, nil
}
func (a *API) Peer_Update(p *Peer) error {
return db.Peer_Update(a.db, p)
}
func (a *API) Peer_Delete(ip byte) error {
return db.Peer_Delete(a.db, ip)
}
func (a *API) Peer_List() ([]*Peer, error) {
return db.Peer_ListAll(a.db)
}
func (a *API) Peer_Get(ip byte) (*Peer, error) {
return db.Peer_Get(a.db, ip)
}
func (a *API) Peer_GetByAPIKey(key string) (*Peer, error) {
return db.Peer_GetByAPIKey(a.db, key)
}

3
hub/api/db/generate.go Normal file
View File

@@ -0,0 +1,3 @@
package db
//go:generate sqlgen sqlite tables.defs generated.go

0
hub/api/db/generated Normal file
View File

480
hub/api/db/generated.go Normal file
View File

@@ -0,0 +1,480 @@
package db
import (
"database/sql"
"iter"
)
type TX interface {
Exec(query string, args ...any) (sql.Result, error)
Query(query string, args ...any) (*sql.Rows, error)
QueryRow(query string, args ...any) *sql.Row
}
// ----------------------------------------------------------------------------
// Table: config
// ----------------------------------------------------------------------------
type Config struct {
ConfigID int64
HubAddress string
VPNNetwork []byte
Password []byte
}
const Config_SelectQuery = "SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config"
func Config_Insert(
tx TX,
row *Config,
) (err error) {
Config_Sanitize(row)
if err = Config_Validate(row); err != nil {
return err
}
_, err = tx.Exec("INSERT INTO config(ConfigID,HubAddress,VPNNetwork,Password) VALUES(?,?,?,?)", row.ConfigID, row.HubAddress, row.VPNNetwork, row.Password)
return err
}
func Config_Update(
tx TX,
row *Config,
) (err error) {
Config_Sanitize(row)
if err = Config_Validate(row); err != nil {
return err
}
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.ConfigID)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows updated")
}
}
func Config_UpdateFull(
tx TX,
row *Config,
) (err error) {
Config_Sanitize(row)
if err = Config_Validate(row); err != nil {
return err
}
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=?,Password=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.Password, row.ConfigID)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows updated")
}
}
func Config_Delete(
tx TX,
ConfigID int64,
) (err error) {
result, err := tx.Exec("DELETE FROM config WHERE ConfigID=?", ConfigID)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows deleted")
}
}
func Config_Get(
tx TX,
ConfigID int64,
) (
row *Config,
err error,
) {
row = &Config{}
r := tx.QueryRow("SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config WHERE ConfigID=?", ConfigID)
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
return
}
func Config_GetWhere(
tx TX,
query string,
args ...any,
) (
row *Config,
err error,
) {
row = &Config{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
return
}
func Config_Iterate(
tx TX,
query string,
args ...any,
) iter.Seq2[*Config, error] {
rows, err := tx.Query(query, args...)
if err != nil {
return func(yield func(*Config, error) bool) {
yield(nil, err)
}
}
return func(yield func(*Config, error) bool) {
defer rows.Close()
for rows.Next() {
row := &Config{}
err := rows.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
if !yield(row, err) {
return
}
}
}
}
func Config_List(
tx TX,
query string,
args ...any,
) (
l []*Config,
err error,
) {
for row, err := range Config_Iterate(tx, query, args...) {
if err != nil {
return nil, err
}
l = append(l, row)
}
return l, nil
}
// ----------------------------------------------------------------------------
// Table: sessions
// ----------------------------------------------------------------------------
type Session struct {
SessionID string
CSRF string
SignedIn bool
CreatedAt int64
LastSeenAt int64
}
const Session_SelectQuery = "SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions"
func Session_Insert(
tx TX,
row *Session,
) (err error) {
Session_Sanitize(row)
if err = Session_Validate(row); err != nil {
return err
}
_, err = tx.Exec("INSERT INTO sessions(SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt) VALUES(?,?,?,?,?)", row.SessionID, row.CSRF, row.SignedIn, row.CreatedAt, row.LastSeenAt)
return err
}
func Session_Delete(
tx TX,
SessionID string,
) (err error) {
result, err := tx.Exec("DELETE FROM sessions WHERE SessionID=?", SessionID)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows deleted")
}
}
func Session_Get(
tx TX,
SessionID string,
) (
row *Session,
err error,
) {
row = &Session{}
r := tx.QueryRow("SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions WHERE SessionID=?", SessionID)
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
return
}
func Session_GetWhere(
tx TX,
query string,
args ...any,
) (
row *Session,
err error,
) {
row = &Session{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
return
}
func Session_Iterate(
tx TX,
query string,
args ...any,
) iter.Seq2[*Session, error] {
rows, err := tx.Query(query, args...)
if err != nil {
return func(yield func(*Session, error) bool) {
yield(nil, err)
}
}
return func(yield func(*Session, error) bool) {
defer rows.Close()
for rows.Next() {
row := &Session{}
err := rows.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
if !yield(row, err) {
return
}
}
}
}
func Session_List(
tx TX,
query string,
args ...any,
) (
l []*Session,
err error,
) {
for row, err := range Session_Iterate(tx, query, args...) {
if err != nil {
return nil, err
}
l = append(l, row)
}
return l, nil
}
// ----------------------------------------------------------------------------
// Table: peers
// ----------------------------------------------------------------------------
type Peer struct {
PeerIP byte
APIKey string
Name string
IP []byte
Port uint16
Mediator bool
EncPubKey []byte
SignPubKey []byte
}
const Peer_SelectQuery = "SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers"
func Peer_Insert(
tx TX,
row *Peer,
) (err error) {
Peer_Sanitize(row)
if err = Peer_Validate(row); err != nil {
return err
}
_, err = tx.Exec("INSERT INTO peers(PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?)", row.PeerIP, row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey)
return err
}
func Peer_Update(
tx TX,
row *Peer,
) (err error) {
Peer_Sanitize(row)
if err = Peer_Validate(row); err != nil {
return err
}
result, err := tx.Exec("UPDATE peers SET Name=?,IP=?,Port=?,Mediator=? WHERE PeerIP=?", row.Name, row.IP, row.Port, row.Mediator, row.PeerIP)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows updated")
}
}
func Peer_UpdateFull(
tx TX,
row *Peer,
) (err error) {
Peer_Sanitize(row)
if err = Peer_Validate(row); err != nil {
return err
}
result, err := tx.Exec("UPDATE peers SET APIKey=?,Name=?,IP=?,Port=?,Mediator=?,EncPubKey=?,SignPubKey=? WHERE PeerIP=?", row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey, row.PeerIP)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows updated")
}
}
func Peer_Delete(
tx TX,
PeerIP byte,
) (err error) {
result, err := tx.Exec("DELETE FROM peers WHERE PeerIP=?", PeerIP)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
panic(err)
}
switch n {
case 0:
return sql.ErrNoRows
case 1:
return nil
default:
panic("multiple rows deleted")
}
}
func Peer_Get(
tx TX,
PeerIP byte,
) (
row *Peer,
err error,
) {
row = &Peer{}
r := tx.QueryRow("SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers WHERE PeerIP=?", PeerIP)
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
return
}
func Peer_GetWhere(
tx TX,
query string,
args ...any,
) (
row *Peer,
err error,
) {
row = &Peer{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
return
}
func Peer_Iterate(
tx TX,
query string,
args ...any,
) iter.Seq2[*Peer, error] {
rows, err := tx.Query(query, args...)
if err != nil {
return func(yield func(*Peer, error) bool) {
yield(nil, err)
}
}
return func(yield func(*Peer, error) bool) {
defer rows.Close()
for rows.Next() {
row := &Peer{}
err := rows.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
if !yield(row, err) {
return
}
}
}
}
func Peer_List(
tx TX,
query string,
args ...any,
) (
l []*Peer,
err error,
) {
for row, err := range Peer_Iterate(tx, query, args...) {
if err != nil {
return nil, err
}
l = append(l, row)
}
return l, nil
}

View File

@@ -0,0 +1,69 @@
package db
import (
"errors"
"net/netip"
"net/url"
"strings"
)
var (
ErrInvalidIP = errors.New("invalid IP")
ErrInvalidPort = errors.New("invalid port")
)
func Config_Sanitize(c *Config) {
if u, err := url.Parse(c.HubAddress); err == nil {
c.HubAddress = u.String()
}
if addr, ok := netip.AddrFromSlice(c.VPNNetwork); ok {
c.VPNNetwork = addr.AsSlice()
}
}
func Config_Validate(c *Config) error {
if _, err := url.Parse(c.HubAddress); err != nil {
return err
}
addr, ok := netip.AddrFromSlice(c.VPNNetwork)
if !ok || !addr.Is4() || addr.As4()[3] != 0 || addr.As4()[0] == 0 {
return ErrInvalidIP
}
return nil
}
func Session_Sanitize(s *Session) {
}
func Session_Validate(s *Session) error {
return nil
}
func Peer_Sanitize(p *Peer) {
p.Name = strings.TrimSpace(p.Name)
if len(p.IP) != 0 {
addr, ok := netip.AddrFromSlice(p.IP)
if ok && addr.Is4() {
p.IP = addr.AsSlice()
}
}
if p.Port == 0 {
p.Port = 515
}
}
func Peer_Validate(p *Peer) error {
if len(p.IP) > 0 {
_, ok := netip.AddrFromSlice(p.IP)
if !ok {
return ErrInvalidIP
}
}
if p.Port == 0 {
return ErrInvalidPort
}
return nil
}

25
hub/api/db/tables.defs Normal file
View File

@@ -0,0 +1,25 @@
TABLE config OF Config (
ConfigID int64 PK,
HubAddress string,
VPNNetwork []byte,
Password []byte NoUpdate
);
TABLE sessions OF Session NoUpdate (
SessionID string PK,
CSRF string,
SignedIn bool,
CreatedAt int64,
LastSeenAt int64
);
TABLE peers OF Peer (
PeerIP byte PK,
APIKey string NoUpdate,
Name string,
IP []byte,
Port uint16,
Mediator bool,
EncPubKey []byte NoUpdate,
SignPubKey []byte NoUpdate
);

51
hub/api/db/written.go Normal file
View File

@@ -0,0 +1,51 @@
package db
import "vppn/fasttime"
func Session_UpdateLastSeenAt(
tx TX,
id string,
) (err error) {
_, err = tx.Exec("UPDATE sessions SET LastSeenAt=? WHERE SessionID=?", fasttime.Now(), id)
return err
}
func Session_SetSignedIn(
tx TX,
id string,
) (err error) {
_, err = tx.Exec("UPDATE sessions SET SignedIn=1 WHERE SessionID=?", id)
return err
}
func Session_DeleteBefore(
tx TX,
timestamp int64,
) (err error) {
_, err = tx.Exec("DELETE FROM sessions WHERE LastSeenAt<?", timestamp)
return err
}
func Config_UpdatePassword(
tx TX,
pwdHash []byte,
) (err error) {
_, err = tx.Exec("UPDATE config SET Password=? WHERE ConfigID=1", pwdHash)
return err
}
func Peer_ListAll(tx TX) ([]*Peer, error) {
return Peer_List(tx, Peer_SelectQuery)
}
func Peer_GetByAPIKey(tx TX, apiKey string) (*Peer, error) {
return Peer_GetWhere(
tx,
Peer_SelectQuery+` WHERE APIKey=?`,
apiKey)
}
func Peer_Exists(tx TX, ip byte) (exists bool, err error) {
err = tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM peers WHERE PeerIP=?)`, ip).Scan(&exists)
return
}

13
hub/api/errors.go Normal file
View File

@@ -0,0 +1,13 @@
package api
import (
"errors"
"vppn/hub/api/db"
)
var (
ErrNotAuthorized = errors.New("not authorized")
ErrNoIPAvailable = errors.New("no IP address available")
ErrInvalidIP = db.ErrInvalidIP
ErrInvalidPort = db.ErrInvalidPort
)

View File

@@ -0,0 +1,27 @@
CREATE TABLE config (
ConfigID INTEGER NOT NULL PRIMARY KEY, -- Always 1.
HubAddress TEXT NOT NULL, -- https://for.example.com
VPNNetwork BLOB NOT NULL, -- Network (/24), example 10.51.50.0
Password BLOB NOT NULL -- bcrypt password for web interface
) WITHOUT ROWID;
CREATE TABLE sessions (
SessionID TEXT NOT NULL PRIMARY KEY,
CSRF TEXT NOT NULL,
SignedIn INTEGER NOT NULL,
CreatedAt INTEGER NOT NULL,
LastSeenAt INTEGER NOT NULL
) WITHOUT ROWID;
CREATE INDEX sessions_last_seen_index ON sessions(LastSeenAt);
CREATE TABLE peers (
PeerIP INTEGER NOT NULL PRIMARY KEY, -- Final byte.
APIKey TEXT NOT NULL UNIQUE,
Name TEXT NOT NULL UNIQUE, -- For humans.
IP BLOB NOT NULL,
Port INTEGER NOT NULL,
Mediator INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Must also have public address.
EncPubKey BLOB NOT NULL,
SignPubKey BLOB NOT NULL
) WITHOUT ROWID;

1
hub/api/session.go Normal file
View File

@@ -0,0 +1 @@
package api

7
hub/api/time.go Normal file
View File

@@ -0,0 +1,7 @@
package api
import "time"
func timeSince(ts int64) int64 {
return time.Now().Unix() - ts
}

7
hub/api/types.go Normal file
View File

@@ -0,0 +1,7 @@
package api
import "vppn/hub/api/db"
type Config = db.Config
type Session = db.Session
type Peer = db.Peer