diff --git a/README.md b/README.md index 87e3072..e3739ef 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,8 @@ # vppn: Virtual Pretty Private Network -## Roadmap +## TODO -* Use probe and relayed-probe packets vs ping/pong. -* Rename Mediator -> Relay -* Use default port 456 -* Remove signing key from hub -* Peer: UDP hole-punching -* Peer: local peer discovery - part of RoutingProcessor -* Peer: update hub w/ latest port on startup - -## Learnings - -* Encryption / decryption is 20x faster than signing/opening. -* Allowing out-of order packets is massively important for throughput with TCP - -## Principles - -* Creates an IPv4/24 network with a maximum of 254 peers. (1-254) -* Simple setup: via setup link from the hub. -* Each peer has full network state replicated from the hub. - -## Routing - -* Routing is different for public vs non-public peers - * Public: routes are initialized via incoming ping requests - * NonPub: routes are initialized via incoming ping responses - -A non-public peer needs to maintain connections with every public peer. - -* Sending: - * Public: send to address - * Non-public: send to a mediator - -* Pings: - * Servers don't need to ping - * Clients need to ping all public and local peers to keep connections open +* Add `-force-init` argument to `node` main? ## Hub Server Configuration @@ -106,7 +73,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN Type=simple User=user WorkingDirectory=/home/user/ -ExecStart=/home/user/vppn -name vppn +ExecStart=/home/user/vppn -name vppn -hub-address https://my.hub -api-key 1234567890 Restart=always RestartSec=8 TimeoutStopSec=24 diff --git a/hub/api/api.go b/hub/api/api.go index ec8d77b..801f689 100644 --- a/hub/api/api.go +++ b/hub/api/api.go @@ -1,7 +1,6 @@ package api import ( - "crypto/rand" "database/sql" "embed" "errors" @@ -14,17 +13,14 @@ import ( "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 - initIntents map[string]byte // Map from intent key to peer IP + db *sql.DB + lock sync.Mutex } func New(dbPath string) (*API, error) { @@ -38,8 +34,7 @@ func New(dbPath string) (*API, error) { } a := &API{ - db: sqlDB, - initIntents: map[string]byte{}, + db: sqlDB, } return a, a.ensurePassword() @@ -151,55 +146,13 @@ func (a *API) Peer_CreateNew(p *Peer) error { return db.Peer_Insert(a.db, p) } -// Create the intention to initialize a peer. The returned code is used to -// complete the peer initialization. The code is valid for 5 minutes. -func (a *API) Peer_CreateInitIntent(peerIP byte) string { +func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) (*m.PeerConfig, error) { a.lock.Lock() defer a.lock.Unlock() - code := idgen.NewToken() - a.initIntents[code] = peerIP - - go func() { - time.Sleep(5 * time.Minute) - a.lock.Lock() - defer a.lock.Unlock() - delete(a.initIntents, code) - }() - - return code -} - -func (a *API) Peer_Init(initCode string) (*m.PeerConfig, error) { - a.lock.Lock() - defer a.lock.Unlock() - - ip, ok := a.initIntents[initCode] - if !ok { - return nil, ErrNotAuthorized - } - - peer, err := a.Peer_Get(ip) - if err != nil { - return nil, err - } - - delete(a.initIntents, initCode) - - 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 - } - peer.Version = idgen.NextID(0) - peer.APIKey = idgen.NewToken() - peer.PubKey = encPubKey[:] - peer.PubSignKey = signPubKey[:] + peer.PubKey = args.EncPubKey + peer.PubSignKey = args.PubSignKey if err := db.Peer_UpdateFull(a.db, peer); err != nil { return nil, err @@ -208,17 +161,11 @@ func (a *API) Peer_Init(initCode string) (*m.PeerConfig, error) { conf := a.Config_Get() return &m.PeerConfig{ - PeerIP: peer.PeerIP, - HubAddress: conf.HubAddress, - APIKey: peer.APIKey, - Network: conf.VPNNetwork, - PublicIP: peer.PublicIP, - Port: peer.Port, - Relay: peer.Relay, - PubKey: encPubKey[:], - PrivKey: encPrivKey[:], - PubSignKey: signPubKey[:], - PrivSignKey: signPrivKey[:], + PeerIP: peer.PeerIP, + Network: conf.VPNNetwork, + PublicIP: peer.PublicIP, + Port: peer.Port, + Relay: peer.Relay, }, nil } diff --git a/hub/handler.go b/hub/handler.go index ffaf6fc..f3602b1 100644 --- a/hub/handler.go +++ b/hub/handler.go @@ -65,13 +65,26 @@ func (app *App) handleSignedIn(pattern string, fn handlerFunc) { }) } -type peerHandlerFunc func(w http.ResponseWriter, r *http.Request) error +type peerHandlerFunc func(p *api.Peer, w http.ResponseWriter, r *http.Request) error func (app *App) handlePeer(pattern string, fn peerHandlerFunc) { wrapped := func(w http.ResponseWriter, r *http.Request) { + _, apiKey, ok := r.BasicAuth() + if !ok { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + peer, err := app.api.Peer_GetByAPIKey(apiKey) + if err != nil { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + r.ParseForm() - if err := fn(w, r); err != nil { + if err := fn(peer, w, r); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + return } } diff --git a/hub/handlers.go b/hub/handlers.go index dcd2688..c81c9ad 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -1,6 +1,7 @@ package hub import ( + "encoding/json" "errors" "log" "net/http" @@ -201,22 +202,6 @@ func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *h return a.redirect(w, r, "/admin/peer/view/?PeerIP=%d", p.PeerIP) } -func (a *App) _adminPeerInit(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 - } - code := a.api.Peer_CreateInitIntent(peerIP) - log.Printf("Got code: %v / %v", peerIP, code) - - return a.render("/admin-peer-init.html", w, struct { - Session *api.Session - HubAddress string - Code string - }{s, a.api.Config_Get().HubAddress, code}) -} - 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() @@ -321,9 +306,13 @@ func (a *App) _adminPeerDeleteSubmit(s *api.Session, w http.ResponseWriter, r *h return a.redirect(w, r, "/admin/peer/list/") } -func (a *App) _peerInit(w http.ResponseWriter, r *http.Request) error { - code := r.FormValue("Code") - conf, err := a.api.Peer_Init(code) +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) if err != nil { return err } @@ -331,31 +320,13 @@ func (a *App) _peerInit(w http.ResponseWriter, r *http.Request) error { return a.sendJSON(w, conf) } -func (a *App) _peerFetchState(w http.ResponseWriter, r *http.Request) error { - _, apiKey, ok := r.BasicAuth() - if !ok { - return api.ErrNotAuthorized - } - - peer, err := a.api.Peer_GetByAPIKey(apiKey) - if err != nil { - return err - } - +func (a *App) _peerFetchState(peer *api.Peer, w http.ResponseWriter, r *http.Request) error { peers, err := a.api.Peer_List() if err != nil { return err } - conf := a.api.Config_Get() - - state := m.NetworkState{ - HubAddress: conf.HubAddress, - Network: conf.VPNNetwork, - PeerIP: peer.PeerIP, - PublicIP: peer.PublicIP, - Port: peer.Port, - } + state := m.NetworkState{} for _, p := range peers { if len(p.PubKey) != 0 { diff --git a/hub/routes.go b/hub/routes.go index adf9b58..7d505c5 100644 --- a/hub/routes.go +++ b/hub/routes.go @@ -19,13 +19,12 @@ func (a *App) registerRoutes() { a.handleSignedIn("GET /admin/peer/hosts/", a._adminHosts) a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate) a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit) - a.handleSignedIn("GET /admin/peer/init/", a._adminPeerInit) a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView) a.handleSignedIn("GET /admin/peer/edit/", a._adminPeerEdit) a.handleSignedIn("POST /admin/peer/edit/", a._adminPeerEditSubmit) a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete) a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit) - a.handlePeer("GET /peer/init/", a._peerInit) + a.handlePeer("POST /peer/init/", a._peerInit) a.handlePeer("GET /peer/fetch-state/", a._peerFetchState) } diff --git a/hub/templates/base.html b/hub/templates/base.html index f984646..5179441 100644 --- a/hub/templates/base.html +++ b/hub/templates/base.html @@ -10,6 +10,7 @@

