diff --git a/README.md b/README.md index 4567196..29ae92c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Add and start the hub server: ``` systemctl daemon-reload +systemctl enable hub systemctl start hub ``` @@ -62,10 +63,19 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN Type=simple User=user WorkingDirectory=/home/user/ -ExecStart=/home/user/vppn -name vppn -hub-address https://my.hub -api-key 1234567890 +ExecStart=/home/user/vppn -hub-address https://my.hub -api-key 1234567890 Restart=always RestartSec=8 [Install] -WantedBy=default.target +WantedBy=multi-user.target +``` + +Add and start the service: + + +``` +systemctl daemon-reload +systemctl enable vppn +systemctl start vppn ``` diff --git a/go.mod b/go.mod index 82d87c4..e55e1f6 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module vppn go 1.24.1 require ( - git.crumpington.com/lib/go v0.8.1 - golang.org/x/crypto v0.29.0 - golang.org/x/sys v0.27.0 + git.crumpington.com/lib/go v0.9.0 + golang.org/x/crypto v0.36.0 + golang.org/x/sys v0.31.0 ) require ( github.com/mattn/go-sqlite3 v1.14.24 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index fde1083..a173169 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ -git.crumpington.com/lib/go v0.8.1 h1:rWjddllSxQ4yReraqDaGZAod4NpRD9LtGx1yV71ytcU= -git.crumpington.com/lib/go v0.8.1/go.mod h1:XjQaf2NFlje9BJ1EevZL8NNioPrAe7WwHpKUhcDw2Lk= +git.crumpington.com/lib/go v0.9.0 h1:QXoMhsycSgEUWNiiPZWl0jgBls+NI9TNR5Z6nNXslCM= +git.crumpington.com/lib/go v0.9.0/go.mod h1:i3DXiPDo/pgPMHAxUTpyo1Xj2spcvXwXcBef3aSYlnQ= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= diff --git a/hub/api/api.go b/hub/api/api.go index 801f689..7a534ca 100644 --- a/hub/api/api.go +++ b/hub/api/api.go @@ -54,17 +54,11 @@ func (a *API) ensurePassword() error { 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, - } - + conf := &Config{ConfigID: 1, Password: hashed} return db.Config_Insert(a.db, conf) } @@ -80,10 +74,6 @@ 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) } @@ -137,6 +127,24 @@ func (a *API) Session_SignIn(s *Session, pwd string) error { return db.Session_SetSignedIn(a.db, s.SessionID) } +func (a *API) Network_Create(n *Network) error { + n.NetworkID = idgen.NextID(0) + return db.Network_Insert(a.db, n) +} + +func (a *API) Network_Delete(n *Network) error { + return db.Network_Delete(a.db, n.NetworkID) +} + +func (a *API) Network_Get(id int64) (*Network, error) { + return db.Network_Get(a.db, id) +} + +func (a *API) Network_List() ([]*Network, error) { + const query = db.Network_SelectQuery + ` ORDER BY Name ASC` + return db.Network_List(a.db, query) +} + func (a *API) Peer_CreateNew(p *Peer) error { p.Version = idgen.NextID(0) p.PubKey = []byte{} @@ -146,7 +154,7 @@ func (a *API) Peer_CreateNew(p *Peer) error { return db.Peer_Insert(a.db, p) } -func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) (*m.PeerConfig, error) { +func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) error { a.lock.Lock() defer a.lock.Unlock() @@ -154,19 +162,7 @@ func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) (*m.PeerConfig, error) peer.PubKey = args.EncPubKey peer.PubSignKey = args.PubSignKey - if err := db.Peer_UpdateFull(a.db, peer); err != nil { - return nil, err - } - - conf := a.Config_Get() - - return &m.PeerConfig{ - PeerIP: peer.PeerIP, - Network: conf.VPNNetwork, - PublicIP: peer.PublicIP, - Port: peer.Port, - Relay: peer.Relay, - }, nil + return db.Peer_UpdateFull(a.db, peer) } func (a *API) Peer_Update(p *Peer) error { @@ -177,16 +173,16 @@ 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_Delete(networkID int64, peerIP byte) error { + return db.Peer_Delete(a.db, networkID, peerIP) } -func (a *API) Peer_List() ([]*Peer, error) { - return db.Peer_ListAll(a.db) +func (a *API) Peer_List(networkID int64) ([]*Peer, error) { + return db.Peer_ListAll(a.db, networkID) } -func (a *API) Peer_Get(ip byte) (*Peer, error) { - return db.Peer_Get(a.db, ip) +func (a *API) Peer_Get(networkID int64, ip byte) (*Peer, error) { + return db.Peer_Get(a.db, networkID, ip) } func (a *API) Peer_GetByAPIKey(key string) (*Peer, error) { diff --git a/hub/api/db/generated b/hub/api/db/generated deleted file mode 100644 index e69de29..0000000 diff --git a/hub/api/db/generated.go b/hub/api/db/generated.go index 1548afd..88aec6c 100644 --- a/hub/api/db/generated.go +++ b/hub/api/db/generated.go @@ -16,13 +16,11 @@ type TX interface { // ---------------------------------------------------------------------------- type Config struct { - ConfigID int64 - HubAddress string - VPNNetwork []byte - Password []byte + ConfigID int64 + Password []byte } -const Config_SelectQuery = "SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config" +const Config_SelectQuery = "SELECT ConfigID,Password FROM config" func Config_Insert( tx TX, @@ -33,7 +31,7 @@ func Config_Insert( return err } - _, err = tx.Exec("INSERT INTO config(ConfigID,HubAddress,VPNNetwork,Password) VALUES(?,?,?,?)", row.ConfigID, row.HubAddress, row.VPNNetwork, row.Password) + _, err = tx.Exec("INSERT INTO config(ConfigID,Password) VALUES(?,?)", row.ConfigID, row.Password) return err } @@ -46,7 +44,7 @@ func Config_Update( return err } - result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.ConfigID) + result, err := tx.Exec("UPDATE config SET Password=? WHERE ConfigID=?", row.Password, row.ConfigID) if err != nil { return err } @@ -74,7 +72,7 @@ func Config_UpdateFull( return err } - result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=?,Password=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.Password, row.ConfigID) + result, err := tx.Exec("UPDATE config SET Password=? WHERE ConfigID=?", row.Password, row.ConfigID) if err != nil { return err } @@ -124,8 +122,8 @@ func Config_Get( 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) + r := tx.QueryRow("SELECT ConfigID,Password FROM config WHERE ConfigID=?", ConfigID) + err = r.Scan(&row.ConfigID, &row.Password) return } @@ -139,7 +137,7 @@ func Config_GetWhere( ) { row = &Config{} r := tx.QueryRow(query, args...) - err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password) + err = r.Scan(&row.ConfigID, &row.Password) return } @@ -159,7 +157,7 @@ func Config_Iterate( defer rows.Close() for rows.Next() { row := &Config{} - err := rows.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password) + err := rows.Scan(&row.ConfigID, &row.Password) if !yield(row, err) { return } @@ -302,11 +300,156 @@ func Session_List( return l, nil } +// ---------------------------------------------------------------------------- +// Table: networks +// ---------------------------------------------------------------------------- + +type Network struct { + NetworkID int64 + Name string + Network []byte +} + +const Network_SelectQuery = "SELECT NetworkID,Name,Network FROM networks" + +func Network_Insert( + tx TX, + row *Network, +) (err error) { + Network_Sanitize(row) + if err = Network_Validate(row); err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO networks(NetworkID,Name,Network) VALUES(?,?,?)", row.NetworkID, row.Name, row.Network) + return err +} + +func Network_UpdateFull( + tx TX, + row *Network, +) (err error) { + Network_Sanitize(row) + if err = Network_Validate(row); err != nil { + return err + } + + result, err := tx.Exec("UPDATE networks SET Name=?,Network=? WHERE NetworkID=?", row.Name, row.Network, row.NetworkID) + 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 Network_Delete( + tx TX, + NetworkID int64, +) (err error) { + result, err := tx.Exec("DELETE FROM networks WHERE NetworkID=?", NetworkID) + 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 Network_Get( + tx TX, + NetworkID int64, +) ( + row *Network, + err error, +) { + row = &Network{} + r := tx.QueryRow("SELECT NetworkID,Name,Network FROM networks WHERE NetworkID=?", NetworkID) + err = r.Scan(&row.NetworkID, &row.Name, &row.Network) + return +} + +func Network_GetWhere( + tx TX, + query string, + args ...any, +) ( + row *Network, + err error, +) { + row = &Network{} + r := tx.QueryRow(query, args...) + err = r.Scan(&row.NetworkID, &row.Name, &row.Network) + return +} + +func Network_Iterate( + tx TX, + query string, + args ...any, +) iter.Seq2[*Network, error] { + rows, err := tx.Query(query, args...) + if err != nil { + return func(yield func(*Network, error) bool) { + yield(nil, err) + } + } + + return func(yield func(*Network, error) bool) { + defer rows.Close() + for rows.Next() { + row := &Network{} + err := rows.Scan(&row.NetworkID, &row.Name, &row.Network) + if !yield(row, err) { + return + } + } + } +} + +func Network_List( + tx TX, + query string, + args ...any, +) ( + l []*Network, + err error, +) { + for row, err := range Network_Iterate(tx, query, args...) { + if err != nil { + return nil, err + } + l = append(l, row) + } + return l, nil +} + // ---------------------------------------------------------------------------- // Table: peers // ---------------------------------------------------------------------------- type Peer struct { + NetworkID int64 PeerIP byte Version int64 APIKey string @@ -318,7 +461,7 @@ type Peer struct { PubSignKey []byte } -const Peer_SelectQuery = "SELECT PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers" +const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers" func Peer_Insert( tx TX, @@ -329,7 +472,7 @@ func Peer_Insert( return err } - _, err = tx.Exec("INSERT INTO peers(PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey) VALUES(?,?,?,?,?,?,?,?,?)", row.PeerIP, row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey) + _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey) VALUES(?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey) return err } @@ -342,7 +485,7 @@ func Peer_Update( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,PublicIP=?,Port=?,Relay=? WHERE PeerIP=?", row.Version, row.Name, row.PublicIP, row.Port, row.Relay, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,PublicIP=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.PublicIP, row.Port, row.Relay, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -370,7 +513,7 @@ func Peer_UpdateFull( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,PublicIP=?,Port=?,Relay=?,PubKey=?,PubSignKey=? WHERE PeerIP=?", row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,PublicIP=?,Port=?,Relay=?,PubKey=?,PubSignKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.PublicIP, row.Port, row.Relay, row.PubKey, row.PubSignKey, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -391,9 +534,10 @@ func Peer_UpdateFull( func Peer_Delete( tx TX, + NetworkID int64, PeerIP byte, ) (err error) { - result, err := tx.Exec("DELETE FROM peers WHERE PeerIP=?", PeerIP) + result, err := tx.Exec("DELETE FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) if err != nil { return err } @@ -414,14 +558,15 @@ func Peer_Delete( func Peer_Get( tx TX, + NetworkID int64, PeerIP byte, ) ( row *Peer, err error, ) { row = &Peer{} - r := tx.QueryRow("SELECT PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers WHERE PeerIP=?", PeerIP) - err = r.Scan(&row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) + r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP,Port,Relay,PubKey,PubSignKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) + err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) return } @@ -435,7 +580,7 @@ func Peer_GetWhere( ) { row = &Peer{} r := tx.QueryRow(query, args...) - err = r.Scan(&row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) + err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) return } @@ -455,7 +600,7 @@ func Peer_Iterate( defer rows.Close() for rows.Next() { row := &Peer{} - err := rows.Scan(&row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) + err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP, &row.Port, &row.Relay, &row.PubKey, &row.PubSignKey) if !yield(row, err) { return } diff --git a/hub/api/db/sanitize-validate.go b/hub/api/db/sanitize-validate.go index e06ad94..71785e9 100644 --- a/hub/api/db/sanitize-validate.go +++ b/hub/api/db/sanitize-validate.go @@ -3,35 +3,21 @@ package db import ( "errors" "net/netip" - "net/url" "strings" ) var ( - ErrInvalidIP = errors.New("invalid IP") - ErrInvalidPort = errors.New("invalid port") + ErrInvalidIP = errors.New("invalid IP") + ErrNonPrivateIP = errors.New("non-private IP") + ErrInvalidPort = errors.New("invalid port") + ErrInvalidNetName = errors.New("invalid network name") + ErrInvalidPeerName = errors.New("invalid peer name") ) 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 } @@ -42,6 +28,42 @@ func Session_Validate(s *Session) error { return nil } +func Network_Sanitize(n *Network) { + n.Name = strings.TrimSpace(n.Name) + + if addr, ok := netip.AddrFromSlice(n.Network); ok { + n.Network = addr.AsSlice() + } +} + +func Network_Validate(c *Network) error { + // 16 bytes is linux limit for network interface names. + if len(c.Name) == 0 || len(c.Name) > 16 { + return ErrInvalidNetName + } + + for _, c := range c.Name { + if c >= 'a' && c <= 'z' { + continue + } + if c >= '0' && c <= '9' { + continue + } + return ErrInvalidNetName + } + + addr, ok := netip.AddrFromSlice(c.Network) + if !ok || !addr.Is4() || addr.As4()[3] != 0 || addr.As4()[0] == 0 { + return ErrInvalidIP + } + + if !addr.IsPrivate() { + return ErrNonPrivateIP + } + + return nil +} + func Peer_Sanitize(p *Peer) { p.Name = strings.TrimSpace(p.Name) if len(p.PublicIP) != 0 { @@ -65,5 +87,20 @@ func Peer_Validate(p *Peer) error { if p.Port == 0 { return ErrInvalidPort } + + for _, c := range p.Name { + if c >= 'a' && c <= 'z' { + continue + } + if c >= '0' && c <= '9' { + continue + } + if c == '.' || c == '-' || c == '_' { + continue + } + + return ErrInvalidPeerName + } + return nil } diff --git a/hub/api/db/tables.defs b/hub/api/db/tables.defs index 08244e4..d6dc338 100644 --- a/hub/api/db/tables.defs +++ b/hub/api/db/tables.defs @@ -1,8 +1,6 @@ TABLE config OF Config ( ConfigID int64 PK, - HubAddress string, - VPNNetwork []byte, - Password []byte NoUpdate + Password []byte ); TABLE sessions OF Session NoUpdate ( @@ -13,7 +11,14 @@ TABLE sessions OF Session NoUpdate ( LastSeenAt int64 ); +TABLE networks OF Network ( + NetworkID int64 PK, + Name string NoUpdate, + Network []byte NoUpdate +); + TABLE peers OF Peer ( + NetworkID int64 PK, PeerIP byte PK, Version int64, APIKey string NoUpdate, diff --git a/hub/api/db/written.go b/hub/api/db/written.go index 5b8bb15..6d61bb5 100644 --- a/hub/api/db/written.go +++ b/hub/api/db/written.go @@ -26,16 +26,9 @@ func Session_DeleteBefore( 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_ListAll(tx TX, networkID int64) ([]*Peer, error) { + const query = Peer_SelectQuery + ` WHERE NetworkID=? ORDER BY PeerIP ASC` + return Peer_List(tx, query, networkID) } func Peer_GetByAPIKey(tx TX, apiKey string) (*Peer, error) { @@ -45,7 +38,8 @@ func Peer_GetByAPIKey(tx TX, apiKey string) (*Peer, error) { 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) +func Peer_Exists(tx TX, networkID int64, ip byte) (exists bool, err error) { + const query = `SELECT EXISTS(SELECT 1 FROM peers WHERE NetworkID=? AND PeerIP=?)` + err = tx.QueryRow(query, networkID, ip).Scan(&exists) return } diff --git a/hub/api/migrations/2024-11-30-init.sql b/hub/api/migrations/2024-11-30-init.sql index ee37ddc..f60aa77 100644 --- a/hub/api/migrations/2024-11-30-init.sql +++ b/hub/api/migrations/2024-11-30-init.sql @@ -1,7 +1,5 @@ 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; @@ -15,13 +13,22 @@ CREATE TABLE sessions ( CREATE INDEX sessions_last_seen_index ON sessions(LastSeenAt); +CREATE TABLE networks ( + NetworkID INTEGER NOT NULL PRIMARY KEY, + Name TEXT NOT NULL UNIQUE, -- Network/interface name. + Network BLOB NOT NULL UNIQUE -- Network (/24), example 10.51.50.0 +) WITHOUT ROWID; + CREATE TABLE peers ( - PeerIP INTEGER NOT NULL PRIMARY KEY, -- Final byte. - Version INTEGER NOT NULL, - APIKey TEXT NOT NULL UNIQUE, + NetworkID INTEGER NOT NULL, + PeerIP INTEGER NOT NULL, -- Final byte of IP. + Version INTEGER NOT NULL, -- Changes when updated. + APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key. Name TEXT NOT NULL UNIQUE, -- For humans. PublicIP BLOB NOT NULL, Port INTEGER NOT NULL, Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Must also have public address. - PubKey BLOB NOT NULL + PubKey BLOB NOT NULL, + PubSignKey BLOB NOT NULL, + PRIMARY KEY(NetworkID, PeerIP) ) WITHOUT ROWID; diff --git a/hub/api/migrations/2024-12-27-signing-keys.sql b/hub/api/migrations/2024-12-27-signing-keys.sql deleted file mode 100644 index 8731917..0000000 --- a/hub/api/migrations/2024-12-27-signing-keys.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE peers ADD COLUMN PubSignKey BLOB NOT NULL DEFAULT ''; diff --git a/hub/api/types.go b/hub/api/types.go index 92d227c..bfcfc04 100644 --- a/hub/api/types.go +++ b/hub/api/types.go @@ -4,4 +4,5 @@ import "vppn/hub/api/db" type Config = db.Config type Session = db.Session +type Network = db.Network type Peer = db.Peer diff --git a/hub/form.go b/hub/form.go new file mode 100644 index 0000000..645223d --- /dev/null +++ b/hub/form.go @@ -0,0 +1,42 @@ +package hub + +import ( + "net/url" + "vppn/hub/api" + + "git.crumpington.com/lib/go/webutil" +) + +func (app *App) formGetNetwork(form url.Values) (*api.Network, error) { + var id int64 + if err := webutil.NewFormScanner(form).Scan("NetworkID", &id).Error(); err != nil { + return nil, err + } + + return app.api.Network_Get(id) +} + +func (app *App) formGetNetworkPeers(form url.Values) (*api.Network, []*api.Peer, error) { + n, err := app.formGetNetwork(form) + if err != nil { + return nil, nil, err + } + + peers, err := app.api.Peer_List(n.NetworkID) + return n, peers, err +} + +func (app *App) formGetPeer(form url.Values) (*api.Network, *api.Peer, error) { + net, err := app.formGetNetwork(form) + if err != nil { + return nil, nil, err + } + + var ip byte + if err := webutil.NewFormScanner(form).Scan("PeerIP", &ip).Error(); err != nil { + return nil, nil, err + } + + peer, err := app.api.Peer_Get(net.NetworkID, ip) + return net, peer, err +} diff --git a/hub/handlers.go b/hub/handlers.go index c81c9ad..ab3c625 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -16,7 +16,7 @@ import ( func (a *App) _root(s *api.Session, w http.ResponseWriter, r *http.Request) error { if s.SignedIn { - return a.redirect(w, r, "/admin/config/") + return a.redirect(w, r, "/admin/network/list/") } else { return a.redirect(w, r, "/sign-in/") } @@ -54,54 +54,219 @@ func (a *App) _adminSignOutSubmit(s *api.Session, w http.ResponseWriter, r *http return a.redirect(w, r, "/") } -func (a *App) _adminConfig(s *api.Session, w http.ResponseWriter, r *http.Request) error { - peers, err := a.api.Peer_List() +func (a *App) _adminNetworkList(s *api.Session, w http.ResponseWriter, r *http.Request) error { + l, err := a.api.Network_List() if err != nil { return err } - - return a.render("/admin-config.html", w, struct { - Session *api.Session - Peers []*api.Peer - Config *api.Config - }{ - s, - peers, - a.api.Config_Get(), - }) + return a.render("/admin-network-list.html", w, struct { + Session *api.Session + Networks []*api.Network + }{s, l}) } -func (a *App) _adminConfigEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - return a.render("/admin-config-edit.html", w, struct { - Session *api.Session - Config *api.Config - }{ - s, - a.api.Config_Get(), - }) +func (a *App) _adminNetworkCreate(s *api.Session, w http.ResponseWriter, r *http.Request) error { + return a.render("/admin-network-create.html", w, struct{ Session *api.Session }{s}) } -func (a *App) _adminConfigEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var ( - conf = a.api.Config_Get() - ipStr string - ) +func (a *App) _adminNetworkCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n := &api.Network{} + var netStr string err := webutil.NewFormScanner(r.Form). - Scan("HubAddress", &conf.HubAddress). - Scan("VPNNetwork", &ipStr). + Scan("Name", &n.Name). + Scan("Network", &netStr). Error() if err != nil { return err } - if conf.VPNNetwork, err = stringToIP(ipStr); err != nil { + n.Network, err = stringToIP(netStr) + if err != nil { return err } - if err := a.api.Config_Update(conf); err != nil { + + if err := a.api.Network_Create(n); err != nil { return err } - return a.redirect(w, r, "/admin/config/") + + return a.redirect(w, r, "/admin/network/view/?NetworkID=%d", n.NetworkID) +} + +func (a *App) _adminNetworkView(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, peers, err := a.formGetNetworkPeers(r.Form) + if err != nil { + return err + } + + return a.render("/network/network-view.html", w, struct { + Session *api.Session + Network *api.Network + Peers []*api.Peer + }{s, n, peers}) +} + +func (a *App) _adminNetworkDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, peers, err := a.formGetNetworkPeers(r.Form) + if err != nil { + return err + } + + return a.render("/network/network-delete.html", w, struct { + Session *api.Session + Network *api.Network + Peers []*api.Peer + }{s, n, peers}) +} + +func (a *App) _adminNetworkDeleteSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, err := a.formGetNetwork(r.Form) + if err != nil { + return err + } + + if err = a.api.Network_Delete(n); err != nil { + return err + } + return a.redirect(w, r, "/admin/network/list/") +} + +func (a *App) _adminPeerCreate(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, err := a.formGetNetwork(r.Form) + if err != nil { + return err + } + + return a.render("/network/peer-create.html", w, struct { + Session *api.Session + Network *api.Network + }{s, n}) +} + +func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + var ipStr string + + p := &api.Peer{} + err := webutil.NewFormScanner(r.Form). + Scan("NetworkID", &p.NetworkID). + Scan("IP", &p.PeerIP). + Scan("Name", &p.Name). + Scan("PublicIP", &ipStr). + Scan("Port", &p.Port). + Scan("Relay", &p.Relay). + Error() + if err != nil { + return err + } + + if p.PublicIP, err = stringToIP(ipStr); err != nil { + return err + } + + if err := a.api.Peer_CreateNew(p); err != nil { + return err + } + return a.redirect(w, r, "/admin/peer/view/?NetworkID=%d&PeerIP=%d", p.NetworkID, p.PeerIP) +} + +func (a *App) _adminPeerView(s *api.Session, w http.ResponseWriter, r *http.Request) error { + net, peer, err := a.formGetPeer(r.Form) + if err != nil { + return err + } + + return a.render("/network/peer-view.html", w, struct { + Session *api.Session + Network *api.Network + Peer *api.Peer + }{s, net, peer}) +} + +func (a *App) _adminPeerEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + net, peer, err := a.formGetPeer(r.Form) + if err != nil { + return err + } + + return a.render("/network/peer-edit.html", w, struct { + Session *api.Session + Network *api.Network + Peer *api.Peer + }{s, net, peer}) +} + +func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + _, peer, err := a.formGetPeer(r.Form) + if err != nil { + return err + } + + var ipStr string + + err = webutil.NewFormScanner(r.Form). + Scan("Name", &peer.Name). + Scan("PublicIP", &ipStr). + Scan("Port", &peer.Port). + Scan("Relay", &peer.Relay). + Error() + if err != nil { + return err + } + + if peer.PublicIP, err = stringToIP(ipStr); err != nil { + return err + } + + if err = a.api.Peer_Update(peer); err != nil { + return err + } + + return a.redirect(w, r, "/admin/peer/view/?NetworkID=%d&PeerIP=%d", peer.NetworkID, peer.PeerIP) +} + +func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, peer, err := a.formGetPeer(r.Form) + if err != nil { + return err + } + + return a.render("/network/peer-delete.html", w, struct { + Session *api.Session + Network *api.Network + Peer *api.Peer + }{s, n, peer}) +} + +func (a *App) _adminPeerDeleteSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, peer, err := a.formGetPeer(r.Form) + if err != nil { + return err + } + if err := a.api.Peer_Delete(n.NetworkID, peer.PeerIP); err != nil { + return err + } + return a.redirect(w, r, "/admin/network/view/?NetworkID=%d", n.NetworkID) +} + +func (a *App) _adminNetworkHosts(s *api.Session, w http.ResponseWriter, r *http.Request) error { + n, peers, err := a.formGetNetworkPeers(r.Form) + if err != nil { + return err + } + + b := strings.Builder{} + + for _, peer := range peers { + ip := n.Network + ip[3] = peer.PeerIP + b.WriteString(netip.AddrFrom4([4]byte(ip)).String()) + b.WriteString(" ") + b.WriteString(peer.Name) + b.WriteString("\n") + } + + w.Write([]byte(b.String())) + return nil } func (a *App) _adminPasswordEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error { @@ -143,194 +308,61 @@ func (a *App) _adminPasswordSubmit(s *api.Session, w http.ResponseWriter, r *htt return err } - if err := a.api.Config_UpdatePassword(hash); err != nil { + conf.Password = hash + + if err := a.api.Config_Update(conf); err != nil { return err } return a.redirect(w, r, "/admin/config/") } -func (a *App) _adminHosts(s *api.Session, w http.ResponseWriter, r *http.Request) error { - conf := a.api.Config_Get() - - peers, err := a.api.Peer_List() - if err != nil { - return err - } - - b := strings.Builder{} - - for _, peer := range peers { - ip := conf.VPNNetwork - ip[3] = peer.PeerIP - b.WriteString(netip.AddrFrom4([4]byte(ip)).String()) - b.WriteString(" ") - b.WriteString(peer.Name) - b.WriteString("\n") - } - - w.Write([]byte(b.String())) - return nil -} - -func (a *App) _adminPeerCreate(s *api.Session, w http.ResponseWriter, r *http.Request) error { - return a.render("/admin-peer-create.html", w, struct{ Session *api.Session }{s}) -} - -func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var ipStr string - - p := &api.Peer{} - err := webutil.NewFormScanner(r.Form). - Scan("IP", &p.PeerIP). - Scan("Name", &p.Name). - Scan("PublicIP", &ipStr). - Scan("Port", &p.Port). - Scan("Relay", &p.Relay). - Error() - if err != nil { - return err - } - - if p.PublicIP, err = stringToIP(ipStr); err != nil { - return err - } - - if err := a.api.Peer_CreateNew(p); err != nil { - return err - } - return a.redirect(w, r, "/admin/peer/view/?PeerIP=%d", p.PeerIP) -} - -func (a *App) _adminPeerView(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var peerIP byte - err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error() - if err != nil { - return err - } - - peer, err := a.api.Peer_Get(peerIP) - if err != nil { - return err - } - - return a.render("/admin-peer-view.html", w, struct { - Session *api.Session - Peer *api.Peer - }{s, peer}) -} - -func (a *App) _adminPeerEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var peerIP byte - err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error() - if err != nil { - return err - } - - peer, err := a.api.Peer_Get(peerIP) - if err != nil { - return err - } - - return a.render("/admin-peer-edit.html", w, struct { - Session *api.Session - Peer *api.Peer - }{s, peer}) -} - -func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var ( - peerIP byte - ipStr string - ) - - err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error() - if err != nil { - return err - } - - peer, err := a.api.Peer_Get(peerIP) - if err != nil { - return err - } - err = webutil.NewFormScanner(r.Form). - Scan("Name", &peer.Name). - Scan("PublicIP", &ipStr). - Scan("Port", &peer.Port). - Scan("Relay", &peer.Relay). - Error() - if err != nil { - return err - } - - if peer.PublicIP, err = stringToIP(ipStr); err != nil { - return err - } - - if err = a.api.Peer_Update(peer); err != nil { - return err - } - - return a.redirect(w, r, "/admin/peer/view/?PeerIP=%d", peer.PeerIP) -} - -func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var peerIP byte - err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error() - if err != nil { - return err - } - - peer, err := a.api.Peer_Get(peerIP) - if err != nil { - return err - } - - return a.render("/admin-peer-delete.html", w, struct { - Session *api.Session - Peer *api.Peer - }{s, peer}) -} - -func (a *App) _adminPeerDeleteSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - var peerIP byte - err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error() - if err != nil { - return err - } - - if err := a.api.Peer_Delete(peerIP); err != nil { - return err - } - - return a.redirect(w, r, "/admin/peer/list/") -} - func (a *App) _peerInit(peer *api.Peer, w http.ResponseWriter, r *http.Request) error { args := m.PeerInitArgs{} if err := json.NewDecoder(r.Body).Decode(&args); err != nil { return err } - conf, err := a.api.Peer_Init(peer, args) + net, err := a.api.Network_Get(peer.NetworkID) if err != nil { return err } - return a.sendJSON(w, conf) + if err := a.api.Peer_Init(peer, args); err != nil { + return err + } + + resp := m.PeerInitResp{ + PeerIP: peer.PeerIP, + Network: net.Network, + } + + resp.NetworkState.Peers, err = a.peersArray(net.NetworkID) + if err != nil { + return err + } + + return a.sendJSON(w, resp) } func (a *App) _peerFetchState(peer *api.Peer, w http.ResponseWriter, r *http.Request) error { - peers, err := a.api.Peer_List() + + peers, err := a.peersArray(peer.NetworkID) if err != nil { return err } + return a.sendJSON(w, m.NetworkState{Peers: peers}) +} - state := m.NetworkState{} +func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) { + l, err := a.api.Peer_List(networkID) + if err != nil { + return peers, err + } - for _, p := range peers { + for _, p := range l { if len(p.PubKey) != 0 { - state.Peers[p.PeerIP] = &m.Peer{ + peers[p.PeerIP] = &m.Peer{ PeerIP: p.PeerIP, Version: p.Version, Name: p.Name, @@ -343,5 +375,5 @@ func (a *App) _peerFetchState(peer *api.Peer, w http.ResponseWriter, r *http.Req } } - return a.sendJSON(w, state) + return } diff --git a/hub/routes.go b/hub/routes.go index 7d505c5..f94e271 100644 --- a/hub/routes.go +++ b/hub/routes.go @@ -9,14 +9,17 @@ func (a *App) registerRoutes() { a.handleNotSignedIn("GET /sign-in/", a._signin) a.handleNotSignedIn("POST /sign-in/", a._signinSubmit) - a.handleSignedIn("GET /admin/config/", a._adminConfig) - a.handleSignedIn("GET /admin/config/edit/", a._adminConfigEdit) - a.handleSignedIn("POST /admin/config/edit/", a._adminConfigEditSubmit) a.handleSignedIn("GET /admin/sign-out/", a._adminSignOut) a.handleSignedIn("POST /admin/sign-out/", a._adminSignOutSubmit) - a.handleSignedIn("GET /admin/password/edit/", a._adminPasswordEdit) - a.handleSignedIn("POST /admin/password/edit/", a._adminPasswordSubmit) - a.handleSignedIn("GET /admin/peer/hosts/", a._adminHosts) + + a.handleSignedIn("GET /admin/network/list/", a._adminNetworkList) + a.handleSignedIn("GET /admin/network/create/", a._adminNetworkCreate) + a.handleSignedIn("POST /admin/network/create/", a._adminNetworkCreateSubmit) + a.handleSignedIn("GET /admin/network/delete/", a._adminNetworkDelete) + a.handleSignedIn("POST /admin/network/delete/", a._adminNetworkDeleteSubmit) + + a.handleSignedIn("GET /admin/network/view/", a._adminNetworkView) + a.handleSignedIn("GET /admin/network/hosts/", a._adminNetworkHosts) a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate) a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit) a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView) @@ -25,6 +28,9 @@ func (a *App) registerRoutes() { a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete) a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit) + a.handleSignedIn("GET /admin/password/edit/", a._adminPasswordEdit) + a.handleSignedIn("POST /admin/password/edit/", a._adminPasswordSubmit) + a.handlePeer("POST /peer/init/", a._peerInit) a.handlePeer("GET /peer/fetch-state/", a._peerFetchState) } diff --git a/hub/templates/admin-config-edit.html b/hub/templates/admin-config-edit.html deleted file mode 100644 index 0dcb1b3..0000000 --- a/hub/templates/admin-config-edit.html +++ /dev/null @@ -1,20 +0,0 @@ -{{define "body" -}} -
+ Create +
+ +{{if .Networks -}} +Name | +Network | +
---|---|
+ + {{.Name}} + + | +{{ipToString .Network}} | +
No networks.
+{{- end}} + +- Configure the peer with the following URL: -
-- {{.HubAddress}}/peer/init/?Code={{.Code}} --
- Done -
-{{- end}} diff --git a/hub/templates/admin-peer-intent.html b/hub/templates/admin-peer-intent.html deleted file mode 100644 index 9a1d05f..0000000 --- a/hub/templates/admin-peer-intent.html +++ /dev/null @@ -1,13 +0,0 @@ -{{define "body" -}} -- Configure the peer with the following URL: -
-- {{.HubAddress}}/peer/create/?Code={{.Code}} --
- Done -
-{{- end}} diff --git a/hub/templates/admin-sign-out.html b/hub/templates/admin-sign-out.html index 812d103..7141fb8 100644 --- a/hub/templates/admin-sign-out.html +++ b/hub/templates/admin-sign-out.html @@ -5,7 +5,7 @@ {{- end}} diff --git a/hub/templates/base.html b/hub/templates/base.html index 5179441..8d73aaf 100644 --- a/hub/templates/base.html +++ b/hub/templates/base.html @@ -10,7 +10,7 @@You must first delete all peers.
+{{- else -}} + +{{- end}} +{{- end}} diff --git a/hub/templates/admin-config.html b/hub/templates/network/network-view.html similarity index 57% rename from hub/templates/admin-config.html rename to hub/templates/network/network-view.html index 2c9cb82..3e97698 100644 --- a/hub/templates/admin-config.html +++ b/hub/templates/network/network-view.html @@ -1,27 +1,20 @@ {{define "body" -}} -- Edit / - Change Password + Delete / + Hosts
Hub Address | -{{.Config.HubAddress}} | -||
VPN Network | -{{ipToString .Config.VPNNetwork}} | +Network | +{{ipToString .Network.Network}}/24 |
-
+
@@ -28,9 +28,8 @@
- Edit / - Delete + Edit / + Delete
{{with .Peer -}}Peer IP | {{.PeerIP}} |
Name | {{.Name}} |
Public IP | {{ipToString .PublicIP}} |
Port | {{.Port}} |
Relay | {{if .Relay}}T{{else}}F{{end}} |
API Key | {{.APIKey}} |
{{.APIKey}}
+