VPPN

diff --git a/m/models.go b/m/models.go index d00ecd6..bf9b73e 100644 --- a/m/models.go +++ b/m/models.go @@ -1,18 +1,17 @@ // The package `m` contains models shared between the hub and peer programs. package m +type PeerInitArgs struct { + EncPubKey []byte + PubSignKey []byte +} + type PeerConfig struct { - PeerIP byte - HubAddress string - Network []byte - APIKey string - PublicIP []byte - Port uint16 - Relay bool - PubKey []byte - PrivKey []byte - PubSignKey []byte - PrivSignKey []byte + PeerIP byte + Network []byte + PublicIP []byte + Port uint16 + Relay bool } type Peer struct { @@ -27,14 +26,5 @@ type Peer struct { } type NetworkState struct { - HubAddress string - - // The requester's data: - Network []byte - PeerIP byte - PublicIP []byte - Port uint16 - - // All peer data. Peers [256]*Peer } diff --git a/node/files.go b/node/files.go index 6f0ec77..18f539b 100644 --- a/node/files.go +++ b/node/files.go @@ -56,7 +56,7 @@ func storeJson(x any, outPath string) error { return os.Rename(tmpPath, outPath) } -func storePeerConfig(netName string, pc m.PeerConfig) error { +func storePeerConfig(netName string, pc localConfig) error { return storeJson(pc, peerConfigPath(netName)) } @@ -73,7 +73,7 @@ func loadJson(dataPath string, ptr any) error { return json.Unmarshal(data, ptr) } -func loadPeerConfig(netName string) (pc m.PeerConfig, err error) { +func loadPeerConfig(netName string) (pc localConfig, err error) { return pc, loadJson(peerConfigPath(netName), &pc) } diff --git a/node/globals.go b/node/globals.go index a6f0e57..44615f8 100644 --- a/node/globals.go +++ b/node/globals.go @@ -3,6 +3,7 @@ package node import ( "net" "net/netip" + "net/url" "sync/atomic" "time" ) @@ -33,6 +34,9 @@ type peerRoute struct { } var ( + hubURL *url.URL + apiKey string + // Configuration for this peer. netName string localIP byte diff --git a/node/hubpoller.go b/node/hubpoller.go index fc9a309..8d51e01 100644 --- a/node/hubpoller.go +++ b/node/hubpoller.go @@ -5,7 +5,6 @@ import ( "io" "log" "net/http" - "net/url" "time" "vppn/m" ) @@ -16,21 +15,18 @@ type hubPoller struct { versions [256]int64 } -func newHubPoller(conf m.PeerConfig) *hubPoller { - u, err := url.Parse(conf.HubAddress) - if err != nil { - log.Fatalf("Failed to parse hub address %s: %v", conf.HubAddress, err) - } +func newHubPoller() *hubPoller { + u := *hubURL u.Path = "/peer/fetch-state/" client := &http.Client{Timeout: 8 * time.Second} req := &http.Request{ Method: http.MethodGet, - URL: u, + URL: &u, Header: http.Header{}, } - req.SetBasicAuth("", conf.APIKey) + req.SetBasicAuth("", apiKey) return &hubPoller{ client: client, @@ -71,7 +67,7 @@ func (hp *hubPoller) pollHub() { } if err := json.Unmarshal(body, &state); err != nil { - log.Printf("Failed to unmarshal response from hub: %v", err) + log.Printf("Failed to unmarshal response from hub: %v\n%s", err, body) return } diff --git a/node/main.go b/node/main.go index 46b16d7..4a4bc41 100644 --- a/node/main.go +++ b/node/main.go @@ -1,6 +1,8 @@ package node import ( + "bytes" + "crypto/rand" "encoding/json" "flag" "fmt" @@ -9,10 +11,14 @@ import ( "net" "net/http" "net/netip" + "net/url" "os" "runtime/debug" "time" "vppn/m" + + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/sign" ) func panicHandler() { @@ -24,33 +30,61 @@ func panicHandler() { func Main() { defer panicHandler() - var ( - initURL string - listenIP string - ) + var hubAddress string flag.StringVar(&netName, "name", "", "[REQUIRED] The network name.") - flag.StringVar(&initURL, "init-url", "", "Initializes peer from the hub URL.") - flag.StringVar(&listenIP, "listen-ip", "", "IP address to listen on.") + flag.StringVar(&hubAddress, "hub-address", "", "[REQUIRED] The hub address.") + flag.StringVar(&apiKey, "api-key", "", "[REQUIRED] The node's API key.") flag.Parse() - if netName == "" { + if netName == "" || hubAddress == "" || apiKey == "" { flag.Usage() os.Exit(1) } - if initURL != "" { - mainInit(initURL) - return + var err error + + hubURL, err = url.Parse(hubAddress) + if err != nil { + log.Fatalf("Failed to parse hub address: %v", err) } - main(listenIP) + main() } -func mainInit(initURL string) { - resp, err := http.Get(initURL) +func initPeerWithHub() { + encPubKey, encPrivKey, err := box.GenerateKey(rand.Reader) if err != nil { - log.Fatalf("Failed to fetch data from hub: %v", err) + log.Fatalf("Failed to generate encryption keys: %v", err) + } + + signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader) + if err != nil { + log.Fatalf("Failed to generate signing keys: %v", err) + } + + initURL := *hubURL + initURL.Path = "/peer/init/" + + args := m.PeerInitArgs{ + EncPubKey: encPubKey[:], + PubSignKey: signPubKey[:], + } + + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(args); err != nil { + log.Fatalf("Failed to encode init args: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, initURL.String(), buf) + if err != nil { + log.Fatalf("Failed to construct request: %v", err) + } + req.SetBasicAuth("", apiKey) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Failed to init with hub: %v", err) } defer resp.Body.Close() @@ -59,11 +93,16 @@ func mainInit(initURL string) { log.Fatalf("Failed to read response body: %v", err) } - peerConfig := m.PeerConfig{} - if err := json.Unmarshal(data, &peerConfig); err != nil { - log.Fatalf("Failed to parse configuration: %v", err) + peerConfig := localConfig{} + if err := json.Unmarshal(data, &peerConfig.PeerConfig); err != nil { + log.Fatalf("Failed to parse configuration: %v\n%s", err, data) } + peerConfig.PubKey = encPubKey[:] + peerConfig.PrivKey = encPrivKey[:] + peerConfig.PubSignKey = signPubKey[:] + peerConfig.PrivSignKey = signPrivKey[:] + if err := storePeerConfig(netName, peerConfig); err != nil { log.Fatalf("Failed to store configuration: %v", err) } @@ -73,10 +112,17 @@ func mainInit(initURL string) { // ---------------------------------------------------------------------------- -func main(listenIP string) { +func main() { config, err := loadPeerConfig(netName) if err != nil { - log.Fatalf("Failed to load configuration: %v", err) + log.Printf("Failed to load configuration: %v", err) + log.Printf("Initializing...") + initPeerWithHub() + + config, err = loadPeerConfig(netName) + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } } iface, err := openInterface(config.Network, config.PeerIP, netName) @@ -84,7 +130,7 @@ func main(listenIP string) { log.Fatalf("Failed to open interface: %v", err) } - myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", listenIP, config.Port)) + myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", config.Port)) if err != nil { log.Fatalf("Failed to resolve UDP address: %v", err) } @@ -137,7 +183,7 @@ func main(listenIP string) { } }() - go newHubPoller(config).Run() + go newHubPoller().Run() go readFromConn(conn) readFromIFace(iface) } @@ -258,7 +304,7 @@ func handleDataPacket(h header, data []byte, decBuf []byte) { destRoute := routingTable[h.DestIP].Load() if !destRoute.Up { - log.Printf("Not connected (relay): %v", destRoute) + log.Printf("Not connected (relay): %d", destRoute.IP) return } diff --git a/node/peer-supervisor.go b/node/peer-supervisor.go index 5363ac2..6f10761 100644 --- a/node/peer-supervisor.go +++ b/node/peer-supervisor.go @@ -214,7 +214,7 @@ func (s *peerSupervisor) server() stateFunc { s.sendControlPacketTo(probePacket{TraceID: msg.Packet.TraceID}, msg.SrcAddr) case pingTimerMsg: - if time.Since(lastSeen) > timeoutInterval { + if time.Since(lastSeen) > timeoutInterval && s.staged.Up { logf("Connection timeout") s.staged.Up = false s.publish()