23 Commits

Author SHA1 Message Date
jdl
b972784d90 WIP 2026-06-05 19:43:27 +02:00
jdl
8b2c9709fc WIP 2026-06-04 16:00:21 +02:00
jdl
5ae075647d Bug fixes 2025-09-25 10:03:37 +02:00
jdl
29bbb442c8 Cleanup 2025-09-25 09:08:10 +02:00
3d93c0206c client-interface-cleanup (#6)
Refactoring and code cleanup. Improved client command interface.
2025-09-17 08:00:12 +00:00
jdl
75c7c2d3d9 README cleanup. 2025-09-04 11:29:26 +02:00
jdl
c61319ed16 Cleanup 2025-09-03 20:41:35 +02:00
jdl
0a7328ed5f Refactor 2025-09-01 18:15:42 +02:00
jdl
5f0b00ff46 Refactor 2025-09-01 18:09:24 +02:00
jdl
e91cbfe957 Merge branch 'refactor-2025' 2025-09-01 18:05:42 +02:00
jdl
7c476fc332 WIP: Cleanup 2025-09-01 18:03:09 +02:00
jdl
69823d1d19 WIP 2025-09-01 18:00:41 +02:00
jdl
b7cb4e20f0 WIP 2025-08-26 19:50:59 +02:00
jdl
6382c13d1a WIP 2025-08-26 19:12:07 +02:00
jdl
1ca55158c2 wip 2025-08-26 17:01:38 +02:00
jdl
302d27692b WIP: Cleanup 2025-08-26 16:57:46 +02:00
jdl
31c48fbafd Cleanup 2025-08-26 16:20:47 +02:00
jdl
3c4534f620 WIP: Fixed rate limiting 2025-08-26 16:17:46 +02:00
jdl
169231d848 WIP: Apparently working? 2025-08-26 16:11:21 +02:00
jdl
f4589a1031 Don't crash 2025-08-26 15:45:06 +02:00
jdl
ab246b2a90 WIP 2025-08-26 15:33:27 +02:00
b9e773ec83 Update - modify hub to support multiple networks. (#4)
Co-authored-by: jdl <jdl@desktop>
Reviewed-on: #4
2025-04-12 11:43:18 +00:00
jdl
d558ebbd14 WIP 2025-04-06 07:51:47 +02:00
100 changed files with 2220 additions and 5385 deletions

View File

@@ -1,5 +1,11 @@
# vppn: Virtual Potentially Private Network
## TO DO
* peer - write status to file instead of using sockets
* peer - improve relay selection
* Double buffering in IFReader and ConnReader ?
## Hub Server Configuration
```
@@ -9,7 +15,6 @@ adduser user
# Enable ssh.
cp -r ~/.ssh /home/user/
chown -R user:user /home/user/.ssh
```
Upload `hub` executable:
@@ -38,6 +43,7 @@ Add and start the hub server:
```
systemctl daemon-reload
systemctl enable hub
systemctl start hub
```
@@ -55,17 +61,25 @@ Install the binary somewhere, for example `~/bin/vppn`.
Create systemd file in `/etc/systemd/system/vppn.service`.
```
[Service]
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 run my_net_name https://my.hub my_api_key
Restart=always
RestartSec=8
TimeoutStopSec=24
[Install]
WantedBy=default.target
WantedBy=multi-user.target
```
Add and start the service:
```
systemctl daemon-reload
systemctl enable vppn
systemctl start vppn
```

View File

@@ -7,5 +7,5 @@ import (
func main() {
log.SetFlags(0)
peer.Main()
peer.Main2()
}

24
go.mod
View File

@@ -1,15 +1,25 @@
module vppn
go 1.24.1
go 1.25.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.1
golang.org/x/crypto v0.42.0
golang.org/x/sys v0.36.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
github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect
)

46
go.sum
View File

@@ -1,12 +1,34 @@
git.crumpington.com/lib/go v0.8.1 h1:rWjddllSxQ4yReraqDaGZAod4NpRD9LtGx1yV71ytcU=
git.crumpington.com/lib/go v0.8.1/go.mod h1:XjQaf2NFlje9BJ1EevZL8NNioPrAe7WwHpKUhcDw2Lk=
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=
git.crumpington.com/lib/go v0.9.1 h1:xLBzcgiZRB6Ky3Ce9hKE+Ko0YbkA4USF4eJk5i5RJF4=
git.crumpington.com/lib/go v0.9.1/go.mod h1:5nnfjdnUnj/FHhakaliKQKsKeSkUb0GEUKF3PqRgUXg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=

View File

@@ -21,6 +21,8 @@ var migrations embed.FS
type API struct {
db *sql.DB
lock sync.Mutex
sessionsMu sync.Mutex
sessions map[string]*Session
}
func New(dbPath string) (*API, error) {
@@ -35,6 +37,7 @@ func New(dbPath string) (*API, error) {
a := &API{
db: sqlDB,
sessions: make(map[string]*Session),
}
return a, a.ensurePassword()
@@ -54,17 +57,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,52 +77,42 @@ 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)
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
delete(a.sessions, sessionID)
return nil
}
func (a *API) Session_Get(sessionID string) (*Session, error) {
if sessionID == "" {
return a.session_CreatePub()
}
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
session, err := db.Session_Get(a.db, sessionID)
if err != nil {
return a.session_CreatePub()
if sessionID != "" {
s, ok := a.sessions[sessionID]
if ok {
if timeSince(s.LastSeenAt) <= 86400*21 {
if timeSince(s.LastSeenAt) > 86400*7 {
s.LastSeenAt = time.Now().Unix()
}
if timeSince(session.LastSeenAt) > 86400*21 {
return a.session_CreatePub()
return s, nil
}
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)
delete(a.sessions, sessionID)
}
}
return session, nil
return a.session_Create(), nil
}
func (a *API) session_CreatePub() (*Session, error) {
// caller must hold sessionsMu
func (a *API) session_Create() *Session {
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)
a.sessions[s.SessionID] = s
return s
}
func (a *API) Session_SignIn(s *Session, pwd string) error {
@@ -133,40 +120,46 @@ func (a *API) Session_SignIn(s *Session, pwd string) error {
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized
}
a.sessionsMu.Lock()
s.SignedIn = true
a.sessionsMu.Unlock()
return nil
}
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{}
p.PubSignKey = []byte{}
p.WGPubKey = []byte{}
p.APIKey = idgen.NewToken()
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()
peer.Version = idgen.NextID(0)
peer.PubKey = args.EncPubKey
peer.PubSignKey = args.PubSignKey
peer.WGPubKey = args.WGPubKey
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 +170,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) {

View File

View File

@@ -17,12 +17,10 @@ type TX interface {
type Config struct {
ConfigID int64
HubAddress string
VPNNetwork []byte
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,10 @@ 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)
if err = r.Scan(&row.ConfigID, &row.Password); err != nil {
row = nil
}
return
}
@@ -139,7 +139,9 @@ func Config_GetWhere(
) {
row = &Config{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
if err = r.Scan(&row.ConfigID, &row.Password); err != nil {
row = nil
}
return
}
@@ -159,7 +161,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
}
@@ -185,37 +187,63 @@ func Config_List(
}
// ----------------------------------------------------------------------------
// Table: sessions
// Table: networks
// ----------------------------------------------------------------------------
type Session struct {
SessionID string
CSRF string
SignedIn bool
CreatedAt int64
LastSeenAt int64
type Network struct {
NetworkID int64
Name string
Network []byte
}
const Session_SelectQuery = "SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions"
const Network_SelectQuery = "SELECT NetworkID,Name,Network FROM networks"
func Session_Insert(
func Network_Insert(
tx TX,
row *Session,
row *Network,
) (err error) {
Session_Sanitize(row)
if err = Session_Validate(row); err != nil {
Network_Sanitize(row)
if err = Network_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)
_, err = tx.Exec("INSERT INTO networks(NetworkID,Name,Network) VALUES(?,?,?)", row.NetworkID, row.Name, row.Network)
return err
}
func Session_Delete(
func Network_UpdateFull(
tx TX,
SessionID string,
row *Network,
) (err error) {
result, err := tx.Exec("DELETE FROM sessions WHERE SessionID=?", SessionID)
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
}
@@ -234,50 +262,54 @@ func Session_Delete(
}
}
func Session_Get(
func Network_Get(
tx TX,
SessionID string,
NetworkID int64,
) (
row *Session,
row *Network,
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)
row = &Network{}
r := tx.QueryRow("SELECT NetworkID,Name,Network FROM networks WHERE NetworkID=?", NetworkID)
if err = r.Scan(&row.NetworkID, &row.Name, &row.Network); err != nil {
row = nil
}
return
}
func Session_GetWhere(
func Network_GetWhere(
tx TX,
query string,
args ...any,
) (
row *Session,
row *Network,
err error,
) {
row = &Session{}
row = &Network{}
r := tx.QueryRow(query, args...)
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
if err = r.Scan(&row.NetworkID, &row.Name, &row.Network); err != nil {
row = nil
}
return
}
func Session_Iterate(
func Network_Iterate(
tx TX,
query string,
args ...any,
) iter.Seq2[*Session, error] {
) iter.Seq2[*Network, error] {
rows, err := tx.Query(query, args...)
if err != nil {
return func(yield func(*Session, error) bool) {
return func(yield func(*Network, error) bool) {
yield(nil, err)
}
}
return func(yield func(*Session, error) bool) {
return func(yield func(*Network, error) bool) {
defer rows.Close()
for rows.Next() {
row := &Session{}
err := rows.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
row := &Network{}
err := rows.Scan(&row.NetworkID, &row.Name, &row.Network)
if !yield(row, err) {
return
}
@@ -285,15 +317,15 @@ func Session_Iterate(
}
}
func Session_List(
func Network_List(
tx TX,
query string,
args ...any,
) (
l []*Session,
l []*Network,
err error,
) {
for row, err := range Session_Iterate(tx, query, args...) {
for row, err := range Network_Iterate(tx, query, args...) {
if err != nil {
return nil, err
}
@@ -307,18 +339,20 @@ func Session_List(
// ----------------------------------------------------------------------------
type Peer struct {
NetworkID int64
PeerIP byte
Version int64
APIKey string
Name string
PublicIP []byte
Port uint16
PublicIP1 []byte
Port1 uint16
PublicIP2 []byte
Port2 uint16
Relay bool
PubKey []byte
PubSignKey []byte
WGPubKey []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,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers"
func Peer_Insert(
tx TX,
@@ -329,7 +363,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,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.WGPubKey)
return err
}
@@ -342,7 +376,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=?,PublicIP1=?,Port1=?,PublicIP2=?,Port2=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.NetworkID, row.PeerIP)
if err != nil {
return err
}
@@ -370,7 +404,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=?,PublicIP1=?,Port1=?,PublicIP2=?,Port2=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.PublicIP1, row.Port1, row.PublicIP2, row.Port2, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP)
if err != nil {
return err
}
@@ -391,9 +425,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 +449,17 @@ 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,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP)
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey); err != nil {
row = nil
}
return
}
@@ -435,7 +473,9 @@ 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)
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey); err != nil {
row = nil
}
return
}
@@ -455,7 +495,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.PublicIP1, &row.Port1, &row.PublicIP2, &row.Port2, &row.Relay, &row.WGPubKey)
if !yield(row, err) {
return
}

View File

@@ -3,67 +3,110 @@ package db
import (
"errors"
"net/netip"
"net/url"
"strings"
)
var (
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
return nil
}
addr, ok := netip.AddrFromSlice(c.VPNNetwork)
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
}
return nil
if !addr.IsPrivate() {
return ErrNonPrivateIP
}
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.PublicIP) != 0 {
addr, ok := netip.AddrFromSlice(p.PublicIP)
if ok && addr.Is4() {
p.PublicIP = addr.AsSlice()
if len(p.PublicIP1) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok {
p.PublicIP1 = addr.AsSlice()
}
}
if p.Port == 0 {
p.Port = 456
if len(p.PublicIP2) != 0 {
if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok {
p.PublicIP2 = addr.AsSlice()
}
}
if p.Port1 == 0 {
p.Port1 = 456
}
if len(p.PublicIP2) != 0 && p.Port2 == 0 {
p.Port2 = 456
}
}
func Peer_Validate(p *Peer) error {
if len(p.PublicIP) > 0 {
_, ok := netip.AddrFromSlice(p.PublicIP)
if !ok {
if len(p.PublicIP1) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok {
return ErrInvalidIP
}
}
if p.Port == 0 {
if len(p.PublicIP2) > 0 {
if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok {
return ErrInvalidIP
}
if p.Port2 == 0 {
return ErrInvalidPort
}
}
if p.Port1 == 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
}

View File

@@ -1,26 +1,24 @@
TABLE config OF Config (
ConfigID int64 PK,
HubAddress string,
VPNNetwork []byte,
Password []byte NoUpdate
Password []byte
);
TABLE sessions OF Session NoUpdate (
SessionID string PK,
CSRF string,
SignedIn bool,
CreatedAt int64,
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,
Name string,
PublicIP []byte,
Port uint16,
PublicIP1 []byte,
Port1 uint16,
PublicIP2 []byte,
Port2 uint16,
Relay bool,
PubKey []byte NoUpdate,
PubSignKey []byte NoUpdate
WGPubKey []byte NoUpdate
);

View File

@@ -1,41 +1,8 @@
package db
import "time"
func Session_UpdateLastSeenAt(
tx TX,
id string,
) (err error) {
_, err = tx.Exec("UPDATE sessions SET LastSeenAt=? WHERE SessionID=?", time.Now().Unix(), 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_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 +12,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
}

View File

@@ -1,27 +1,25 @@
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
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 INDEX sessions_last_seen_index ON sessions(LastSeenAt);
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
PublicIP1 BLOB NOT NULL,
Port1 INTEGER NOT NULL,
PublicIP2 BLOB NOT NULL,
Port2 INTEGER NOT NULL,
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
WGPubKey BLOB NOT NULL,
PRIMARY KEY(NetworkID, PeerIP)
) WITHOUT ROWID;

View File

@@ -1 +0,0 @@
ALTER TABLE peers ADD COLUMN PubSignKey BLOB NOT NULL DEFAULT '';

View File

@@ -3,5 +3,12 @@ package api
import "vppn/hub/api/db"
type Config = db.Config
type Session = db.Session
type Network = db.Network
type Peer = db.Peer
type Session struct {
SessionID string
SignedIn bool
CreatedAt int64
LastSeenAt int64
}

View File

@@ -2,6 +2,7 @@ package hub
import (
"embed"
"encoding/base64"
"html/template"
"net/http"
"path/filepath"
@@ -47,6 +48,19 @@ func NewApp(conf Config) (*App, error) {
return app, nil
}
func (app *App) Handler() http.Handler {
cop := http.NewCrossOriginProtection()
return cop.Handler(app.mux)
}
var templateFuncs = template.FuncMap{
"ipToString": ipBytesTostring,
"wgKeyString": wgKeyString,
}
func wgKeyString(key []byte) string {
if len(key) == 0 {
return "not set"
}
return base64.StdEncoding.EncodeToString(key)
}

42
hub/form.go Normal file
View File

@@ -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
}

View File

@@ -26,11 +26,6 @@ func (app *App) handlePub(pattern string, fn handlerFunc) {
if r.Method == http.MethodPost {
r.ParseMultipartForm(64 * 1024)
if r.FormValue("CSRF") != s.CSRF {
log.Printf("%s != %s", r.FormValue("CSRF"), s.CSRF)
http.Error(w, "CSRF mismatch", http.StatusBadRequest)
return
}
} else {
r.ParseForm()
}

View File

@@ -5,8 +5,6 @@ import (
"errors"
"log"
"net/http"
"net/netip"
"strings"
"vppn/hub/api"
"vppn/m"
@@ -16,7 +14,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 +52,208 @@ 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 {
return a.render("/admin-network-list.html", w, struct {
Session *api.Session
Peers []*api.Peer
Config *api.Config
}{
s,
peers,
a.api.Config_Get(),
})
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 ip1Str, ip2Str string
p := &api.Peer{}
err := webutil.NewFormScanner(r.Form).
Scan("NetworkID", &p.NetworkID).
Scan("IP", &p.PeerIP).
Scan("Name", &p.Name).
Scan("PublicIP1", &ip1Str).
Scan("Port1", &p.Port1).
Scan("PublicIP2", &ip2Str).
Scan("Port2", &p.Port2).
Scan("Relay", &p.Relay).
Error()
if err != nil {
return err
}
if p.PublicIP1, err = stringToIP(ip1Str); err != nil {
return err
}
if p.PublicIP2, err = stringToIP(ip2Str); 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 ip1Str, ip2Str string
err = webutil.NewFormScanner(r.Form).
Scan("Name", &peer.Name).
Scan("PublicIP1", &ip1Str).
Scan("Port1", &peer.Port1).
Scan("PublicIP2", &ip2Str).
Scan("Port2", &peer.Port2).
Scan("Relay", &peer.Relay).
Error()
if err != nil {
return err
}
if peer.PublicIP1, err = stringToIP(ip1Str); err != nil {
return err
}
if peer.PublicIP2, err = stringToIP(ip2Str); 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) _adminPasswordEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
@@ -143,205 +295,78 @@ 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()))
func (a *App) _peerInit(peer *api.Peer, w http.ResponseWriter, r *http.Request) error {
if len(peer.WGPubKey) != 0 {
http.Error(w, "Already initialized", http.StatusConflict)
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 {
if len(p.PubKey) != 0 {
state.Peers[p.PeerIP] = &m.Peer{
for _, p := range l {
if len(p.WGPubKey) != 0 {
peers[p.PeerIP] = &m.Peer{
PeerIP: p.PeerIP,
Version: p.Version,
Name: p.Name,
PublicIP: p.PublicIP,
Port: p.Port,
PublicIP1: p.PublicIP1,
Port1: p.Port1,
PublicIP2: p.PublicIP2,
Port2: p.Port2,
Relay: p.Relay,
PubKey: p.PubKey,
PubSignKey: p.PubSignKey,
WGPubKey: p.WGPubKey,
}
}
}
return a.sendJSON(w, state)
return
}

View File

@@ -31,7 +31,7 @@ func Main() {
srv := &http.Server{
Addr: conf.ListenAddr,
Handler: app.mux,
Handler: app.Handler(),
}
log.Fatal(webutil.ListenAndServe(srv))

View File

@@ -9,14 +9,16 @@ 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/peer/create/", a._adminPeerCreate)
a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit)
a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView)
@@ -25,6 +27,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)
}

View File

@@ -1,20 +0,0 @@
{{define "body" -}}
<h2>Config</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Hub Address</label><br>
<input type="url" name="HubAddress" value="{{.Config.HubAddress}}">
</p>
<p>
<label>VPN Network</label><br>
<input type="text" name="VPNNetwork" value="{{ipToString .Config.VPNNetwork}}">
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/config/">Cancel</a>
</p>
</form>
{{- end}}

View File

@@ -1,57 +0,0 @@
{{define "body" -}}
<h2>Config</h2>
<p>
<a href="/admin/config/edit/">Edit</a> /
<a href="/admin/password/edit/">Change Password</a>
</p>
<table class="def-list">
<tr>
<td>Hub Address</td>
<td>{{.Config.HubAddress}}</td>
</tr>
<tr>
<td>VPN Network</td>
<td>{{ipToString .Config.VPNNetwork}}</td>
</tr>
</table>
<h2>Peers</h2>
<p>
<a href="/admin/peer/create/">Add Peer</a> /
<a href="/admin/peer/hosts/">Hosts</a>
</p>
{{if .Peers -}}
<table>
<thead>
<tr>
<th>PeerIP</th>
<th>Name</th>
<th>Public IP</th>
<th>Port</th>
<th>Relay</th>
</tr>
</thead>
<tbody>
{{range .Peers -}}
<tr>
<td>
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">
{{.PeerIP}}
</a>
</td>
<td>{{.Name}}</td>
<td>{{ipToString .PublicIP}}</td>
<td>{{.Port}}</td>
<td>{{if .Relay}}T{{else}}F{{end}}</td>
</tr>
</tbody>
{{- end}}
</table>
{{- else}}
<p>No peers.</p>
{{- end}}
{{- end}}

View File

@@ -0,0 +1,18 @@
{{define "body" -}}
<h2>Create Network</h2>
<form method="POST">
<p>
<label>Name</label><br>
<input type="text" name="Name">
</p>
<p>
<label>Network /24</label><br>
<input type="text" name="Network">
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/network/list/">Cancel</a>
</p>
</form>
{{- end}}

View File

@@ -0,0 +1,38 @@
{{define "body" -}}
<h2>Networks</h2>
<p>
<a href="/admin/network/create/">Create</a>
</p>
{{if .Networks -}}
<table>
<thead>
<tr>
<th>Name</th>
<th>Network</th>
</tr>
</thead>
<tbody>
{{range .Networks -}}
<tr>
<td>
<a href="/admin/network/view/?NetworkID={{.NetworkID}}">
{{.Name}}
</a>
</td>
<td>{{ipToString .Network}}</td>
</tr>
</tbody>
{{- end}}
</table>
{{- else}}
<p>No networks.</p>
{{- end}}
<h3>Settings</h3>
<ul>
<li><a href="/admin/password/edit/">Password</a></li>
</ul>
{{- end}}

View File

@@ -2,7 +2,6 @@
<h2>Change Password</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Current Password</label><br>
<input type="password" name="CurrentPassword">

View File

@@ -1,34 +0,0 @@
{{define "body" -}}
<h2>New Peer</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>IP</label><br>
<input type="number" name="IP" min="1" max="255" value="0">
</p>
<p>
<label>Name</label><br>
<input type="text" name="Name">
</p>
<p>
<label>Public IP</label><br>
<input type="text" name="PublicIP">
</p>
<p>
<label>Port</label><br>
<input type="number" name="Port" value="456">
</p>
<p>
<label>
<input type="checkbox" name="Relay">
Relay
</label>
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/config/">Cancel</a>
</p>
</form>
{{- end}}

View File

@@ -1,36 +0,0 @@
{{define "body" -}}
<h2>Delete Peer</h2>
{{with .Peer -}}
<form method="POST">
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
<p>
<label>Peer IP</label><br>
<input type="number" name="PeerIP" value="{{.PeerIP}}" disabled>
</p>
<p>
<label>Name</label><br>
<input type="text" value="{{.Name}}" disabled>
</p>
<p>
<label>Public IP</label><br>
<input type="text" value="{{ipToString .PublicIP}}" disabled>
</p>
<p>
<label>Port</label><br>
<input type="number" value="{{.Port}}" disabled>
</p>
<p>
<label>
<input type="checkbox" {{if .Relay}}checked{{end}} disabled>
Relay
</label>
</p>
<p>
<button type="submit">Delete</button>
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -1,36 +0,0 @@
{{define "body" -}}
<h2>Edit Peer</h2>
{{with .Peer -}}
<form method="POST">
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
<p>
<label>Peer IP</label><br>
<input type="number" name="PeerIP" value="{{.PeerIP}}" disabled>
</p>
<p>
<label>Name</label><br>
<input type="text" name="Name" value="{{.Name}}">
</p>
<p>
<label>Public IP</label><br>
<input type="text" name="PublicIP" value="{{ipToString .PublicIP}}">
</p>
<p>
<label>Port</label><br>
<input type="number" name="Port" value="{{.Port}}">
</p>
<p>
<label>
<input type="checkbox" name="Relay" {{if .Relay}}checked{{end}}>
Relay
</label>
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -1,13 +0,0 @@
{{define "body" -}}
<h2>Initialize Peer</h2>
<p>
Configure the peer with the following URL:
</p>
<pre>
{{.HubAddress}}/peer/init/?Code={{.Code}}
</pre>
<p>
<a href="/admin/config/">Done</a>
</p>
{{- end}}

View File

@@ -1,13 +0,0 @@
{{define "body" -}}
<h2>Create Peer</h2>
<p>
Configure the peer with the following URL:
</p>
<pre>
{{.HubAddress}}/peer/create/?Code={{.Code}}
</pre>
<p>
<a href="/admin/config/">Done</a>
</p>
{{- end}}

View File

@@ -1,20 +0,0 @@
{{define "body" -}}
<h2>Peer</h2>
<p>
<a href="/admin/peer/edit/?PeerIP={{.Peer.PeerIP}}">Edit</a> /
<a href="/admin/peer/delete/?PeerIP={{.Peer.PeerIP}}">Delete</a>
</p>
{{with .Peer -}}
<table class="def-list">
<tr><td>Peer IP</td><td>{{.PeerIP}}</td></tr>
<tr><td>Name</td><td>{{.Name}}</td></tr>
<tr><td>Public IP</td><td>{{ipToString .PublicIP}}</td></tr>
<tr><td>Port</td><td>{{.Port}}</td></tr>
<tr><td>Relay</td><td>{{if .Relay}}T{{else}}F{{end}}</td></tr>
<tr><td>API Key</td><td>{{.APIKey}}</td></tr>
</table>
{{- end}}
{{- end}}

View File

@@ -2,10 +2,9 @@
<h2>Sign Out</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<button type="submit">Sign Out</button>
<a href="/admin/config/">Cancel</a>
<a href="/">Cancel</a>
</p>
</form>
{{- end}}

View File

@@ -10,7 +10,7 @@
<h1>VPPN</h1>
<nav>
{{if .Session.SignedIn -}}
<a href="/admin/config/">Home</a> /
<a href="/admin/networks/list/">Home</a> /
<a href="/admin/sign-out/">Sign out</a>
{{- end}}
</nav>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>VPPN Hub</title>
<link rel="stylesheet" href="/static/new.min.css">
<link rel="stylesheet" href="/static/custom.css">
</head>
<body>
<header>
<h1>VPPN</h1>
<nav>
{{if .Session.SignedIn -}}
<a href="/admin/networks/list/">Home</a> /
<a href="/admin/sign-out/">Sign out</a>
{{- end}}
</nav>
</header>
<h2>
Network:
<a href="/admin/network/view/?NetworkID={{.Network.NetworkID}}">{{.Network.Name}}</a>
</h2>
{{block "body" .}}There's nothing here.{{end}}
</body>
</html>

View File

@@ -0,0 +1,15 @@
{{define "body" -}}
<h3>Delete</h3>
{{if .Peers -}}
<p>You must first delete all peers.</p>
{{- else -}}
<form method="POST">
<input type="hidden" name="NetworkID" value="{{.Network.NetworkID}}">
<p>
<button type="submit">Delete</button>
<a href="/admin/network/view/?NetworkID={{.Network.NetworkID}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -0,0 +1,53 @@
{{define "body" -}}
<p>
<a href="/admin/network/delete/?NetworkID={{.Network.NetworkID}}">Delete</a>
</p>
<table class="def-list">
<tr>
<td>Network</td>
<td>{{ipToString .Network.Network}}/24</td>
</tr>
</table>
<h3>Peers</h3>
<p>
<a href="/admin/peer/create/?NetworkID={{.Network.NetworkID}}">Create</a>
</p>
{{if .Peers -}}
<table>
<thead>
<tr>
<th>PeerIP</th>
<th>Name</th>
<th>Public IP 1</th>
<th>Port 1</th>
<th>Public IP 2</th>
<th>Port 2</th>
<th>Relay</th>
</tr>
</thead>
<tbody>
{{range .Peers -}}
<tr>
<td>
<a href="/admin/peer/view/?NetworkID={{$.Network.NetworkID}}&PeerIP={{.PeerIP}}">
{{.PeerIP}}
</a>
</td>
<td>{{.Name}}</td>
<td>{{ipToString .PublicIP1}}</td>
<td>{{.Port1}}</td>
<td>{{ipToString .PublicIP2}}</td>
<td>{{.Port2}}</td>
<td>{{if .Relay}}T{{else}}F{{end}}</td>
</tr>
</tbody>
{{- end}}
</table>
{{- else}}
<p>No peers.</p>
{{- end}}
{{- end}}

View File

@@ -0,0 +1,42 @@
{{define "body" -}}
<h3>New Peer</h3>
<form method="POST">
<input type="hidden" name="NetworkID" value="{{.Network.NetworkID}}">
<p>
<label>IP</label><br>
<input type="number" name="IP" min="1" max="255" value="0">
</p>
<p>
<label>Name</label><br>
<input type="text" name="Name">
</p>
<p>
<label>Public IP 1</label><br>
<input type="text" name="PublicIP1">
</p>
<p>
<label>Port 1</label><br>
<input type="number" name="Port1" value="456">
</p>
<p>
<label>Public IP 2 (optional)</label><br>
<input type="text" name="PublicIP2">
</p>
<p>
<label>Port 2</label><br>
<input type="number" name="Port2" value="0">
</p>
<p>
<label>
<input type="checkbox" name="Relay">
Relay
</label>
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/network/view/?NetworkID={{.Network.NetworkID}}">Cancel</a>
</p>
</form>
{{- end}}

View File

@@ -0,0 +1,14 @@
{{define "body" -}}
<h3>Delete {{.Peer.Name}}</h3>
{{with .Peer -}}
<form method="POST">
<input type="hidden" name="NetworkID" value="{{.NetworkID}}">
<input type="hidden" name="NetworkID" value="{{.PeerIP}}">
<p>
<button type="submit">Delete</button>
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}&NetworkID={{.NetworkID}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -0,0 +1,42 @@
{{define "body" -}}
<h2>Edit Peer</h2>
{{with .Peer -}}
<form method="POST">
<p>
<label>Peer IP</label><br>
<input type="text" value="{{.PeerIP}}" disabled>
</p>
<p>
<label>Name</label><br>
<input type="text" name="Name" value="{{.Name}}">
</p>
<p>
<label>Public IP 1</label><br>
<input type="text" name="PublicIP1" value="{{ipToString .PublicIP1}}">
</p>
<p>
<label>Port 1</label><br>
<input type="number" name="Port1" value="{{.Port1}}">
</p>
<p>
<label>Public IP 2 (optional)</label><br>
<input type="text" name="PublicIP2" value="{{ipToString .PublicIP2}}">
</p>
<p>
<label>Port 2</label><br>
<input type="number" name="Port2" value="{{.Port2}}">
</p>
<p>
<label>
<input type="checkbox" name="Relay" {{if .Relay}}checked{{end}}>
Relay
</label>
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/peer/view/?NetworkID={{$.Network.NetworkID}}&PeerIP={{.PeerIP}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -0,0 +1,26 @@
{{define "body" -}}
<h3>{{.Peer.Name}}</h3>
<p>
<a href="/admin/peer/edit/?NetworkID={{.Network.NetworkID}}&PeerIP={{.Peer.PeerIP}}">Edit</a> /
<a href="/admin/peer/delete/?NetworkID={{.Network.NetworkID}}&PeerIP={{.Peer.PeerIP}}">Delete</a>
</p>
{{with .Peer -}}
<table class="def-list">
<tr><td>Peer IP</td><td>{{.PeerIP}}</td></tr>
<tr><td>Public IP 1</td><td>{{ipToString .PublicIP1}}</td></tr>
<tr><td>Port 1</td><td>{{.Port1}}</td></tr>
<tr><td>Public IP 2</td><td>{{ipToString .PublicIP2}}</td></tr>
<tr><td>Port 2</td><td>{{.Port2}}</td></tr>
<tr><td>Relay</td><td>{{if .Relay}}T{{else}}F{{end}}</td></tr>
<tr><td>WG Public Key</td><td>{{wgKeyString .WGPubKey}}</td></tr>
</table>
<details>
<summary>API Key</summary>
<p>{{.APIKey}}</p>
</details>
{{- end}}
{{- end}}

View File

@@ -2,7 +2,6 @@
<h2>Sign In</h2>
<form method="POST">
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
<p>
<label>Password</label><br>
<input type="password" name="Password">

View File

@@ -2,27 +2,25 @@
package m
type PeerInitArgs struct {
EncPubKey []byte
PubSignKey []byte
WGPubKey []byte
}
type PeerConfig struct {
type PeerInitResp struct {
PeerIP byte
Network []byte
PublicIP []byte
Port uint16
Relay bool
NetworkState NetworkState
}
type Peer struct {
PeerIP byte
Version int64
Name string
PublicIP []byte
Port uint16
PublicIP1 []byte
Port1 uint16
PublicIP2 []byte
Port2 uint16
Relay bool
PubKey []byte
PubSignKey []byte
WGPubKey []byte
}
type NetworkState struct {

View File

@@ -1,21 +0,0 @@
package peer
const bitSetSize = 512 // Multiple of 64.
type bitSet [bitSetSize / 64]uint64
func (bs *bitSet) Set(i int) {
bs[i/64] |= 1 << (i % 64)
}
func (bs *bitSet) Clear(i int) {
bs[i/64] &= ^(1 << (i % 64))
}
func (bs *bitSet) ClearAll() {
clear(bs[:])
}
func (bs *bitSet) Get(i int) bool {
return bs[i/64]&(1<<(i%64)) != 0
}

View File

@@ -1,48 +0,0 @@
package peer
import (
"math/rand"
"testing"
)
func TestBitSet(t *testing.T) {
state := make([]bool, bitSetSize)
for i := range state {
state[i] = rand.Float32() > 0.5
}
bs := bitSet{}
for i := range state {
if state[i] {
bs.Set(i)
}
}
for i := range state {
if bs.Get(i) != state[i] {
t.Fatal(i, state[i], bs.Get(i))
}
}
for i := range state {
if rand.Float32() > 0.5 {
state[i] = false
bs.Clear(i)
}
}
for i := range state {
if bs.Get(i) != state[i] {
t.Fatal(i, state[i], bs.Get(i))
}
}
bs.ClearAll()
for i := range state {
if bs.Get(i) {
t.Fatal(i, bs.Get(i))
}
}
}

View File

@@ -1,26 +0,0 @@
package peer
import "golang.org/x/crypto/nacl/box"
type controlCipher struct {
sharedKey [32]byte
}
func newControlCipher(privKey, pubKey []byte) *controlCipher {
shared := [32]byte{}
box.Precompute(&shared, (*[32]byte)(pubKey), (*[32]byte)(privKey))
return &controlCipher{shared}
}
func (cc *controlCipher) Encrypt(h header, data, out []byte) []byte {
const s = controlHeaderSize
out = out[:s+controlCipherOverhead+len(data)]
h.Marshal(out[:s])
box.SealAfterPrecomputation(out[s:s], data, (*[24]byte)(out[:s]), &cc.sharedKey)
return out
}
func (cc *controlCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) {
const s = controlHeaderSize
return box.OpenAfterPrecomputation(out[:0], encrypted[s:], (*[24]byte)(encrypted[:s]), &cc.sharedKey)
}

View File

@@ -1,122 +0,0 @@
package peer
import (
"bytes"
"crypto/rand"
"reflect"
"testing"
"golang.org/x/crypto/nacl/box"
)
func newControlCipherForTesting() (c1, c2 *controlCipher) {
pubKey1, privKey1, err := box.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
pubKey2, privKey2, err := box.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return newControlCipher(privKey1[:], pubKey2[:]),
newControlCipher(privKey2[:], pubKey1[:])
}
func TestControlCipher(t *testing.T) {
c1, c2 := newControlCipherForTesting()
maxSizePlaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := header{
StreamID: controlStreamID,
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
encrypted = c1.Encrypt(h1, plaintext, encrypted)
h2 := header{}
h2.Parse(encrypted)
if !reflect.DeepEqual(h1, h2) {
t.Fatal(h1, h2)
}
decrypted, ok := c2.Decrypt(encrypted, make([]byte, bufferSize))
if !ok {
t.Fatal(ok)
}
if !bytes.Equal(decrypted, plaintext) {
t.Fatal("not equal")
}
}
}
func TestControlCipher_ShortCiphertext(t *testing.T) {
c1, _ := newControlCipherForTesting()
shortText := make([]byte, controlHeaderSize+controlCipherOverhead-1)
rand.Read(shortText)
_, ok := c1.Decrypt(shortText, make([]byte, bufferSize))
if ok {
t.Fatal(ok)
}
}
func BenchmarkControlCipher_Encrypt(b *testing.B) {
c1, _ := newControlCipherForTesting()
h1 := header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
encrypted = c1.Encrypt(h1, plaintext, encrypted)
}
}
func BenchmarkControlCipher_Decrypt(b *testing.B) {
c1, c2 := newControlCipherForTesting()
h1 := header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-controlHeaderSize-controlCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
encrypted = c1.Encrypt(h1, plaintext, encrypted)
decrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
decrypted, _ = c2.Decrypt(encrypted, decrypted)
}
}

View File

@@ -1,61 +0,0 @@
package peer
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"log"
)
type dataCipher struct {
key [32]byte
aead cipher.AEAD
}
func newDataCipher() *dataCipher {
key := [32]byte{}
if _, err := rand.Read(key[:]); err != nil {
log.Fatalf("Failed to read random data: %v", err)
}
return newDataCipherFromKey(key)
}
func newDataCipherFromKey(key [32]byte) *dataCipher {
block, err := aes.NewCipher(key[:])
if err != nil {
log.Fatalf("Failed to create new cipher: %v", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
log.Fatalf("Failed to create new GCM: %v", err)
}
return &dataCipher{key: key, aead: aead}
}
func (sc *dataCipher) Key() [32]byte {
return sc.key
}
func (sc *dataCipher) Encrypt(h header, data, out []byte) []byte {
const s = dataHeaderSize
out = out[:s+dataCipherOverhead+len(data)]
h.Marshal(out[:s])
sc.aead.Seal(out[s:s], out[:s], data, nil)
return out
}
func (sc *dataCipher) Decrypt(encrypted, out []byte) (data []byte, ok bool) {
const s = dataHeaderSize
if len(encrypted) < s+dataCipherOverhead {
ok = false
return
}
var err error
data, err = sc.aead.Open(out[:0], encrypted[:s], encrypted[s:], nil)
ok = err == nil
return
}

View File

@@ -1,141 +0,0 @@
package peer
import (
"bytes"
"crypto/rand"
mrand "math/rand/v2"
"reflect"
"testing"
)
func TestDataCipher(t *testing.T) {
maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := header{
StreamID: dataStreamID,
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
h2 := header{}
h2.Parse(encrypted)
dc2 := newDataCipherFromKey(dc1.Key())
decrypted, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize))
if !ok {
t.Fatal(ok)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatal("not equal")
}
if !reflect.DeepEqual(h1, h2) {
t.Fatalf("%v != %v", h1, h2)
}
}
}
func TestDataCipher_ModifyCiphertext(t *testing.T) {
maxSizePlaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(maxSizePlaintext)
testCases := [][]byte{
make([]byte, 0),
{1},
{255},
{1, 2, 3, 4, 5},
[]byte("Hello world"),
maxSizePlaintext,
}
for _, plaintext := range testCases {
h1 := header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
encrypted[mrand.IntN(len(encrypted))]++
dc2 := newDataCipherFromKey(dc1.Key())
_, ok := dc2.Decrypt(encrypted, make([]byte, bufferSize-dataHeaderSize))
if ok {
t.Fatal(ok)
}
}
}
func TestDataCipher_ShortCiphertext(t *testing.T) {
dc1 := newDataCipher()
shortText := make([]byte, dataHeaderSize+dataCipherOverhead-1)
rand.Read(shortText)
_, ok := dc1.Decrypt(shortText, make([]byte, bufferSize))
if ok {
t.Fatal(ok)
}
}
func BenchmarkDataCipher_Encrypt(b *testing.B) {
h1 := header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
b.ResetTimer()
for i := 0; i < b.N; i++ {
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
}
}
func BenchmarkDataCipher_Decrypt(b *testing.B) {
h1 := header{
Counter: 235153,
SourceIP: 4,
DestIP: 88,
}
plaintext := make([]byte, bufferSize-dataHeaderSize-dataCipherOverhead)
rand.Read(plaintext)
encrypted := make([]byte, bufferSize)
dc1 := newDataCipher()
encrypted = dc1.Encrypt(h1, plaintext, encrypted)
decrypted := make([]byte, bufferSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
decrypted, _ = dc1.Decrypt(encrypted, decrypted)
}
}

View File

@@ -1,13 +0,0 @@
package peer
/*
func signData(privKey *[64]byte, h header, data, out []byte) []byte {
out = out[:headerSize]
h.Marshal(out)
return sign.Sign(out, data, privKey)
}
func openData(pubKey *[32]byte, signed, out []byte) (data []byte, ok bool) {
return sign.Open(out[:0], signed[headerSize:], pubKey)
}
*/

View File

@@ -1,140 +0,0 @@
package peer
import (
"io"
"log"
"net/netip"
"sync/atomic"
)
type connReader struct {
// Input
readFromUDPAddrPort func([]byte) (int, netip.AddrPort, error)
// Output
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error)
iface io.Writer
handleControlMsg func(fromIP byte, pkt any)
localIP byte
rt *atomic.Pointer[routingTable]
buf []byte
decBuf []byte
}
func newConnReader(
readFromUDPAddrPort func([]byte) (int, netip.AddrPort, error),
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error),
iface io.Writer,
handleControlMsg func(fromIP byte, pkt any),
rt *atomic.Pointer[routingTable],
) *connReader {
return &connReader{
readFromUDPAddrPort: readFromUDPAddrPort,
writeToUDPAddrPort: writeToUDPAddrPort,
iface: iface,
handleControlMsg: handleControlMsg,
localIP: rt.Load().LocalIP,
rt: rt,
buf: newBuf(),
decBuf: newBuf(),
}
}
func (r *connReader) Run() {
for {
r.handleNextPacket()
}
}
func (r *connReader) handleNextPacket() {
buf := r.buf[:bufferSize]
n, remoteAddr, err := r.readFromUDPAddrPort(buf)
if err != nil {
log.Fatalf("Failed to read from UDP port: %v", err)
}
if n < headerSize {
return
}
remoteAddr = netip.AddrPortFrom(remoteAddr.Addr().Unmap(), remoteAddr.Port())
buf = buf[:n]
h := parseHeader(buf)
rt := r.rt.Load()
peer := rt.Peers[h.SourceIP]
switch h.StreamID {
case controlStreamID:
r.handleControlPacket(remoteAddr, peer, h, buf)
case dataStreamID:
r.handleDataPacket(rt, peer, h, buf)
default:
r.logf("Unknown stream ID: %d", h.StreamID)
}
}
func (r *connReader) handleControlPacket(
remoteAddr netip.AddrPort,
peer remotePeer,
h header,
enc []byte,
) {
if peer.ControlCipher == nil {
r.logf("No control cipher for peer: %d", h.SourceIP)
return
}
if h.DestIP != r.localIP {
r.logf("Incorrect destination IP on control packet: %d", h.DestIP)
return
}
msg, err := peer.DecryptControlPacket(remoteAddr, h, enc, r.decBuf)
if err != nil {
r.logf("Failed to decrypt control packet: %v", err)
return
}
r.handleControlMsg(h.SourceIP, msg)
}
func (r *connReader) handleDataPacket(
rt *routingTable,
peer remotePeer,
h header,
enc []byte,
) {
if !peer.Up {
r.logf("Not connected (recv).")
return
}
data, err := peer.DecryptDataPacket(h, enc, r.decBuf)
if err != nil {
r.logf("Failed to decrypt data packet: %v", err)
return
}
if h.DestIP == r.localIP {
if _, err := r.iface.Write(data); err != nil {
log.Fatalf("Failed to write to interface: %v", err)
}
return
}
remote := rt.Peers[h.DestIP]
if !remote.Direct {
r.logf("Unable to relay data to %d.", h.DestIP)
return
}
r.writeToUDPAddrPort(data, remote.DirectAddr)
}
func (r *connReader) logf(format string, args ...any) {
log.Printf("[ConnReader] "+format, args...)
}

View File

@@ -1,64 +0,0 @@
package peer
import (
"net/netip"
"vppn/m"
)
// ----------------------------------------------------------------------------
type controlMsg[T any] struct {
SrcIP byte
SrcAddr netip.AddrPort
Packet T
}
func parseControlMsg(srcIP byte, srcAddr netip.AddrPort, buf []byte) (any, error) {
switch buf[0] {
case packetTypeInit:
packet, err := parsePacketInit(buf)
return controlMsg[packetInit]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeSyn:
packet, err := parsePacketSyn(buf)
return controlMsg[packetSyn]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeAck:
packet, err := parsePacketAck(buf)
return controlMsg[packetAck]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
case packetTypeProbe:
packet, err := parsePacketProbe(buf)
return controlMsg[packetProbe]{
SrcIP: srcIP,
SrcAddr: srcAddr,
Packet: packet,
}, err
default:
return nil, errUnknownPacketType
}
}
// ----------------------------------------------------------------------------
type peerUpdateMsg struct {
Peer *m.Peer
}
// ----------------------------------------------------------------------------
type pingTimerMsg struct{}

View File

@@ -1,30 +1,15 @@
package peer
import (
"crypto/rand"
"log"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/sign"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type cryptoKeys struct {
PubKey []byte
PrivKey []byte
PubSignKey []byte
PrivSignKey []byte
}
func generateKeys() cryptoKeys {
pubKey, privKey, err := box.GenerateKey(rand.Reader)
func generateWGKey() wgtypes.Key {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
log.Fatalf("Failed to generate encryption keys: %v", err)
log.Fatalf("Failed to generate WireGuard private key: %v", err)
}
pubSignKey, privSignKey, err := sign.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Failed to generate signing keys: %v", err)
}
return cryptoKeys{pubKey[:], privKey[:], pubSignKey[:], privSignKey[:]}
return key
}

View File

@@ -1,191 +0,0 @@
package peer
import (
"net/netip"
"reflect"
"testing"
)
func newRoutePairForTesting() (*remotePeer, *remotePeer) {
keys1 := generateKeys()
keys2 := generateKeys()
r1 := newRemotePeer(1)
r1.PubSignKey = keys1.PubSignKey
r1.ControlCipher = newControlCipher(keys1.PrivKey, keys2.PubKey)
r1.DataCipher = newDataCipher()
r2 := newRemotePeer(2)
r2.PubSignKey = keys2.PubSignKey
r2.ControlCipher = newControlCipher(keys2.PrivKey, keys1.PubKey)
r2.DataCipher = r1.DataCipher
return r1, r2
}
func TestDecryptControlPacket(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
tmp = make([]byte, bufferSize)
out = make([]byte, bufferSize)
)
in := packetSyn{
TraceID: newTraceID(),
SharedKey: r1.DataCipher.Key(),
Direct: true,
}
enc := r1.EncryptControlPacket(in, tmp, out)
h := parseHeader(enc)
iMsg, err := r2.DecryptControlPacket(netip.AddrPort{}, h, enc, tmp)
if err != nil {
t.Fatal(err)
}
msg, ok := iMsg.(controlMsg[packetSyn])
if !ok {
t.Fatal(ok)
}
if !reflect.DeepEqual(msg.Packet, in) {
t.Fatal(msg)
}
}
/*
func TestDecryptControlPacket_decryptionFailed(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
tmp = make([]byte, bufferSize)
out = make([]byte, bufferSize)
)
in := packetSyn{
TraceID: newTraceID(),
SharedKey: r1.DataCipher.Key(),
Direct: true,
}
enc := encryptControlPacket(r1.IP, r2, in, tmp, out)
h := parseHeader(enc)
for i := range enc {
x := bytes.Clone(enc)
x[i]++
_, err := decryptControlPacket(r2, netip.AddrPort{}, h, x, tmp)
if !errors.Is(err, errDecryptionFailed) {
t.Fatal(i, err)
}
}
}
func TestDecryptControlPacket_duplicate(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
tmp = make([]byte, bufferSize)
out = make([]byte, bufferSize)
)
in := packetSyn{
TraceID: newTraceID(),
SharedKey: r1.DataCipher.Key(),
Direct: true,
}
enc := encryptControlPacket(r1.IP, r2, in, tmp, out)
h := parseHeader(enc)
if _, err := decryptControlPacket(r2, netip.AddrPort{}, h, enc, tmp); err != nil {
t.Fatal(err)
}
_, err := decryptControlPacket(r2, netip.AddrPort{}, h, enc, tmp)
if !errors.Is(err, errDuplicateSeqNum) {
t.Fatal(err)
}
}
func TestDecryptControlPacket_invalidPacket(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
tmp = make([]byte, bufferSize)
out = make([]byte, bufferSize)
)
in := testPacket("hello!")
enc := encryptControlPacket(r1.IP, r2, in, tmp, out)
h := parseHeader(enc)
_, err := decryptControlPacket(r2, netip.AddrPort{}, h, enc, tmp)
if !errors.Is(err, errUnknownPacketType) {
t.Fatal(err)
}
}
func TestDecryptDataPacket(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
out = make([]byte, bufferSize)
data = make([]byte, 1024)
)
rand.Read(data)
enc := encryptDataPacket(r1.IP, r2.IP, r2, data, out)
h := parseHeader(enc)
out, err := decryptDataPacket(r1, h, bytes.Clone(enc), out)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, out) {
t.Fatal(data, out)
}
}
func TestDecryptDataPacket_incorrectCipher(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
out = make([]byte, bufferSize)
data = make([]byte, 1024)
)
rand.Read(data)
enc := encryptDataPacket(r1.IP, r2.IP, r2, data, bytes.Clone(out))
h := parseHeader(enc)
r1.DataCipher = newDataCipher()
_, err := decryptDataPacket(r1, h, enc, bytes.Clone(out))
if !errors.Is(err, errDecryptionFailed) {
t.Fatal(err)
}
}
func TestDecryptDataPacket_duplicate(t *testing.T) {
var (
r1, r2 = newRoutePairForTesting()
out = make([]byte, bufferSize)
data = make([]byte, 1024)
)
rand.Read(data)
enc := encryptDataPacket(r1.IP, r2.IP, r2, data, bytes.Clone(out))
h := parseHeader(enc)
_, err := decryptDataPacket(r1, h, enc, bytes.Clone(out))
if err != nil {
t.Fatal(err)
}
_, err = decryptDataPacket(r1, h, enc, bytes.Clone(out))
if !errors.Is(err, errDuplicateSeqNum) {
t.Fatal(err)
}
}
*/

View File

@@ -1,12 +1,11 @@
digraph d {
ifReader -> connWriter;
connReader -> ifWriter;
connReader -> connWriter;
connReader -> supervisor;
mcReader -> supervisor;
supervisor -> connWriter;
supervisor -> mcWriter;
hubPoller -> supervisor;
ifReader -> remote;
connReader -> remote;
mcReader -> remote;
remote -> connWriter;
remote -> ifWriter;
hubPoller -> remote;
connWriter [shape="box"];
mcWriter [shape="box"];

View File

@@ -1,76 +0,0 @@
package peer
type dupCheck struct {
bitSet
head int
tail int
headCounter uint64
tailCounter uint64 // Also next expected counter value.
}
func newDupCheck(headCounter uint64) *dupCheck {
return &dupCheck{
headCounter: headCounter,
tailCounter: headCounter + 1,
tail: 1,
}
}
func (dc *dupCheck) IsDup(counter uint64) bool {
// Before head => it's late, say it's a dup.
if counter < dc.headCounter {
return true
}
// It's within the counter bounds.
if counter < dc.tailCounter {
index := (int(counter-dc.headCounter) + dc.head) % bitSetSize
if dc.Get(index) {
return true
}
dc.Set(index)
return false
}
// It's more than 1 beyond the tail.
delta := counter - dc.tailCounter
// Full clear.
if delta >= bitSetSize-1 {
dc.ClearAll()
dc.Set(0)
dc.tail = 1
dc.head = 2
dc.tailCounter = counter + 1
dc.headCounter = dc.tailCounter - bitSetSize + 1
return false
}
// Clear if necessary.
for i := 0; i < int(delta); i++ {
dc.put(false)
}
dc.put(true)
return false
}
func (dc *dupCheck) put(set bool) {
if set {
dc.Set(dc.tail)
} else {
dc.Clear(dc.tail)
}
dc.tail = (dc.tail + 1) % bitSetSize
dc.tailCounter++
if dc.head == dc.tail {
dc.head = (dc.head + 1) % bitSetSize
dc.headCounter++
}
}

View File

@@ -1,57 +0,0 @@
package peer
import (
"testing"
)
func TestDupCheck(t *testing.T) {
dc := newDupCheck(0)
for i := range bitSetSize {
if dc.IsDup(uint64(i)) {
t.Fatal("!")
}
}
type TestCase struct {
Counter uint64
Dup bool
}
testCases := []TestCase{
{511, true},
{0, true},
{1, true},
{2, true},
{3, true},
{63, true},
{256, true},
{510, true},
{511, true},
{512, false},
{0, true},
{512, true},
{513, false},
{517, false},
{512, true},
{513, true},
{514, false},
{515, false},
{516, false},
{517, true},
{2512, false},
{2512, true},
{2001, true},
{2002, false},
{2002, true},
{4000, false},
{4000 - 511, true}, // Too old.
{4000 - 510, false}, // Just in the window.
}
for i, tc := range testCases {
if ok := dc.IsDup(tc.Counter); ok != tc.Dup {
t.Fatal(i, ok, tc)
}
}
}

View File

@@ -1,10 +0,0 @@
package peer
import "errors"
var (
errDecryptionFailed = errors.New("decryption failed")
errDuplicateSeqNum = errors.New("duplicate sequence number")
errMalformedPacket = errors.New("malformed packet")
errUnknownPacketType = errors.New("unknown packet type")
)

View File

@@ -8,12 +8,10 @@ import (
"vppn/m"
)
type localConfig struct {
m.PeerConfig
PubKey []byte
PrivKey []byte
PubSignKey []byte
PrivSignKey []byte
type LocalConfig struct {
LocalPeerIP byte
Network []byte
WGPrivKey string
}
func configDir(netName string) string {
@@ -24,12 +22,20 @@ func configDir(netName string) string {
return filepath.Join(d, ".vppn", netName)
}
func lockFilePath(netName string) string {
return filepath.Join(configDir(netName), "__lock__")
}
func peerConfigPath(netName string) string {
return filepath.Join(configDir(netName), "peer-config.json")
return filepath.Join(configDir(netName), "config.json")
}
func peerStatePath(netName string) string {
return filepath.Join(configDir(netName), "peer-state.json")
return filepath.Join(configDir(netName), "state.json")
}
func statusSocketPath(netName string) string {
return filepath.Join(configDir(netName), "status.sock")
}
func storeJson(x any, outPath string) error {
@@ -64,7 +70,7 @@ func storeJson(x any, outPath string) error {
return os.Rename(tmpPath, outPath)
}
func storePeerConfig(netName string, pc localConfig) error {
func storePeerConfig(netName string, pc LocalConfig) error {
return storeJson(pc, peerConfigPath(netName))
}
@@ -81,10 +87,11 @@ func loadJson(dataPath string, ptr any) error {
return json.Unmarshal(data, ptr)
}
func loadPeerConfig(netName string) (pc localConfig, err error) {
func loadPeerConfig(netName string) (pc LocalConfig, err error) {
return pc, loadJson(peerConfigPath(netName), &pc)
}
func loadNetworkState(netName string) (ps m.NetworkState, err error) {
return ps, loadJson(peerStatePath(netName), &ps)
}

View File

@@ -16,12 +16,12 @@ func TestFilePaths(t *testing.T) {
}
path := peerConfigPath("netName")
if path != filepath.Join(confDir, "peer-config.json") {
if path != filepath.Join(confDir, "config.json") {
t.Fatal(path)
}
path = peerStatePath("netName")
if path != filepath.Join(confDir, "peer-state.json") {
if path != filepath.Join(confDir, "state.json") {
t.Fatal(path)
}
}

View File

@@ -4,22 +4,12 @@ import (
"net"
"net/netip"
"time"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
version = 1
bufferSize = 1536
if_mtu = 1200
if_queue_len = 2048
controlCipherOverhead = 16
dataCipherOverhead = 16
signOverhead = 64
pingInterval = 8 * time.Second
timeoutInterval = 30 * time.Second
broadcastInterval = 16 * time.Second
broadcastErrorTimeoutInterval = 8 * time.Second
)
@@ -28,10 +18,22 @@ var multicastAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(
netip.AddrFrom4([4]byte{224, 0, 0, 157}),
4560))
func newBuf() []byte {
return make([]byte, bufferSize)
type Globals struct {
LocalConfig // Embed, immutable.
// WireGuard private key, client, and device name. Immutable after init.
WGPrivKey wgtypes.Key
WGClient *wgctrl.Client
WGDevName string
// Local public address (if available). Immutable.
LocalAddr netip.AddrPort
LocalAddrValid bool
}
type marshaller interface {
Marshal([]byte) []byte
func NewGlobals(localConfig LocalConfig, localAddr netip.AddrPort) (g Globals) {
g.LocalConfig = localConfig
g.LocalAddr = localAddr
g.LocalAddrValid = localAddr.IsValid()
return g
}

View File

@@ -1,46 +0,0 @@
package peer
import "unsafe"
// ----------------------------------------------------------------------------
const (
headerSize = 12
controlStreamID = 2
controlHeaderSize = 24
dataStreamID = 1
dataHeaderSize = 12
)
type header struct {
Version byte
StreamID byte
SourceIP byte
DestIP byte
Counter uint64 // Init with time.Now().Unix << 30 to ensure monotonic.
}
func parseHeader(b []byte) (h header) {
h.Version = b[0]
h.StreamID = b[1]
h.SourceIP = b[2]
h.DestIP = b[3]
h.Counter = *(*uint64)(unsafe.Pointer(&b[4]))
return h
}
func (h *header) Parse(b []byte) {
h.Version = b[0]
h.StreamID = b[1]
h.SourceIP = b[2]
h.DestIP = b[3]
h.Counter = *(*uint64)(unsafe.Pointer(&b[4]))
}
func (h *header) Marshal(buf []byte) {
buf[0] = h.Version
buf[1] = h.StreamID
buf[2] = h.SourceIP
buf[3] = h.DestIP
*(*uint64)(unsafe.Pointer(&buf[4])) = h.Counter
}

View File

@@ -1,21 +0,0 @@
package peer
import "testing"
func TestHeaderMarshalParse(t *testing.T) {
nIn := header{
StreamID: 23,
Counter: 3212,
SourceIP: 34,
DestIP: 200,
}
buf := make([]byte, headerSize)
nIn.Marshal(buf)
nOut := header{}
nOut.Parse(buf)
if nIn != nOut {
t.Fatal(nIn, nOut)
}
}

View File

@@ -5,27 +5,30 @@ import (
"io"
"log"
"net/http"
"net/netip"
"net/url"
"time"
"vppn/m"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type hubPoller struct {
type HubPoller struct {
Globals
holePunch *HolePunch
client *http.Client
req *http.Request
versions [256]int64
localIP byte
netName string
handleControlMsg func(fromIP byte, msg any)
}
func newHubPoller(
localIP byte,
func NewHubPoller(
g Globals,
hp *HolePunch,
netName,
hubURL,
apiKey string,
handleControlMsg func(byte, any),
) (*hubPoller, error) {
) (*HubPoller, error) {
u, err := url.Parse(hubURL)
if err != nil {
return nil, err
@@ -41,20 +44,20 @@ func newHubPoller(
}
req.SetBasicAuth("", apiKey)
return &hubPoller{
return &HubPoller{
Globals: g,
holePunch: hp,
client: client,
req: req,
localIP: localIP,
netName: netName,
handleControlMsg: handleControlMsg,
}, nil
}
func (hp *hubPoller) logf(s string, args ...any) {
func (hp *HubPoller) logf(s string, args ...any) {
log.Printf("[HubPoller] "+s, args...)
}
func (hp *hubPoller) Run() {
func (hp *HubPoller) Run() {
state, err := loadNetworkState(hp.netName)
if err != nil {
hp.logf("Failed to load network state: %v", err)
@@ -69,7 +72,7 @@ func (hp *hubPoller) Run() {
}
}
func (hp *hubPoller) pollHub() {
func (hp *HubPoller) pollHub() {
var state m.NetworkState
resp, err := hp.client.Do(hp.req)
@@ -89,22 +92,57 @@ func (hp *hubPoller) pollHub() {
return
}
hp.applyNetworkState(state)
if err := storeNetworkState(hp.netName, state); err != nil {
hp.logf("Failed to store network state: %v", err)
}
hp.applyNetworkState(state)
}
func (hp *hubPoller) applyNetworkState(state m.NetworkState) {
func (hp *HubPoller) applyNetworkState(state m.NetworkState) {
for i, peer := range state.Peers {
if i != int(hp.localIP) {
if peer == nil || peer.Version != hp.versions[i] {
hp.handleControlMsg(byte(i), peerUpdateMsg{Peer: state.Peers[i]})
if i == int(hp.LocalPeerIP) {
continue
}
if peer != nil && peer.Version == hp.versions[i] {
continue
}
hp.applyPeerConfig(peer)
if peer != nil {
hp.versions[i] = peer.Version
}
}
}
func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
return
}
if len(peer.PublicIP1) == 0 || peer.Port1 == 0 {
return
}
pubKey, err := wgtypes.NewKey(peer.WGPubKey)
if err != nil {
hp.logf("Invalid WG key for peer %d: %v", peer.PeerIP, err)
return
}
ip, ok := netip.AddrFromSlice(peer.PublicIP1)
if !ok {
hp.logf("Invalid public IP for peer %d", peer.PeerIP)
return
}
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1)
if peer.Relay {
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {
hp.logf("Failed to update relay config: %v", err)
}
return
}
if hp.holePunch != nil {
hp.holePunch.OnEndpointLearned(peer.PeerIP, pubKey, endpoint, true)
}
}

View File

@@ -1,103 +0,0 @@
package peer
import (
"io"
"log"
"net/netip"
"sync/atomic"
)
type ifReader struct {
iface io.Reader
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error)
rt *atomic.Pointer[routingTable]
buf1 []byte
buf2 []byte
}
func newIFReader(
iface io.Reader,
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error),
rt *atomic.Pointer[routingTable],
) *ifReader {
return &ifReader{iface, writeToUDPAddrPort, rt, newBuf(), newBuf()}
}
func (r *ifReader) Run() {
packet := newBuf()
for {
r.handleNextPacket(packet)
}
}
func (r *ifReader) handleNextPacket(packet []byte) {
packet = r.readNextPacket(packet)
remoteIP, ok := r.parsePacket(packet)
if !ok {
return
}
rt := r.rt.Load()
peer := rt.Peers[remoteIP]
if !peer.Up {
r.logf("Peer %d not up.", peer.IP)
return
}
enc := peer.EncryptDataPacket(peer.IP, packet, r.buf1)
if peer.Direct {
r.writeToUDPAddrPort(enc, peer.DirectAddr)
return
}
relay, ok := rt.GetRelay()
if !ok {
r.logf("Relay not available for peer %d.", peer.IP)
return
}
enc = relay.EncryptDataPacket(peer.IP, enc, r.buf2)
r.writeToUDPAddrPort(enc, relay.DirectAddr)
}
func (r *ifReader) readNextPacket(buf []byte) []byte {
n, err := r.iface.Read(buf[:cap(buf)])
if err != nil {
log.Fatalf("Failed to read from interface: %v", err)
}
return buf[:n]
}
func (r *ifReader) parsePacket(buf []byte) (byte, bool) {
n := len(buf)
if n == 0 {
return 0, false
}
version := buf[0] >> 4
switch version {
case 4:
if n < 20 {
r.logf("Short IPv4 packet: %d", len(buf))
return 0, false
}
return buf[19], true
case 6:
if len(buf) < 40 {
r.logf("Short IPv6 packet: %d", len(buf))
return 0, false
}
return buf[39], true
default:
r.logf("Invalid IP packet version: %v", version)
return 0, false
}
}
func (*ifReader) logf(s string, args ...any) {
log.Printf("[IFReader] "+s, args...)
}

View File

@@ -1,81 +0,0 @@
package peer
/*
func TestIFReader_IPv4(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
pkt := make([]byte, 1234)
pkt[0] = 4 << 4
pkt[19] = 2 // IP.
p1.IFace.UserWrite(pkt)
p1.IFReader.handleNextPacket(newBuf())
packets := p2.Conn.Packets()
if len(packets) != 1 {
t.Fatal(packets)
}
}
func TestIFReader_IPv6(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
pkt := make([]byte, 1234)
pkt[0] = 6 << 4
pkt[39] = 2 // IP.
p1.IFace.UserWrite(pkt)
p1.IFReader.handleNextPacket(newBuf())
packets := p2.Conn.Packets()
if len(packets) != 1 {
t.Fatal(packets)
}
}
func TestIFReader_parsePacket_emptyPacket(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 0)
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
func TestIFReader_parsePacket_invalidIPVersion(t *testing.T) {
r := NewIFReader(nil, nil)
for i := byte(1); i < 16; i++ {
if i == 4 || i == 6 {
continue
}
pkt := make([]byte, 1234)
pkt[0] = i << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(i, ip, ok)
}
}
}
func TestIFReader_parsePacket_shortIPv4(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 19)
pkt[0] = 4 << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
func TestIFReader_parsePacket_shortIPv6(t *testing.T) {
r := NewIFReader(nil, nil)
pkt := make([]byte, 39)
pkt[0] = 6 << 4
if ip, ok := r.parsePacket(pkt); ok {
t.Fatal(ip, ok)
}
}
*/

View File

@@ -1,137 +0,0 @@
package peer
import (
"fmt"
"io"
"net"
"os"
"syscall"
"golang.org/x/sys/unix"
)
func openInterface(network []byte, localIP byte, name string) (io.ReadWriteCloser, error) {
if len(network) != 4 {
return nil, fmt.Errorf("expected network to be 4 bytes, got %d", len(network))
}
ip := net.IPv4(network[0], network[1], network[2], localIP)
//////////////////////////
// Create TUN Interface //
//////////////////////////
tunFD, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|unix.O_CLOEXEC, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open TUN device: %w", err)
}
// New interface request.
req, err := unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create new TUN interface request: %w", err)
}
// Flags:
//
// IFF_NO_PI => don't add packet info data to packets sent to the interface.
// IFF_TUN => create a TUN device handling IP packets.
req.SetUint16(unix.IFF_NO_PI | unix.IFF_TUN)
err = unix.IoctlIfreq(tunFD, unix.TUNSETIFF, req)
if err != nil {
return nil, fmt.Errorf("failed to set TUN device settings: %w", err)
}
// Name may not be exactly the same?
name = req.Name()
/////////////
// Set MTU //
/////////////
// We need a socket file descriptor to set other options for some reason.
sockFD, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return nil, fmt.Errorf("failed to open socket: %w", err)
}
defer unix.Close(sockFD)
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create MTU interface request: %w", err)
}
req.SetUint32(if_mtu)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFMTU, req); err != nil {
return nil, fmt.Errorf("failed to set interface MTU: %w", err)
}
//////////////////////
// Set Queue Length //
//////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
}
req.SetUint16(if_queue_len)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFTXQLEN, req); err != nil {
return nil, fmt.Errorf("failed to set interface queue length: %w", err)
}
/////////////////////
// Set IP and Mask //
/////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
}
if err := req.SetInet4Addr(ip.To4()); err != nil {
return nil, fmt.Errorf("failed to set interface request IP: %w", err)
}
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFADDR, req); err != nil {
return nil, fmt.Errorf("failed to set interface IP: %w", err)
}
// SET MASK - must happen after setting address.
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create mask interface request: %w", err)
}
if err := req.SetInet4Addr(net.IPv4(255, 255, 255, 0).To4()); err != nil {
return nil, fmt.Errorf("failed to set interface request mask: %w", err)
}
if err := unix.IoctlIfreq(sockFD, unix.SIOCSIFNETMASK, req); err != nil {
return nil, fmt.Errorf("failed to set interface mask: %w", err)
}
////////////////////////
// Bring Interface Up //
////////////////////////
req, err = unix.NewIfreq(name)
if err != nil {
return nil, fmt.Errorf("failed to create up interface request: %w", err)
}
// Get current flags.
if err = unix.IoctlIfreq(sockFD, unix.SIOCGIFFLAGS, req); err != nil {
return nil, fmt.Errorf("failed to get interface flags: %w", err)
}
flags := req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING
// Set UP flag / broadcast flags.
req.SetUint16(flags)
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFFLAGS, req); err != nil {
return nil, fmt.Errorf("failed to set interface up: %w", err)
}
return os.NewFile(uintptr(tunFD), "tun"), nil
}

View File

@@ -1,23 +1,209 @@
package peer
import (
"flag"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"net/netip"
"os"
"time"
)
func Main() {
conf := peerConfig{}
// Usage:
//
// vppn netName run
// vppn netName status
func Main2() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...]
flag.StringVar(&conf.NetName, "name", "", "[REQUIRED] The network name.")
flag.StringVar(&conf.HubAddress, "hub-address", "", "[REQUIRED] The hub address.")
flag.StringVar(&conf.APIKey, "api-key", "", "[REQUIRED] The node's API key.")
flag.Parse()
if conf.NetName == "" || conf.HubAddress == "" || conf.APIKey == "" {
flag.Usage()
Available commands:
run
status
hosts
`, os.Args[0])
os.Exit(1)
}
peer := newPeerMain(conf)
peer.Run()
if len(os.Args) < 2 {
printUsage()
}
command := os.Args[1]
switch command {
case "run":
main_run()
case "status":
main_status()
case "hosts":
main_hosts()
default:
printUsage()
}
}
// ----------------------------------------------------------------------------
type mainArgs struct {
NetName string
HubAddress string
APIKey string
}
func main_run() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `Usage: %s run NETWORK_NAME HUB_ADDRESS API_KEY
NETWORK_NAME
Unique name of the network interface created. The network name
shouldn't change between invocations of the application.
HUB_ADDRESS
The address of the hub server. This should also contain the scheme, for
example https://hub.domain.com/.
API_KEY
The API key assigned to this peer by the hub.
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) != 5 {
printUsage()
}
args := mainArgs{
NetName: os.Args[2],
HubAddress: os.Args[3],
APIKey: os.Args[4],
}
newPeerMain(args).Run()
}
// ----------------------------------------------------------------------------
func main_status() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `Usage: %s status NETWORK_NAME
NETWORK_NAME
Unique name of the network interface created.
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) != 3 {
printUsage()
}
netName := os.Args[2]
report := fetchStatusReport(netName)
fmt.Printf("\n%s Status\n\n", netName)
if len(report.Network) != 4 {
fmt.Println("ERROR: Network isn't 4 bytes.")
fmt.Printf("Network: %v\n\n", report.Network)
} else {
nw := report.Network
fmt.Printf("%-8s %d.%d.%d.%d\n", "IP", nw[0], nw[1], nw[2], report.LocalPeerIP)
fmt.Printf("%-8s %d.%d.%d.%d/24\n", "Network", nw[0], nw[1], nw[2], nw[3])
}
if report.RelayPeerIP != 0 {
fmt.Printf("%-8s %d\n\n", "Relay", report.RelayPeerIP)
} else {
fmt.Printf("%-8s -\n\n", "Relay")
}
for _, status := range report.Remotes {
fmt.Printf("%3d %s\n", status.PeerIP, status.Name)
fmt.Printf(" %-11s %v\n", "Up", status.Up)
pubIP, ok := netip.AddrFromSlice(status.PublicIP)
if ok {
fmt.Printf(" %-11s %v\n", "Public IP", pubIP)
} else {
fmt.Printf(" %-11s\n", "Public IP")
}
fmt.Printf(" %-11s %d\n", "Port", status.Port)
fmt.Printf(" %-11s %v\n", "Relay", status.Relay)
fmt.Printf(" %-11s %v\n", "Server", status.Server)
fmt.Printf(" %-11s %v\n", "Direct", status.Direct)
if status.DirectAddr.IsValid() {
fmt.Printf(" %-11s %v\n", "Address", status.DirectAddr)
}
fmt.Println("")
}
}
// ----------------------------------------------------------------------------
func main_hosts() {
printUsage := func() {
fmt.Fprintf(os.Stderr, `Usage: %s hosts NETWORK_NAME
NETWORK_NAME
Unique name of the network interface created.
`, os.Args[0])
os.Exit(1)
}
if len(os.Args) != 3 {
printUsage()
}
netName := os.Args[2]
state, err := loadNetworkState(netName)
if err != nil {
log.Fatalf("Failed to load network state: %v", err)
}
config, err := loadPeerConfig(netName)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
nw := config.Network
for _, peer := range state.Peers {
if peer == nil {
continue
}
fmt.Printf("%d.%d.%d.%d %s\n",
nw[0], nw[1], nw[2], peer.PeerIP, peer.Name)
}
fmt.Println("")
}
// ----------------------------------------------------------------------------
func fetchStatusReport(netName string) StatusReport {
client := http.Client{
Transport: &http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return net.Dial("unix", statusSocketPath(netName))
},
},
Timeout: 8 * time.Second,
}
getURL := "http://unix" + statusSocketPath(netName)
resp, err := client.Get(getURL)
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
report := StatusReport{}
if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
log.Fatalf("Failed to decode status report: %v", err)
}
return report
}

View File

@@ -1,70 +1,66 @@
package peer
import (
"encoding/binary"
"fmt"
"log"
"net"
"sync/atomic"
"net/netip"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func runMCReader(
rt *atomic.Pointer[routingTable],
handleControlMsg func(destIP byte, msg any),
) {
func RunMCReader(g Globals, hp *HolePunch, netName string) {
for {
runMCReaderInner(rt, handleControlMsg)
if err := runMCReaderInner(g, hp, netName); err != nil {
log.Printf("[MCReader] %v", err)
}
time.Sleep(broadcastErrorTimeoutInterval)
}
}
func runMCReaderInner(
rt *atomic.Pointer[routingTable],
handleControlMsg func(destIP byte, msg any),
) {
var (
raw = newBuf()
buf = newBuf()
logf = func(s string, args ...any) {
log.Printf("[MCReader] "+s, args...)
}
)
func runMCReaderInner(g Globals, hp *HolePunch, netName string) error {
conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr)
if err != nil {
logf("Failed to bind to multicast address: %v", err)
return
return fmt.Errorf("bind: %w", err)
}
defer conn.Close()
buf := make([]byte, 64)
for {
conn.SetReadDeadline(time.Now().Add(32 * time.Second))
n, remoteAddr, err := conn.ReadFromUDPAddrPort(raw[:bufferSize])
n, src, err := conn.ReadFromUDPAddrPort(buf)
if err != nil {
logf("Failed to read from UDP port): %v", err)
return fmt.Errorf("read: %w", err)
}
if n != beaconLen {
continue
}
handleBeacon(g, hp, netName, buf[:n], src)
}
}
func handleBeacon(g Globals, hp *HolePunch, netName string, beacon []byte, src netip.AddrPort) {
peerIPByte := beacon[0]
if peerIPByte == g.LocalPeerIP {
return
}
raw = raw[:n]
h, ok := headerFromLocalDiscoveryPacket(raw)
if !ok {
logf("Failed to open discovery packet?")
continue
pubKey, err := wgtypes.NewKey(beacon[1:33])
if err != nil {
return
}
peer := rt.Load().Peers[h.SourceIP]
if peer.PubSignKey == nil {
logf("No signing key for peer %d.", h.SourceIP)
continue
// Skip relay peers: probing would replace their /24 AllowedIPs with empty.
if state, err := loadNetworkState(netName); err == nil {
if p := state.Peers[peerIPByte]; p != nil && p.Relay {
return
}
}
if !verifyLocalDiscoveryPacket(raw, buf, peer.PubSignKey) {
logf("Invalid signature from peer: %d", h.SourceIP)
continue
}
wgPort := binary.BigEndian.Uint16(beacon[33:35])
endpoint := netip.AddrPortFrom(src.Addr().Unmap(), wgPort)
msg := controlMsg[packetLocalDiscovery]{
SrcIP: h.SourceIP,
SrcAddr: remoteAddr,
}
handleControlMsg(h.SourceIP, msg)
}
hp.OnEndpointLearned(peerIPByte, pubKey, endpoint, false)
}

View File

@@ -1,132 +0,0 @@
package peer
/*
type mcMockConn struct {
packets chan []byte
}
func newMCMockConn() *mcMockConn {
return &mcMockConn{make(chan []byte, 32)}
}
func (c *mcMockConn) WriteToUDP(in []byte, addr *net.UDPAddr) (int, error) {
c.packets <- bytes.Clone(in)
return len(in), nil
}
func (c *mcMockConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
buf := <-c.packets
b = b[:len(buf)]
copy(b, buf)
return len(b), netip.AddrPort{}, nil
}
func TestMCReader(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 1, keys.PrivSignKey)
r := newMCReader(conn, super, peers)
w.SendLocalDiscovery()
r.handleNextPacket()
if len(super.Messages) != 1 {
t.Fatal(super.Messages)
}
msg, ok := super.Messages[0].(controlMsg[PacketLocalDiscovery])
if !ok || msg.SrcIP != 1 {
t.Fatal(ok, msg)
}
}
func TestMCReader_noHeader(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
r := newMCReader(conn, super, peers)
conn.WriteToUDP([]byte("0123546789"), nil)
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
func TestMCReader_noPeer(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[2] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 2, keys.PrivSignKey)
r := newMCReader(conn, super, peers)
w.SendLocalDiscovery()
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
func TestMCReader_badSignature(t *testing.T) {
keys := generateKeys()
super := &mockControlMsgHandler{}
conn := newMCMockConn()
peers := [256]*atomic.Pointer[RemotePeer]{}
peer := &RemotePeer{
IP: 1,
Up: true,
PubSignKey: keys.PubSignKey,
}
peers[1] = &atomic.Pointer[RemotePeer]{}
peers[1].Store(peer)
w := newMCWriter(conn, 1, keys.PrivSignKey)
w.SendLocalDiscovery()
// Break signing.
packet := <-conn.packets
packet[0]++
conn.packets <- packet
r := newMCReader(conn, super, peers)
r.handleNextPacket()
if len(super.Messages) != 0 {
t.Fatal(super.Messages)
}
}
*/

View File

@@ -1,53 +1,43 @@
package peer
import (
"encoding/binary"
"fmt"
"log"
"net"
"time"
"golang.org/x/crypto/nacl/sign"
)
func createLocalDiscoveryPacket(localIP byte, signingKey []byte) []byte {
h := header{
SourceIP: localIP,
DestIP: 255,
}
buf := make([]byte, headerSize)
h.Marshal(buf)
out := make([]byte, headerSize+signOverhead)
return sign.Sign(out[:0], buf, (*[64]byte)(signingKey))
}
func headerFromLocalDiscoveryPacket(pkt []byte) (h header, ok bool) {
if len(pkt) != headerSize+signOverhead {
return
}
h.Parse(pkt[signOverhead:])
ok = true
return
}
func verifyLocalDiscoveryPacket(pkt, buf []byte, pubSignKey []byte) bool {
_, ok := sign.Open(buf[:0], pkt, (*[32]byte)(pubSignKey))
return ok
}
// ----------------------------------------------------------------------------
func runMCWriter(localIP byte, signingKey []byte) {
discoveryPacket := createLocalDiscoveryPacket(localIP, signingKey)
const beaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port
func RunMCWriter(g Globals) {
conn, err := net.ListenMulticastUDP("udp", nil, multicastAddr)
if err != nil {
log.Fatalf("[MCWriter] Failed to bind to multicast address: %v", err)
log.Fatalf("[MCWriter] bind: %v", err)
}
for range time.Tick(broadcastInterval) {
_, err := conn.WriteToUDP(discoveryPacket, multicastAddr)
beacon, err := buildBeacon(g)
if err != nil {
log.Printf("[MCWriter] Failed to write multicast: %v", err)
log.Printf("[MCWriter] build beacon: %v", err)
continue
}
log.Printf("[MCWriter] Broadcasting on %v...", multicastAddr)
if _, err := conn.WriteToUDP(beacon, multicastAddr); err != nil {
log.Printf("[MCWriter] write: %v", err)
}
}
}
func buildBeacon(g Globals) ([]byte, error) {
dev, err := g.WGClient.Device(g.WGDevName)
if err != nil {
return nil, fmt.Errorf("get WG device: %w", err)
}
beacon := make([]byte, beaconLen)
beacon[0] = g.LocalPeerIP
pubKey := g.WGPrivKey.PublicKey()
copy(beacon[1:33], pubKey[:])
binary.BigEndian.PutUint16(beacon[33:35], uint16(dev.ListenPort))
return beacon, nil
}

View File

@@ -1,98 +0,0 @@
package peer
/*
// ----------------------------------------------------------------------------
// Testing that we can create and verify a local discovery packet.
func TestVerifyLocalDiscoveryPacket_valid(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
header, ok := headerFromLocalDiscoveryPacket(created)
if !ok {
t.Fatal(ok)
}
if header.SourceIP != 55 || header.DestIP != 255 {
t.Fatal(header)
}
if !verifyLocalDiscoveryPacket(created, make([]byte, 1024), keys.PubSignKey) {
t.Fatal("Not valid")
}
}
// Testing that we don't try to parse short packets.
func TestVerifyLocalDiscoveryPacket_tooShort(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
_, ok := headerFromLocalDiscoveryPacket(created[:len(created)-1])
if ok {
t.Fatal(ok)
}
}
// Testing that modifying a packet makes it invalid.
func TestVerifyLocalDiscoveryPacket_invalid(t *testing.T) {
keys := generateKeys()
created := createLocalDiscoveryPacket(55, keys.PrivSignKey)
buf := make([]byte, 1024)
for i := range created {
modified := bytes.Clone(created)
modified[i]++
if verifyLocalDiscoveryPacket(modified, buf, keys.PubSignKey) {
t.Fatal("Verification should have failed.")
}
}
}
// ----------------------------------------------------------------------------
type testUDPWriter struct {
written [][]byte
}
func (w *testUDPWriter) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
w.written = append(w.written, bytes.Clone(b))
return len(b), nil
}
func (w *testUDPWriter) Written() [][]byte {
out := w.written
w.written = [][]byte{}
return out
}
// ----------------------------------------------------------------------------
// Testing that the mcWriter sends local discovery packets as expected.
func TestMCWriter_SendLocalDiscovery(t *testing.T) {
keys := generateKeys()
writer := &testUDPWriter{}
mcw := newMCWriter(writer, 42, keys.PrivSignKey)
mcw.SendLocalDiscovery()
out := writer.Written()
if len(out) != 1 {
t.Fatal(out)
}
pkt := out[0]
header, ok := headerFromLocalDiscoveryPacket(pkt)
if !ok {
t.Fatal(ok)
}
if header.SourceIP != 42 || header.DestIP != 255 {
t.Fatal(header)
}
if !verifyLocalDiscoveryPacket(pkt, make([]byte, 1024), keys.PubSignKey) {
t.Fatal("Verification should succeed.")
}
}
*/

View File

@@ -1,31 +0,0 @@
package peer
import "bytes"
type TestIFace struct {
out *bytes.Buffer // Toward the network.
in *bytes.Buffer // From the network
}
func NewTestIFace() *TestIFace {
return &TestIFace{
out: &bytes.Buffer{},
in: &bytes.Buffer{},
}
}
func (iface *TestIFace) Write(b []byte) (int, error) {
return iface.in.Write(b)
}
func (iface *TestIFace) Read(b []byte) (int, error) {
return iface.out.Read(b)
}
func (iface *TestIFace) UserWrite(b []byte) (int, error) {
return iface.out.Write(b)
}
func (iface *TestIFace) UserRead(b []byte) (int, error) {
return iface.in.Read(b)
}

View File

@@ -1,80 +0,0 @@
package peer
import (
"bytes"
"net"
"net/netip"
"sync"
)
type TestPacket struct {
Addr netip.AddrPort
Data []byte
}
type TestNetwork struct {
lock sync.Mutex
packets map[netip.AddrPort]chan TestPacket
}
func NewTestNetwork() *TestNetwork {
return &TestNetwork{packets: map[netip.AddrPort]chan TestPacket{}}
}
func (n *TestNetwork) NewUDPConn(localAddr netip.AddrPort) *TestUDPConn {
n.lock.Lock()
defer n.lock.Unlock()
if _, ok := n.packets[localAddr]; !ok {
n.packets[localAddr] = make(chan TestPacket, 1024)
}
return &TestUDPConn{
addr: localAddr,
n: n,
packets: n.packets[localAddr],
}
}
func (n *TestNetwork) write(b []byte, from, to netip.AddrPort) {
n.lock.Lock()
defer n.lock.Unlock()
if _, ok := n.packets[to]; !ok {
n.packets[to] = make(chan TestPacket, 1024)
}
n.packets[to] <- TestPacket{
Addr: from,
Data: bytes.Clone(b),
}
}
type TestUDPConn struct {
addr netip.AddrPort
n *TestNetwork
packets chan TestPacket
}
func (c *TestUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
c.n.write(b, c.addr, addr)
return len(b), nil
}
func (c *TestUDPConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {
return c.WriteToUDPAddrPort(b, addr.AddrPort())
}
func (c *TestUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
pkt := <-c.packets
b = b[:len(pkt.Data)]
copy(b, pkt.Data)
return len(b), pkt.Addr, nil
}
func (c *TestUDPConn) Packets() (out []TestPacket) {
for {
select {
case pkt := <-c.packets:
out = append(out, pkt)
default:
return
}
}
}

View File

@@ -1,190 +0,0 @@
package peer
import (
"net/netip"
"sync/atomic"
"time"
"unsafe"
)
var traceIDCounter uint64 = uint64(time.Now().Unix()<<30) + 1
func newTraceID() uint64 {
return atomic.AddUint64(&traceIDCounter, 1)
}
// ----------------------------------------------------------------------------
type binWriter struct {
b []byte
i int
}
func newBinWriter(buf []byte) *binWriter {
buf = buf[:cap(buf)]
return &binWriter{buf, 0}
}
func (w *binWriter) Bool(b bool) *binWriter {
if b {
return w.Byte(1)
}
return w.Byte(0)
}
func (w *binWriter) Byte(b byte) *binWriter {
w.b[w.i] = b
w.i++
return w
}
func (w *binWriter) SharedKey(key [32]byte) *binWriter {
copy(w.b[w.i:w.i+32], key[:])
w.i += 32
return w
}
func (w *binWriter) Uint16(x uint16) *binWriter {
*(*uint16)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 2
return w
}
func (w *binWriter) Uint64(x uint64) *binWriter {
*(*uint64)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 8
return w
}
func (w *binWriter) Int64(x int64) *binWriter {
*(*int64)(unsafe.Pointer(&w.b[w.i])) = x
w.i += 8
return w
}
func (w *binWriter) AddrPort(addrPort netip.AddrPort) *binWriter {
w.Bool(addrPort.IsValid())
addr := addrPort.Addr().As16()
copy(w.b[w.i:w.i+16], addr[:])
w.i += 16
return w.Uint16(addrPort.Port())
}
func (w *binWriter) AddrPort8(l [8]netip.AddrPort) *binWriter {
for _, addrPort := range l {
w.AddrPort(addrPort)
}
return w
}
func (w *binWriter) Build() []byte {
return w.b[:w.i]
}
// ----------------------------------------------------------------------------
type binReader struct {
b []byte
i int
err error
}
func newBinReader(buf []byte) *binReader {
return &binReader{b: buf}
}
func (r *binReader) hasBytes(n int) bool {
if r.err != nil || (len(r.b)-r.i) < n {
r.err = errMalformedPacket
return false
}
return true
}
func (r *binReader) Bool(b *bool) *binReader {
var bb byte
r.Byte(&bb)
*b = bb != 0
return r
}
func (r *binReader) Byte(b *byte) *binReader {
if !r.hasBytes(1) {
return r
}
*b = r.b[r.i]
r.i++
return r
}
func (r *binReader) SharedKey(x *[32]byte) *binReader {
if !r.hasBytes(32) {
return r
}
*x = ([32]byte)(r.b[r.i : r.i+32])
r.i += 32
return r
}
func (r *binReader) Uint16(x *uint16) *binReader {
if !r.hasBytes(2) {
return r
}
*x = *(*uint16)(unsafe.Pointer(&r.b[r.i]))
r.i += 2
return r
}
func (r *binReader) Uint64(x *uint64) *binReader {
if !r.hasBytes(8) {
return r
}
*x = *(*uint64)(unsafe.Pointer(&r.b[r.i]))
r.i += 8
return r
}
func (r *binReader) Int64(x *int64) *binReader {
if !r.hasBytes(8) {
return r
}
*x = *(*int64)(unsafe.Pointer(&r.b[r.i]))
r.i += 8
return r
}
func (r *binReader) AddrPort(x *netip.AddrPort) *binReader {
if !r.hasBytes(19) {
return r
}
var (
valid bool
port uint16
)
r.Bool(&valid)
addr := netip.AddrFrom16(([16]byte)(r.b[r.i : r.i+16])).Unmap()
r.i += 16
r.Uint16(&port)
if valid {
*x = netip.AddrPortFrom(addr, port)
} else {
*x = netip.AddrPort{}
}
return r
}
func (r *binReader) AddrPort8(x *[8]netip.AddrPort) *binReader {
for i := range x {
r.AddrPort(&x[i])
}
return r
}
func (r *binReader) Error() error {
return r.err
}

View File

@@ -1,76 +0,0 @@
package peer
import (
"net/netip"
"reflect"
"testing"
)
func TestBinWriteRead_invalidAddrPort(t *testing.T) {
addr := netip.AddrPort{}
buf := make([]byte, 1024)
buf = newBinWriter(buf).
AddrPort(addr).
Build()
var addr2 netip.AddrPort
err := newBinReader(buf).
AddrPort(&addr2).
Error()
if err != nil {
t.Fatal(err)
}
if addr2.IsValid() {
t.Fatal(addr, addr2)
}
}
func TestBinWriteRead(t *testing.T) {
buf := make([]byte, 1024)
type Item struct {
Type byte
TraceID uint64
Addrs [8]netip.AddrPort
DestAddr netip.AddrPort
}
in := Item{
1,
2,
[8]netip.AddrPort{},
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 22),
}
in.Addrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20)
in.Addrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 22)
in.Addrs[3] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 3}), 23)
in.Addrs[4] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 4}), 24)
in.Addrs[5] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 5}), 25)
in.Addrs[6] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 3, 4, 6}), 26)
in.Addrs[7] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{7, 8, 9, 7}), 27)
buf = newBinWriter(buf).
Byte(in.Type).
Uint64(in.TraceID).
AddrPort(in.DestAddr).
AddrPort8(in.Addrs).
Build()
out := Item{}
err := newBinReader(buf).
Byte(&out.Type).
Uint64(&out.TraceID).
AddrPort(&out.DestAddr).
AddrPort8(&out.Addrs).
Error()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(in, out) {
t.Fatal(in, out)
}
}

View File

@@ -1,120 +0,0 @@
package peer
import (
"net/netip"
)
const (
packetTypeSyn = 1
packetTypeInit = 2
packetTypeAck = 3
packetTypeProbe = 4
packetTypeAddrDiscovery = 5
)
// ----------------------------------------------------------------------------
type packetInit struct {
TraceID uint64
Direct bool
Version uint64
}
func (p packetInit) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeInit).
Uint64(p.TraceID).
Bool(p.Direct).
Uint64(p.Version).
Build()
}
func parsePacketInit(buf []byte) (p packetInit, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
Bool(&p.Direct).
Uint64(&p.Version).
Error()
return
}
// ----------------------------------------------------------------------------
type packetSyn struct {
TraceID uint64 // TraceID to match response w/ request.
SharedKey [32]byte // Our shared key.
Direct bool
PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender.
}
func (p packetSyn) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeSyn).
Uint64(p.TraceID).
SharedKey(p.SharedKey).
Bool(p.Direct).
AddrPort8(p.PossibleAddrs).
Build()
}
func parsePacketSyn(buf []byte) (p packetSyn, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
SharedKey(&p.SharedKey).
Bool(&p.Direct).
AddrPort8(&p.PossibleAddrs).
Error()
return
}
// ----------------------------------------------------------------------------
type packetAck struct {
TraceID uint64
ToAddr netip.AddrPort
PossibleAddrs [8]netip.AddrPort // Possible public addresses of the sender.
}
func (p packetAck) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeAck).
Uint64(p.TraceID).
AddrPort(p.ToAddr).
AddrPort8(p.PossibleAddrs).
Build()
}
func parsePacketAck(buf []byte) (p packetAck, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
AddrPort(&p.ToAddr).
AddrPort8(&p.PossibleAddrs).
Error()
return
}
// ----------------------------------------------------------------------------
// A probeReqPacket is sent from a client to a server to determine if direct
// UDP communication can be used.
type packetProbe struct {
TraceID uint64
}
func (p packetProbe) Marshal(buf []byte) []byte {
return newBinWriter(buf).
Byte(packetTypeProbe).
Uint64(p.TraceID).
Build()
}
func parsePacketProbe(buf []byte) (p packetProbe, err error) {
err = newBinReader(buf[1:]).
Uint64(&p.TraceID).
Error()
return
}
// ----------------------------------------------------------------------------
type packetLocalDiscovery struct{}

View File

@@ -1,66 +0,0 @@
package peer
import (
"crypto/rand"
"net/netip"
"reflect"
"testing"
)
func TestSynPacket(t *testing.T) {
p := packetSyn{
TraceID: newTraceID(),
//SentAt: time.Now().UnixMilli(),
//SharedKeyType: 1,
Direct: true,
}
rand.Read(p.SharedKey[:])
p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234)
p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399)
p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000)
buf := p.Marshal(newBuf())
p2, err := parsePacketSyn(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}
func TestAckPacket(t *testing.T) {
p := packetAck{
TraceID: newTraceID(),
ToAddr: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 234),
}
p.PossibleAddrs[0] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 2, 3, 4}), 100)
p.PossibleAddrs[1] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 2, 3, 4}), 12399)
p.PossibleAddrs[2] = netip.AddrPortFrom(netip.AddrFrom4([4]byte{3, 2, 3, 4}), 60000)
buf := p.Marshal(newBuf())
p2, err := parsePacketAck(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}
func TestProbePacket(t *testing.T) {
p := packetProbe{
TraceID: newTraceID(),
}
buf := p.Marshal(newBuf())
p2, err := parsePacketProbe(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, p2) {
t.Fatal(p2)
}
}

View File

@@ -3,142 +3,173 @@ package peer
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"net/url"
"sync"
"sync/atomic"
"os"
"vppn/m"
"git.crumpington.com/lib/go/flock"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type peerMain struct {
conf localConfig
rt *atomic.Pointer[routingTable]
ifReader *ifReader
connReader *connReader
iface io.Writer
hubPoller *hubPoller
super *supervisor
Globals
netName string
holePunch *HolePunch
controlServer *ControlServer
endpointReporter *EndpointReporter // non-nil on relay peers only
hubPoller *HubPoller
lockFile *os.File
}
type peerConfig struct {
NetName string
HubAddress string
APIKey string
}
func newPeerMain(conf peerConfig) *peerMain {
func newPeerMain(args mainArgs) *peerMain {
logf := func(s string, args ...any) {
log.Printf("[Main] "+s, args...)
}
config, err := loadPeerConfig(conf.NetName)
if err := os.MkdirAll(configDir(args.NetName), 0700); err != nil {
log.Fatalf("Failed to create config directory: %v", err)
}
lockFile, err := flock.TryLock(lockFilePath(args.NetName))
if err != nil {
log.Fatalf("Failed to open lock file: %v", err)
}
if lockFile == nil {
log.Fatalf("Failed to obtain file lock.")
}
config, err := loadPeerConfig(args.NetName)
if err != nil {
logf("Failed to load configuration: %v", err)
logf("Initializing...")
initPeerWithHub(conf)
initPeerWithHub(args)
config, err = loadPeerConfig(conf.NetName)
config, err = loadPeerConfig(args.NetName)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
}
iface, err := openInterface(config.Network, config.PeerIP, conf.NetName)
state, err := loadNetworkState(args.NetName)
if err != nil {
log.Fatalf("Failed to open interface: %v", err)
log.Fatalf("Failed to load network state: %v", err)
}
myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", config.Port))
wgPrivKey, err := wgtypes.ParseKey(config.WGPrivKey)
if err != nil {
log.Fatalf("Failed to resolve UDP address: %v", err)
log.Fatalf("Failed to parse WireGuard private key: %v", err)
}
logf("Listening on %v...", myAddr)
conn, err := net.ListenUDP("udp", myAddr)
localPeer := state.Peers[config.LocalPeerIP]
var listenPort int
if localPeer != nil {
listenPort = int(localPeer.Port1)
}
vpnIP := netip.AddrFrom4([4]byte{
config.Network[0],
config.Network[1],
config.Network[2],
config.LocalPeerIP,
})
wgClient, err := createWGDevice(args.NetName, wgPrivKey, listenPort, vpnIP, config.Network)
if err != nil {
log.Fatalf("Failed to open UDP port: %v", err)
log.Fatalf("Failed to create WireGuard device: %v", err)
}
conn.SetReadBuffer(1024 * 1024 * 8)
conn.SetWriteBuffer(1024 * 1024 * 8)
// Wrap write function - this is necessary to avoid starvation.
writeLock := sync.Mutex{}
writeToUDPAddrPort := func(b []byte, addr netip.AddrPort) (n int, err error) {
writeLock.Lock()
n, err = conn.WriteToUDPAddrPort(b, addr)
for _, p := range state.Peers {
if p == nil || !p.Relay || p.PeerIP == config.LocalPeerIP {
continue
}
if len(p.WGPubKey) != wgtypes.KeyLen || len(p.PublicIP1) == 0 || p.Port1 == 0 {
continue
}
relayPubKey, err := wgtypes.NewKey(p.WGPubKey)
if err != nil {
logf("Failed to write packet: %v", err)
logf("Invalid relay WG key: %v", err)
continue
}
writeLock.Unlock()
return n, err
relayIP, ok := netip.AddrFromSlice(p.PublicIP1)
if !ok {
continue
}
relayEndpoint := netip.AddrPortFrom(relayIP.Unmap(), p.Port1)
if err := applyBaseConfig(wgClient, args.NetName, relayPubKey, relayEndpoint, config.Network); err != nil {
logf("Failed to apply relay base config: %v", err)
}
break
}
var localAddr netip.AddrPort
ip, localAddrValid := netip.AddrFromSlice(config.PublicIP)
if localAddrValid {
localAddr = netip.AddrPortFrom(ip, config.Port)
g := NewGlobals(config, netip.AddrPort{})
g.WGPrivKey = wgPrivKey
g.WGClient = wgClient
g.WGDevName = args.NetName
holePunch := NewHolePunch(g)
controlServer, err := NewControlServer(g, holePunch, args.NetName)
if err != nil {
log.Fatalf("Failed to create control server: %v", err)
}
rt := newRoutingTable(config.PeerIP, localAddr)
rtPtr := &atomic.Pointer[routingTable]{}
rtPtr.Store(&rt)
var endpointReporter *EndpointReporter
if localPeer != nil && localPeer.Relay {
if err := enableForwarding(args.NetName); err != nil {
log.Fatalf("Failed to enable IP forwarding: %v", err)
}
endpointReporter = NewEndpointReporter(g, controlServer, args.NetName)
}
ifReader := newIFReader(iface, writeToUDPAddrPort, rtPtr)
super := newSupervisor(writeToUDPAddrPort, rtPtr, config.PrivKey)
connReader := newConnReader(conn.ReadFromUDPAddrPort, writeToUDPAddrPort, iface, super.HandleControlMsg, rtPtr)
hubPoller, err := newHubPoller(config.PeerIP, conf.NetName, conf.HubAddress, conf.APIKey, super.HandleControlMsg)
hubPoller, err := NewHubPoller(g, holePunch, args.NetName, args.HubAddress, args.APIKey)
if err != nil {
log.Fatalf("Failed to create hub poller: %v", err)
}
go runStatusServer(g, statusSocketPath(args.NetName))
return &peerMain{
conf: config,
rt: rtPtr,
iface: iface,
ifReader: ifReader,
connReader: connReader,
Globals: g,
netName: args.NetName,
holePunch: holePunch,
controlServer: controlServer,
endpointReporter: endpointReporter,
hubPoller: hubPoller,
super: super,
lockFile: lockFile,
}
}
func (p *peerMain) Run() {
go p.ifReader.Run()
go p.connReader.Run()
p.super.Start()
if !p.rt.Load().LocalAddr.IsValid() {
go runMCWriter(p.conf.PeerIP, p.conf.PrivSignKey)
go runMCReader(p.rt, p.super.HandleControlMsg)
go p.controlServer.Run()
if p.endpointReporter != nil {
go p.endpointReporter.Run()
}
go RunMCWriter(p.Globals)
go RunMCReader(p.Globals, p.holePunch, p.netName)
go p.hubPoller.Run()
select {}
}
func initPeerWithHub(conf peerConfig) {
keys := generateKeys()
func initPeerWithHub(args mainArgs) {
privKey := generateWGKey()
pubKey := privKey.PublicKey()
initURL, err := url.Parse(conf.HubAddress)
initURL, err := url.Parse(args.HubAddress)
if err != nil {
log.Fatalf("Failed to parse hub URL: %v", err)
}
initURL.Path = "/peer/init/"
args := m.PeerInitArgs{
EncPubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
initArgs := m.PeerInitArgs{
WGPubKey: pubKey[:],
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(args); err != nil {
if err := json.NewEncoder(buf).Encode(initArgs); err != nil {
log.Fatalf("Failed to encode init args: %v", err)
}
@@ -146,7 +177,7 @@ func initPeerWithHub(conf peerConfig) {
if err != nil {
log.Fatalf("Failed to construct request: %v", err)
}
req.SetBasicAuth("", conf.APIKey)
req.SetBasicAuth("", args.APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
@@ -159,17 +190,29 @@ func initPeerWithHub(conf peerConfig) {
log.Fatalf("Failed to read response body: %v", err)
}
peerConfig := localConfig{}
if err := json.Unmarshal(data, &peerConfig.PeerConfig); err != nil {
if resp.StatusCode == http.StatusConflict {
log.Fatalf("WireGuard key already registered (HTTP 409). Delete and re-create the peer to re-register.")
}
if resp.StatusCode != http.StatusOK {
log.Fatalf("Hub returned unexpected status %d: %s", resp.StatusCode, data)
}
initResp := m.PeerInitResp{}
if err := json.Unmarshal(data, &initResp); err != nil {
log.Fatalf("Failed to parse configuration: %v\n%s", err, data)
}
peerConfig.PubKey = keys.PubKey
peerConfig.PrivKey = keys.PrivKey
peerConfig.PubSignKey = keys.PubSignKey
peerConfig.PrivSignKey = keys.PrivSignKey
config := LocalConfig{
LocalPeerIP: initResp.PeerIP,
Network: initResp.Network,
WGPrivKey: privKey.String(),
}
if err := storePeerConfig(conf.NetName, peerConfig); err != nil {
if err := storeNetworkState(args.NetName, initResp.NetworkState); err != nil {
log.Fatalf("Failed to store network state: %v", err)
}
if err := storePeerConfig(args.NetName, config); err != nil {
log.Fatalf("Failed to store configuration: %v", err)
}

View File

@@ -1,114 +0,0 @@
package peer
import (
"bytes"
"crypto/rand"
mrand "math/rand"
"net/netip"
"sync/atomic"
)
// A test peer.
type P struct {
cryptoKeys
RT *atomic.Pointer[routingTable]
Conn *TestUDPConn
IFace *TestIFace
ConnReader *connReader
IFReader *ifReader
}
func NewPeerForTesting(n *TestNetwork, ip byte, addr netip.AddrPort) P {
p := P{
cryptoKeys: generateKeys(),
RT: &atomic.Pointer[routingTable]{},
IFace: NewTestIFace(),
}
rt := newRoutingTable(ip, addr)
p.RT.Store(&rt)
p.Conn = n.NewUDPConn(addr)
//p.ConnWriter = NewConnWriter(p.Conn.WriteToUDPAddrPort, p.RT)
return p
}
func ConnectPeers(p1, p2 *P) {
rt1 := p1.RT.Load()
rt2 := p2.RT.Load()
ip1 := rt1.LocalIP
ip2 := rt2.LocalIP
rt1.Peers[ip2].Up = true
rt1.Peers[ip2].Direct = true
rt1.Peers[ip2].Relay = true
rt1.Peers[ip2].DirectAddr = rt2.LocalAddr
rt1.Peers[ip2].PubSignKey = p2.PubSignKey
rt1.Peers[ip2].ControlCipher = newControlCipher(p1.PrivKey, p2.PubKey)
rt1.Peers[ip2].DataCipher = newDataCipher()
rt2.Peers[ip1].Up = true
rt2.Peers[ip1].Direct = true
rt2.Peers[ip1].Relay = true
rt2.Peers[ip1].DirectAddr = rt1.LocalAddr
rt2.Peers[ip1].PubSignKey = p1.PubSignKey
rt2.Peers[ip1].ControlCipher = newControlCipher(p2.PrivKey, p1.PubKey)
rt2.Peers[ip1].DataCipher = rt1.Peers[ip2].DataCipher
}
func NewPeersForTesting() (p1, p2, p3 P) {
n := NewTestNetwork()
p1 = NewPeerForTesting(
n,
1,
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 100))
p2 = NewPeerForTesting(
n,
2,
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 2}), 200))
p3 = NewPeerForTesting(
n,
3,
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 3}), 300))
ConnectPeers(&p1, &p2)
ConnectPeers(&p1, &p3)
ConnectPeers(&p2, &p3)
return
}
func RandPacket() []byte {
n := mrand.Intn(1200)
b := make([]byte, n)
rand.Read(b)
return b
}
func ModifyPacket(in []byte) []byte {
x := make([]byte, 1)
for {
rand.Read(x)
out := bytes.Clone(in)
idx := mrand.Intn(len(out))
if out[idx] != x[0] {
out[idx] = x[0]
return out
}
}
}
// ----------------------------------------------------------------------------
type UnknownControlPacket struct {
TraceID uint64
}
func (p UnknownControlPacket) Marshal(buf []byte) []byte {
return newBinWriter(buf).Byte(255).Uint64(p.TraceID).Build()
}

28
peer/peerfsm.dot Normal file
View File

@@ -0,0 +1,28 @@
digraph d {
disconnected -> peerUpdating;
peerUpdating -> disconnected;
peerUpdating -> server;
peerUpdating -> clientInit;
server -> peerUpdating;
clientInit -> peerUpdating;
clientInit -> clientInit;
clientInit -> client;
client -> clientInit;
client -> peerUpdating;
clientInitializing -> clientSyncing;
clientSyncing -> clientInitializing;
clientSyncing -> clientUpIndirect;
clientSyncing -> clientUpDirect;
clientUpIndirect -> clientUpDirect;
clientUpIndirect -> clientInitializing;
clientUpDirect -> clientInitializing;
serverInitializing -> serverSyncing;
serverSyncing -> serverInitializing;
serverSyncing -> serverUpIndirect;
serverSyncing -> serverUpDirect;
serverUpIndirect -> serverUpDirect;
serverUpIndirect -> serverInitializing;
serverUpDirect -> serverInitializing;
}

View File

@@ -1,371 +0,0 @@
package peer
import (
"testing"
"vppn/m"
)
// ----------------------------------------------------------------------------
func TestPeerState_OnPeerUpdate_nilPeer(t *testing.T) {
h := NewPeerStateTestHarness()
h.PeerUpdate(nil)
assertType[*stateDisconnected](t, h.State)
}
func TestPeerState_OnPeerUpdate_publicLocalIsServer(t *testing.T) {
keys := generateKeys()
h := NewPeerStateTestHarness()
state := h.State.(*stateDisconnected)
state.localAddr = addrPort4(1, 1, 1, 2, 200)
peer := &m.Peer{
PeerIP: 3,
Port: 456,
PubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
}
h.PeerUpdate(peer)
assertEqual(t, h.Published.Up, false)
assertType[*stateServer](t, h.State)
}
/*
func TestPeerState_OnPeerUpdate_clientDirect(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
}
/*
func TestPeerState_OnPeerUpdate_clientRelayed(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
}
/*
func TestStateServer_directSyn(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
assertEqual(t, h.Published.Up, false)
synMsg := controlMsg[packetSyn]{
SrcIP: 3,
SrcAddr: addrPort4(1, 1, 1, 3, 300),
Packet: packetSyn{
TraceID: newTraceID(),
//SentAt: time.Now().UnixMilli(),
//SharedKeyType: 1,
Direct: true,
},
}
h.State = h.State.OnMsg(synMsg)
assertEqual(t, len(h.Sent), 1)
ack := assertType[packetAck](t, h.Sent[0].Packet)
assertEqual(t, ack.TraceID, synMsg.Packet.TraceID)
assertEqual(t, h.Sent[0].Peer.IP, 3)
assertEqual(t, ack.PossibleAddrs[0].IsValid(), false)
assertEqual(t, h.Published.Up, true)
}
func TestStateServer_relayedSyn(t *testing.T) {
h := NewPeerStateTestHarness()
state := h.ConfigServer_Relayed(t)
state.pubAddrs.Store(addrPort4(4, 5, 6, 7, 1234))
assertEqual(t, h.Published.Up, false)
synMsg := controlMsg[packetSyn]{
SrcIP: 3,
SrcAddr: addrPort4(1, 1, 1, 3, 300),
Packet: packetSyn{
TraceID: newTraceID(),
//SentAt: time.Now().UnixMilli(),
//SharedKeyType: 1,
Direct: false,
},
}
synMsg.Packet.PossibleAddrs[0] = addrPort4(1, 1, 1, 3, 300)
synMsg.Packet.PossibleAddrs[1] = addrPort4(2, 2, 2, 3, 300)
h.State = h.State.OnMsg(synMsg)
assertEqual(t, len(h.Sent), 3)
ack := assertType[packetAck](t, h.Sent[0].Packet)
assertEqual(t, ack.TraceID, synMsg.Packet.TraceID)
assertEqual(t, h.Sent[0].Peer.IP, 3)
assertEqual(t, ack.PossibleAddrs[0], addrPort4(4, 5, 6, 7, 1234))
assertEqual(t, ack.PossibleAddrs[1].IsValid(), false)
assertEqual(t, h.Published.Up, true)
assertType[packetProbe](t, h.Sent[1].Packet)
assertType[packetProbe](t, h.Sent[2].Packet)
assertEqual(t, h.Sent[1].Peer.DirectAddr, addrPort4(1, 1, 1, 3, 300))
assertEqual(t, h.Sent[2].Peer.DirectAddr, addrPort4(2, 2, 2, 3, 300))
}
func TestStateServer_onProbe(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
assertEqual(t, h.Published.Up, false)
probeMsg := controlMsg[packetProbe]{
SrcIP: 3,
SrcAddr: addrPort4(1, 1, 1, 3, 300),
Packet: packetProbe{TraceID: newTraceID()},
}
h.State = h.State.OnMsg(probeMsg)
assertEqual(t, len(h.Sent), 1)
probe := assertType[packetProbe](t, h.Sent[0].Packet)
assertEqual(t, probe.TraceID, probeMsg.Packet.TraceID)
assertEqual(t, h.Sent[0].Peer.DirectAddr, addrPort4(1, 1, 1, 3, 300))
}
func TestStateServer_OnPingTimer_timeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
synMsg := controlMsg[packetSyn]{
SrcIP: 3,
SrcAddr: addrPort4(1, 1, 1, 3, 300),
Packet: packetSyn{
TraceID: newTraceID(),
//SentAt: time.Now().UnixMilli(),
//SharedKeyType: 1,
Direct: true,
},
}
h.State = h.State.OnMsg(synMsg)
assertEqual(t, len(h.Sent), 1)
assertEqual(t, h.Published.Up, true)
// Ping shouldn't timeout.
h.OnPingTimer()
assertEqual(t, h.Published.Up, true)
// Advance the time, then ping.
state := assertType[*stateServer](t, h.State)
state.lastSeen = time.Now().Add(-timeoutInterval - time.Second)
h.OnPingTimer()
assertEqual(t, h.Published.Up, false)
}
func TestStateClientDirect_OnAck(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
h.State = h.State.OnMsg(ack)
assertEqual(t, h.Published.Up, true)
}
func TestStateClientDirect_OnAck_incorrectTraceID(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID + 1},
}
h.State = h.State.OnMsg(ack)
assertEqual(t, h.Published.Up, false)
}
func TestStateClientDirect_OnPingTimer(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
assertType[packetSyn](t, h.Sent[0].Packet)
h.OnPingTimer()
// On ping timer, another syn should be sent. Additionally, we should remain
// in the same state.
assertEqual(t, len(h.Sent), 2)
assertType[packetSyn](t, h.Sent[1].Packet)
assertType[*stateClientDirect](t, h.State)
assertEqual(t, h.Published.Up, false)
}
func TestStateClientDirect_OnPingTimer_timeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
h.State = h.State.OnMsg(ack)
assertEqual(t, h.Published.Up, true)
state := assertType[*stateClientDirect](t, h.State)
state.lastSeen = time.Now().Add(-(timeoutInterval + time.Second))
h.OnPingTimer()
// On ping timer, we should timeout, causing the client to reset. Another SYN
// will be sent when re-entering the state, but the connection should be down.
assertEqual(t, len(h.Sent), 2)
assertType[packetSyn](t, h.Sent[1].Packet)
assertType[*stateClientDirect](t, h.State)
assertEqual(t, h.Published.Up, false)
}
func TestStateClientRelayed_OnAck(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
h.State = h.State.OnMsg(ack)
assertEqual(t, h.Published.Up, true)
}
func TestStateClientRelayed_OnPingTimer_noAddrs(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
// If we haven't had an ack yet, we won't have addresses to probe. Therefore
// we'll have just one more syn packet sent.
h.OnPingTimer()
assertEqual(t, len(h.Sent), 2)
}
func TestStateClientRelayed_OnPingTimer_withAddrs(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
assertEqual(t, h.Published.Up, false)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{Packet: packetAck{TraceID: syn.TraceID}}
ack.Packet.PossibleAddrs[0] = addrPort4(1, 1, 1, 1, 300)
ack.Packet.PossibleAddrs[1] = addrPort4(1, 1, 1, 2, 300)
h.State = h.State.OnMsg(ack)
// Add a local discovery address. Note that the port will be configured port
// and no the one provided here.
h.State = h.State.OnMsg(controlMsg[packetLocalDiscovery]{
SrcIP: 3,
SrcAddr: addrPort4(2, 2, 2, 3, 300),
})
// We should see one SYN and three probe packets.
h.OnPingTimer()
assertEqual(t, len(h.Sent), 5)
assertType[packetSyn](t, h.Sent[1].Packet)
assertType[packetProbe](t, h.Sent[2].Packet)
assertType[packetProbe](t, h.Sent[3].Packet)
assertType[packetProbe](t, h.Sent[4].Packet)
assertEqual(t, h.Sent[2].Peer.DirectAddr, addrPort4(1, 1, 1, 1, 300))
assertEqual(t, h.Sent[3].Peer.DirectAddr, addrPort4(1, 1, 1, 2, 300))
assertEqual(t, h.Sent[4].Peer.DirectAddr, addrPort4(2, 2, 2, 3, 456))
}
func TestStateClientRelayed_OnPingTimer_timeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
// On entering the state, a SYN should have been sent.
assertEqual(t, len(h.Sent), 1)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
h.State = h.State.OnMsg(ack)
assertEqual(t, h.Published.Up, true)
state := assertType[*stateClientRelayed](t, h.State)
state.lastSeen = time.Now().Add(-(timeoutInterval + time.Second))
h.OnPingTimer()
// On ping timer, we should timeout, causing the client to reset. Another SYN
// will be sent when re-entering the state, but the connection should be down.
assertEqual(t, len(h.Sent), 2)
assertType[packetSyn](t, h.Sent[1].Packet)
assertType[*stateClientRelayed](t, h.State)
assertEqual(t, h.Published.Up, false)
}
func TestStateClientRelayed_OnProbe_unknownAddr(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
h.OnProbe(controlMsg[packetProbe]{
Packet: packetProbe{TraceID: newTraceID()},
})
assertType[*stateClientRelayed](t, h.State)
}
func TestStateClientRelayed_OnProbe_upgradeDirect(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
syn := assertType[packetSyn](t, h.Sent[0].Packet)
ack := controlMsg[packetAck]{Packet: packetAck{TraceID: syn.TraceID}}
ack.Packet.PossibleAddrs[0] = addrPort4(1, 1, 1, 1, 300)
ack.Packet.PossibleAddrs[1] = addrPort4(1, 1, 1, 2, 300)
h.State = h.State.OnMsg(ack)
h.OnPingTimer()
probe := assertType[packetProbe](t, h.Sent[2].Packet)
h.OnProbe(controlMsg[packetProbe]{Packet: probe})
assertType[*stateClientDirect](t, h.State)
}
*/

View File

@@ -1,148 +0,0 @@
package peer
import (
"net/netip"
"sync"
"sync/atomic"
"time"
"git.crumpington.com/lib/go/ratelimiter"
)
type supervisor struct {
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error)
staged routingTable
shared *atomic.Pointer[routingTable]
peers [256]*peerSuper
lock sync.Mutex
buf1 []byte
buf2 []byte
}
func newSupervisor(
writeToUDPAddrPort func([]byte, netip.AddrPort) (int, error),
rt *atomic.Pointer[routingTable],
privKey []byte,
) *supervisor {
routes := rt.Load()
s := &supervisor{
writeToUDPAddrPort: writeToUDPAddrPort,
staged: *routes,
shared: rt,
buf1: newBuf(),
buf2: newBuf(),
}
pubAddrs := newPubAddrStore(routes.LocalAddr)
for i := range s.peers {
state := &peerData{
publish: s.publish,
sendControlPacket: s.send,
pingTimer: time.NewTicker(timeoutInterval),
localIP: routes.LocalIP,
remoteIP: byte(i),
privKey: privKey,
localAddr: routes.LocalAddr,
pubAddrs: pubAddrs,
staged: routes.Peers[i],
limiter: ratelimiter.New(ratelimiter.Config{
FillPeriod: 20 * time.Millisecond,
MaxWaitCount: 1,
}),
}
s.peers[i] = newPeerSuper(state, state.pingTimer)
}
return s
}
func (s *supervisor) Start() {
for i := range s.peers {
go s.peers[i].Run()
}
}
func (s *supervisor) HandleControlMsg(destIP byte, msg any) {
s.peers[destIP].HandleControlMsg(msg)
}
func (s *supervisor) send(peer remotePeer, pkt marshaller) {
s.lock.Lock()
defer s.lock.Unlock()
enc := peer.EncryptControlPacket(pkt, s.buf1, s.buf2)
if peer.Direct {
s.writeToUDPAddrPort(enc, peer.DirectAddr)
return
}
relay, ok := s.staged.GetRelay()
if !ok {
return
}
enc = relay.EncryptDataPacket(peer.IP, enc, s.buf1)
s.writeToUDPAddrPort(enc, relay.DirectAddr)
}
func (s *supervisor) publish(rp remotePeer) {
s.lock.Lock()
defer s.lock.Unlock()
s.staged.Peers[rp.IP] = rp
s.ensureRelay()
copy := s.staged
s.shared.Store(&copy)
}
func (s *supervisor) ensureRelay() {
if _, ok := s.staged.GetRelay(); ok {
return
}
// TODO: Random selection? Something else?
for _, peer := range s.staged.Peers {
if peer.Up && peer.Direct && peer.Relay {
s.staged.RelayIP = peer.IP
return
}
}
}
// ----------------------------------------------------------------------------
type peerSuper struct {
messages chan any
state peerState
pingTimer *time.Ticker
}
func newPeerSuper(state *peerData, pingTimer *time.Ticker) *peerSuper {
return &peerSuper{
messages: make(chan any, 8),
state: initPeerState(state, nil),
pingTimer: pingTimer,
}
}
func (s *peerSuper) HandleControlMsg(msg any) {
select {
case s.messages <- msg:
default:
}
}
func (s *peerSuper) Run() {
for {
select {
case <-s.pingTimer.C:
s.state = s.state.OnMsg(pingTimerMsg{})
case raw := <-s.messages:
s.state = s.state.OnMsg(raw)
}
}
}

View File

@@ -1,86 +0,0 @@
package peer
import (
"net/netip"
"sort"
"sync"
"time"
)
type pubAddrStore struct {
lock sync.Mutex
localPub bool
localAddr netip.AddrPort
lastSeen map[netip.AddrPort]time.Time
addrList []netip.AddrPort
}
func newPubAddrStore(localAddr netip.AddrPort) *pubAddrStore {
return &pubAddrStore{
localPub: localAddr.IsValid(),
localAddr: localAddr,
lastSeen: map[netip.AddrPort]time.Time{},
addrList: make([]netip.AddrPort, 0, 32),
}
}
func (store *pubAddrStore) Store(addr netip.AddrPort) {
if store.localPub {
return
}
if !addr.IsValid() {
return
}
if addr.Addr().IsPrivate() {
return
}
store.lock.Lock()
defer store.lock.Unlock()
if _, exists := store.lastSeen[addr]; !exists {
store.addrList = append(store.addrList, addr)
}
store.lastSeen[addr] = time.Now()
store.sort()
}
func (store *pubAddrStore) Get() (addrs [8]netip.AddrPort) {
store.lock.Lock()
defer store.lock.Unlock()
store.clean()
if store.localPub {
addrs[0] = store.localAddr
return
}
copy(addrs[:], store.addrList)
return
}
func (store *pubAddrStore) clean() {
if store.localPub {
return
}
for ip, lastSeen := range store.lastSeen {
if time.Since(lastSeen) > timeoutInterval {
delete(store.lastSeen, ip)
}
}
store.addrList = store.addrList[:0]
for ip := range store.lastSeen {
store.addrList = append(store.addrList, ip)
}
store.sort()
}
func (store *pubAddrStore) sort() {
sort.Slice(store.addrList, func(i, j int) bool {
return store.lastSeen[store.addrList[j]].Before(store.lastSeen[store.addrList[i]])
})
}

View File

@@ -1,29 +0,0 @@
package peer
import (
"net/netip"
"testing"
"time"
)
func TestPubAddrStore(t *testing.T) {
s := newPubAddrStore(netip.AddrPort{})
l := []netip.AddrPort{
netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 1, 2, 3}), 20),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 2, 3}), 21),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{2, 1, 2, 3}), 22),
}
for i := range l {
s.Store(l[i])
time.Sleep(time.Millisecond)
}
s.clean()
l2 := s.Get()
if l2[0] != l[2] || l2[1] != l[1] || l2[2] != l[0] {
t.Fatal(l, l2)
}
}

View File

@@ -1,138 +0,0 @@
package peer
import (
"net/netip"
"sync/atomic"
"time"
)
// TODO: Remove
func newRemotePeer(ip byte) *remotePeer {
counter := uint64(time.Now().Unix()<<30 + 1)
return &remotePeer{
IP: ip,
counter: &counter,
dupCheck: newDupCheck(0),
}
}
// ----------------------------------------------------------------------------
type remotePeer struct {
localIP byte
IP byte // VPN IP of peer (last byte).
Up bool // True if data can be sent on the peer.
Relay bool // True if the peer is a relay.
Direct bool // True if this is a direct connection.
DirectAddr netip.AddrPort // Remote address if directly connected.
PubSignKey []byte
ControlCipher *controlCipher
DataCipher *dataCipher
counter *uint64 // For sending to. Atomic access only.
dupCheck *dupCheck // For receiving from. Not safe for concurrent use.
}
func (p remotePeer) EncryptDataPacket(destIP byte, data, out []byte) []byte {
h := header{
StreamID: dataStreamID,
Counter: atomic.AddUint64(p.counter, 1),
SourceIP: p.localIP,
DestIP: destIP,
}
return p.DataCipher.Encrypt(h, data, out)
}
// Decrypts and de-dups incoming data packets.
func (p remotePeer) DecryptDataPacket(h header, enc, out []byte) ([]byte, error) {
dec, ok := p.DataCipher.Decrypt(enc, out)
if !ok {
return nil, errDecryptionFailed
}
if p.dupCheck.IsDup(h.Counter) {
return nil, errDuplicateSeqNum
}
return dec, nil
}
// Peer must have a ControlCipher.
func (p remotePeer) EncryptControlPacket(pkt marshaller, tmp, out []byte) []byte {
tmp = pkt.Marshal(tmp)
h := header{
StreamID: controlStreamID,
Counter: atomic.AddUint64(p.counter, 1),
SourceIP: p.localIP,
DestIP: p.IP,
}
return p.ControlCipher.Encrypt(h, tmp, out)
}
// Returns a controlMsg[PacketType]. Peer must have a non-nil ControlCipher.
//
// This function also drops packets with duplicate sequence numbers.
func (p remotePeer) DecryptControlPacket(fromAddr netip.AddrPort, h header, enc, tmp []byte) (any, error) {
out, ok := p.ControlCipher.Decrypt(enc, tmp)
if !ok {
return nil, errDecryptionFailed
}
if p.dupCheck.IsDup(h.Counter) {
return nil, errDuplicateSeqNum
}
msg, err := parseControlMsg(h.SourceIP, fromAddr, out)
if err != nil {
return nil, err
}
return msg, nil
}
// ----------------------------------------------------------------------------
type routingTable struct {
// The LocalIP is the configured IP address of the local peer on the VPN.
//
// This value is constant.
LocalIP byte
// The LocalAddr is the configured local public address of the peer on the
// internet. If LocalAddr.IsValid(), then the local peer has a public
// address.
//
// This value is constant.
LocalAddr netip.AddrPort
// The remote peer configurations. These are updated by
Peers [256]remotePeer
// The current relay's VPN IP address, or zero if no relay is available.
RelayIP byte
}
func newRoutingTable(localIP byte, localAddr netip.AddrPort) routingTable {
rt := routingTable{
LocalIP: localIP,
LocalAddr: localAddr,
}
for i := range rt.Peers {
counter := uint64(time.Now().Unix()<<30 + 1)
rt.Peers[i] = remotePeer{
localIP: localIP,
IP: byte(i),
counter: &counter,
dupCheck: newDupCheck(0),
}
}
return rt
}
func (rt *routingTable) GetRelay() (remotePeer, bool) {
relay := rt.Peers[rt.RelayIP]
return relay, relay.Up && relay.Direct
}

View File

@@ -1,169 +0,0 @@
package peer
import (
"bytes"
"reflect"
"testing"
)
func TestRemotePeer_DecryptDataPacket(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
orig := RandPacket()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
enc := peer2.EncryptDataPacket(2, orig, newBuf())
h := parseHeader(enc)
if h.DestIP != 2 || h.SourceIP != 1 {
t.Fatal(h)
}
dec, err := peer1.DecryptDataPacket(h, enc, newBuf())
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(orig, dec) {
t.Fatal(dec)
}
}
func TestRemotePeer_DecryptDataPacket_packetAltered(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
orig := RandPacket()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
enc := peer2.EncryptDataPacket(2, orig, newBuf())
h := parseHeader(enc)
for range 2048 {
_, err := peer1.DecryptDataPacket(h, ModifyPacket(enc), newBuf())
if err == nil {
t.Fatal(enc)
}
}
}
func TestRemotePeer_DecryptDataPacket_duplicateSequenceNumber(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
orig := RandPacket()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
enc := peer2.EncryptDataPacket(2, orig, newBuf())
h := parseHeader(enc)
if _, err := peer1.DecryptDataPacket(h, enc, newBuf()); err != nil {
t.Fatal(err)
}
if _, err := peer1.DecryptDataPacket(h, enc, newBuf()); err == nil {
t.Fatal(err)
}
}
func TestRemotePeer_DecryptControlPacket(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
orig := packetProbe{TraceID: newTraceID()}
enc := peer2.EncryptControlPacket(orig, newBuf(), newBuf())
h := parseHeader(enc)
if h.DestIP != 2 || h.SourceIP != 1 {
t.Fatal(h)
}
ctrlMsg, err := peer1.DecryptControlPacket(p1.RT.Load().LocalAddr, h, enc, newBuf())
if err != nil {
t.Fatal(err)
}
dec, ok := ctrlMsg.(controlMsg[packetProbe])
if !ok {
t.Fatal(ctrlMsg)
}
if dec.SrcIP != 1 || dec.SrcAddr != p1.RT.Load().LocalAddr {
t.Fatal(dec)
}
if !reflect.DeepEqual(dec.Packet, orig) {
t.Fatal(dec)
}
}
func TestRemotePeer_DecryptControlPacket_packetAltered(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
orig := packetProbe{TraceID: newTraceID()}
enc := peer2.EncryptControlPacket(orig, newBuf(), newBuf())
h := parseHeader(enc)
if h.DestIP != 2 || h.SourceIP != 1 {
t.Fatal(h)
}
for range 2048 {
ctrlMsg, err := peer1.DecryptControlPacket(p1.RT.Load().LocalAddr, h, ModifyPacket(enc), newBuf())
if err == nil {
t.Fatal(ctrlMsg)
}
}
}
func TestRemotePeer_DecryptControlPacket_duplicateSequenceNumber(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
orig := packetProbe{TraceID: newTraceID()}
enc := peer2.EncryptControlPacket(orig, newBuf(), newBuf())
h := parseHeader(enc)
if h.DestIP != 2 || h.SourceIP != 1 {
t.Fatal(h)
}
if _, err := peer1.DecryptControlPacket(p1.RT.Load().LocalAddr, h, enc, newBuf()); err != nil {
t.Fatal(err)
}
if _, err := peer1.DecryptControlPacket(p1.RT.Load().LocalAddr, h, enc, newBuf()); err == nil {
t.Fatal(err)
}
}
func TestRemotePeer_DecryptControlPacket_unknownPacketType(t *testing.T) {
p1, p2, _ := NewPeersForTesting()
peer2 := p1.RT.Load().Peers[2]
peer1 := p2.RT.Load().Peers[1]
orig := UnknownControlPacket{TraceID: newTraceID()}
enc := peer2.EncryptControlPacket(orig, newBuf(), newBuf())
h := parseHeader(enc)
if h.DestIP != 2 || h.SourceIP != 1 {
t.Fatal(h)
}
if _, err := peer1.DecryptControlPacket(p1.RT.Load().LocalAddr, h, enc, newBuf()); err == nil {
t.Fatal(err)
}
}

View File

@@ -1,162 +0,0 @@
package peer
import (
"net/netip"
"time"
)
type sentProbe struct {
SentAt time.Time
Addr netip.AddrPort
}
type stateClient struct {
*peerData
lastSeen time.Time
syn packetSyn
probes map[uint64]sentProbe
}
func enterStateClient(data *peerData) peerState {
ip, ipValid := netip.AddrFromSlice(data.peer.PublicIP)
data.staged.Relay = data.peer.Relay && ipValid
data.staged.Direct = ipValid
data.staged.DirectAddr = netip.AddrPortFrom(ip, data.peer.Port)
data.publish(data.staged)
state := &stateClient{
peerData: data,
lastSeen: time.Now(),
syn: packetSyn{
TraceID: newTraceID(),
SharedKey: data.staged.DataCipher.Key(),
Direct: data.staged.Direct,
PossibleAddrs: data.pubAddrs.Get(),
},
probes: map[uint64]sentProbe{},
}
state.Send(state.staged, state.syn)
data.pingTimer.Reset(pingInterval)
state.logf("==> Client")
return state
}
func (s *stateClient) logf(str string, args ...any) {
s.peerData.logf("CLNT | "+str, args...)
}
func (s *stateClient) OnMsg(raw any) peerState {
switch msg := raw.(type) {
case peerUpdateMsg:
return initPeerState(s.peerData, msg.Peer)
case controlMsg[packetAck]:
s.onAck(msg)
case controlMsg[packetProbe]:
return s.onProbe(msg)
case controlMsg[packetLocalDiscovery]:
s.onLocalDiscovery(msg)
case pingTimerMsg:
return s.onPingTimer()
default:
s.logf("Ignoring message: %v", raw)
}
return s
}
func (s *stateClient) onAck(msg controlMsg[packetAck]) {
if msg.Packet.TraceID != s.syn.TraceID {
return
}
s.lastSeen = time.Now()
if !s.staged.Up {
s.staged.Up = true
s.publish(s.staged)
s.logf("Got ACK.")
}
if s.staged.Direct {
s.pubAddrs.Store(msg.Packet.ToAddr)
return
}
// Relayed below.
s.cleanProbes()
for _, addr := range msg.Packet.PossibleAddrs {
if !addr.IsValid() {
break
}
s.sendProbeTo(addr)
}
}
func (s *stateClient) onPingTimer() peerState {
if time.Since(s.lastSeen) > timeoutInterval {
if s.staged.Up {
s.logf("Timeout.")
}
return initPeerState(s.peerData, s.peer)
}
s.Send(s.staged, s.syn)
return s
}
func (s *stateClient) onProbe(msg controlMsg[packetProbe]) peerState {
if s.staged.Direct {
return s
}
s.cleanProbes()
sent, ok := s.probes[msg.Packet.TraceID]
if !ok {
return s
}
s.staged.Direct = true
s.staged.DirectAddr = sent.Addr
s.publish(s.staged)
s.syn.TraceID = newTraceID()
s.syn.Direct = true
s.Send(s.staged, s.syn)
s.logf("Successful probe to %v.", sent.Addr)
return s
}
func (s *stateClient) onLocalDiscovery(msg controlMsg[packetLocalDiscovery]) {
if s.staged.Direct {
return
}
// The source port will be the multicast port, so we'll have to
// construct the correct address using the peer's listed port.
addr := netip.AddrPortFrom(msg.SrcAddr.Addr(), s.peer.Port)
s.sendProbeTo(addr)
}
func (s *stateClient) cleanProbes() {
for key, sent := range s.probes {
if time.Since(sent.SentAt) > pingInterval {
delete(s.probes, key)
}
}
}
func (s *stateClient) sendProbeTo(addr netip.AddrPort) {
probe := packetProbe{TraceID: newTraceID()}
s.probes[probe.TraceID] = sentProbe{
SentAt: time.Now(),
Addr: addr,
}
s.logf("Probing %v...", addr)
s.SendTo(probe, addr)
}

View File

@@ -1,193 +0,0 @@
package peer
import (
"testing"
"time"
)
func TestStateClient_peerUpdate(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
h.PeerUpdate(nil)
assertType[*stateDisconnected](t, h.State)
}
func TestStateClient_initialPackets(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
assertEqual(t, len(h.Sent), 2)
assertType[packetInit](t, h.Sent[0].Packet)
assertType[packetSyn](t, h.Sent[1].Packet)
}
func TestStateClient_onAck_incorrectTraceID(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
h.Sent = h.Sent[:0]
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: newTraceID()},
}
h.OnAck(ack)
// Nothing should have happened.
assertType[*stateClient](t, h.State)
assertEqual(t, len(h.Sent), 0)
}
func TestStateClient_onAck_direct_downToUp(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
assertEqual(t, len(h.Sent), 2)
syn := assertType[packetSyn](t, h.Sent[1].Packet)
h.Sent = h.Sent[:0]
assertEqual(t, h.Published.Up, false)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
h.OnAck(ack)
assertEqual(t, len(h.Sent), 0)
}
func TestStateClient_onAck_relayed_sendsProbes(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
assertEqual(t, len(h.Sent), 2)
syn := assertType[packetSyn](t, h.Sent[1].Packet)
h.Sent = h.Sent[:0]
assertEqual(t, h.Published.Up, false)
ack := controlMsg[packetAck]{
Packet: packetAck{TraceID: syn.TraceID},
}
ack.Packet.PossibleAddrs[0] = addrPort4(1, 2, 3, 4, 100)
ack.Packet.PossibleAddrs[1] = addrPort4(2, 3, 4, 5, 200)
h.OnAck(ack)
assertEqual(t, len(h.Sent), 2)
assertType[packetProbe](t, h.Sent[0].Packet)
assertEqual(t, h.Sent[0].Peer.DirectAddr, ack.Packet.PossibleAddrs[0])
assertType[packetProbe](t, h.Sent[1].Packet)
assertEqual(t, h.Sent[1].Peer.DirectAddr, ack.Packet.PossibleAddrs[1])
}
func TestStateClient_onPing(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
h.Sent = h.Sent[:0]
h.OnPingTimer()
assertEqual(t, len(h.Sent), 1)
assertType[*stateClient](t, h.State)
assertType[packetSyn](t, h.Sent[0].Packet)
}
func TestStateClient_onPing_timeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
h.Sent = h.Sent[:0]
state := assertType[*stateClient](t, h.State)
state.lastSeen = time.Now().Add(-2 * timeoutInterval)
state.staged.Up = true
h.OnPingTimer()
newState := assertType[*stateClientInit](t, h.State)
assertEqual(t, newState.staged.Up, false)
assertEqual(t, len(h.Sent), 1)
assertType[packetInit](t, h.Sent[0].Packet)
}
func TestStateClient_onProbe_direct(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
h.Sent = h.Sent[:0]
probe := controlMsg[packetProbe]{
Packet: packetProbe{
TraceID: newTraceID(),
},
}
h.OnProbe(probe)
assertType[*stateClient](t, h.State)
assertEqual(t, len(h.Sent), 0)
}
func TestStateClient_onProbe_noMatch(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
h.Sent = h.Sent[:0]
probe := controlMsg[packetProbe]{
Packet: packetProbe{
TraceID: newTraceID(),
},
}
h.OnProbe(probe)
assertType[*stateClient](t, h.State)
assertEqual(t, len(h.Sent), 0)
}
func TestStateClient_onProbe_directUpgrade(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
state := assertType[*stateClient](t, h.State)
traceID := newTraceID()
state.probes[traceID] = sentProbe{
SentAt: time.Now(),
Addr: addrPort4(1, 2, 3, 4, 500),
}
probe := controlMsg[packetProbe]{
Packet: packetProbe{TraceID: traceID},
}
assertEqual(t, h.Published.Direct, false)
h.Sent = h.Sent[:0]
h.OnProbe(probe)
assertEqual(t, h.Published.Direct, true)
assertEqual(t, len(h.Sent), 1)
assertType[packetSyn](t, h.Sent[0].Packet)
}
func TestStateClient_onLocalDiscovery_direct(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientDirect(t)
h.Sent = h.Sent[:0]
pkt := controlMsg[packetLocalDiscovery]{
Packet: packetLocalDiscovery{},
}
h.OnLocalDiscovery(pkt)
assertType[*stateClient](t, h.State)
assertEqual(t, len(h.Sent), 0)
}
func TestStateClient_onLocalDiscovery_relayed(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientRelayed(t)
h.Sent = h.Sent[:0]
pkt := controlMsg[packetLocalDiscovery]{
SrcAddr: addrPort4(1, 2, 3, 4, 500),
Packet: packetLocalDiscovery{},
}
h.OnLocalDiscovery(pkt)
assertType[*stateClient](t, h.State)
assertEqual(t, len(h.Sent), 1)
assertType[packetProbe](t, h.Sent[0].Packet)
assertEqual(t, h.Sent[0].Peer.DirectAddr, addrPort4(1, 2, 3, 4, 456))
}

View File

@@ -1,104 +0,0 @@
package peer
import (
"net/netip"
"time"
)
type stateClientInit struct {
*peerData
startedAt time.Time
traceID uint64
}
func enterStateClientInit(data *peerData) peerState {
ip, ipValid := netip.AddrFromSlice(data.peer.PublicIP)
data.staged.Up = false
data.staged.Relay = false
data.staged.Direct = ipValid
data.staged.DirectAddr = netip.AddrPortFrom(ip, data.peer.Port)
data.staged.PubSignKey = data.peer.PubSignKey
data.staged.ControlCipher = newControlCipher(data.privKey, data.peer.PubKey)
data.staged.DataCipher = newDataCipher()
data.publish(data.staged)
state := &stateClientInit{
peerData: data,
startedAt: time.Now(),
traceID: newTraceID(),
}
state.sendInit()
data.pingTimer.Reset(pingInterval)
state.logf("==> ClientInit")
return state
}
func (s *stateClientInit) logf(str string, args ...any) {
s.peerData.logf("INIT | "+str, args...)
}
func (s *stateClientInit) OnMsg(raw any) peerState {
switch msg := raw.(type) {
case peerUpdateMsg:
return initPeerState(s.peerData, msg.Peer)
case controlMsg[packetInit]:
return s.onInit(msg)
case controlMsg[packetSyn]:
s.logf("Unexpected SYN")
return s
case controlMsg[packetAck]:
s.logf("Unexpected ACK")
return s
case controlMsg[packetProbe]:
return s
case controlMsg[packetLocalDiscovery]:
return s
case pingTimerMsg:
return s.onPing()
default:
s.logf("Ignoring message: %#v", raw)
return s
}
}
func (s *stateClientInit) onInit(msg controlMsg[packetInit]) peerState {
if msg.Packet.TraceID != s.traceID {
s.logf("Invalid trace ID on INIT.")
return s
}
s.logf("Got INIT version %d.", msg.Packet.Version)
return enterStateClient(s.peerData)
}
func (s *stateClientInit) onPing() peerState {
if time.Since(s.startedAt) < timeoutInterval {
s.sendInit()
return s
}
if s.staged.Direct {
s.staged.Direct = false
s.publish(s.staged)
s.startedAt = time.Now()
s.sendInit()
s.logf("Direct connection failed. Attempting indirect connection.")
return s
}
s.logf("Timeout.")
return initPeerState(s.peerData, s.peer)
}
func (s *stateClientInit) sendInit() {
s.traceID = newTraceID()
init := packetInit{
TraceID: s.traceID,
Direct: s.staged.Direct,
Version: version,
}
s.Send(s.staged, init)
}

View File

@@ -1,92 +0,0 @@
package peer
import (
"testing"
"time"
)
func TestPeerState_ClientInit_initWithIncorrectTraceID(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
// Should have sent the first init packet.
assertEqual(t, len(h.Sent), 1)
init := assertType[packetInit](t, h.Sent[0].Packet)
init.TraceID = newTraceID()
h.OnInit(controlMsg[packetInit]{Packet: init})
assertType[*stateClientInit](t, h.State)
}
func TestPeerState_ClientInit_init(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
// Should have sent the first init packet.
assertEqual(t, len(h.Sent), 1)
init := assertType[packetInit](t, h.Sent[0].Packet)
h.OnInit(controlMsg[packetInit]{Packet: init})
assertType[*stateClient](t, h.State)
}
func TestPeerState_ClientInit_onPing(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
// Should have sent the first init packet.
assertEqual(t, len(h.Sent), 1)
h.Sent = h.Sent[:0]
for range 3 {
h.OnPingTimer()
}
assertEqual(t, len(h.Sent), 3)
for i := range h.Sent {
assertType[packetInit](t, h.Sent[i].Packet)
}
}
func TestPeerState_ClientInit_onPingTimeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
state := assertType[*stateClientInit](t, h.State)
state.startedAt = time.Now().Add(-2 * timeoutInterval)
assertEqual(t, state.staged.Direct, true)
h.OnPingTimer()
// Should now try indirect connection.
state = assertType[*stateClientInit](t, h.State)
assertEqual(t, state.staged.Direct, false)
// Should re-initialize the peer after another timeout, so should be direct
// again.
state.startedAt = time.Now().Add(-2 * timeoutInterval)
h.OnPingTimer()
assertEqual(t, state.staged.Direct, true)
}
func TestPeerState_ClientInit_onPeerUpdate(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
h.PeerUpdate(nil)
// Should have moved into the client state due to timeout.
assertType[*stateDisconnected](t, h.State)
}
func TestPeerState_ClientInit_ignoreMessage(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigClientInit(t)
h.OnProbe(controlMsg[packetProbe]{})
// Shouldn't do anything.
assertType[*stateClientInit](t, h.State)
}

View File

@@ -1,50 +0,0 @@
package peer
import "net/netip"
type stateDisconnected struct {
*peerData
}
func enterStateDisconnected(data *peerData) peerState {
data.staged.Up = false
data.staged.Relay = false
data.staged.Direct = false
data.staged.DirectAddr = netip.AddrPort{}
data.staged.PubSignKey = nil
data.staged.ControlCipher = nil
data.staged.DataCipher = nil
data.publish(data.staged)
data.pingTimer.Stop()
return &stateDisconnected{data}
}
func (s *stateDisconnected) OnMsg(raw any) peerState {
switch msg := raw.(type) {
case peerUpdateMsg:
return initPeerState(s.peerData, msg.Peer)
case controlMsg[packetInit]:
s.logf("Unexpected INIT")
return s
case controlMsg[packetSyn]:
s.logf("Unexpected SYN")
return s
case controlMsg[packetAck]:
s.logf("Unexpected ACK")
return s
case controlMsg[packetProbe]:
s.logf("Unexpected probe")
return s
case controlMsg[packetLocalDiscovery]:
return s
case pingTimerMsg:
s.logf("Unexpected ping")
return s
default:
s.logf("Ignoring message: %#v", raw)
return s
}
}

View File

@@ -1,136 +0,0 @@
package peer
import (
"net/netip"
"time"
)
type stateServer struct {
*peerData
lastSeen time.Time
synTraceID uint64 // Last syn trace ID.
}
func enterStateServer(data *peerData) peerState {
data.staged.Up = false
data.staged.Relay = false
data.staged.Direct = false
data.staged.DirectAddr = netip.AddrPort{}
data.staged.PubSignKey = data.peer.PubSignKey
data.staged.ControlCipher = newControlCipher(data.privKey, data.peer.PubKey)
data.staged.DataCipher = nil
data.publish(data.staged)
data.pingTimer.Reset(pingInterval)
state := &stateServer{
peerData: data,
lastSeen: time.Now(),
}
state.logf("==> Server")
return state
}
func (s *stateServer) logf(str string, args ...any) {
s.peerData.logf("SRVR | "+str, args...)
}
func (s *stateServer) OnMsg(raw any) peerState {
switch msg := raw.(type) {
case peerUpdateMsg:
return initPeerState(s.peerData, msg.Peer)
case controlMsg[packetInit]:
return s.onInit(msg)
case controlMsg[packetSyn]:
return s.onSyn(msg)
case controlMsg[packetAck]:
s.logf("Unexpected ACK")
return s
case controlMsg[packetProbe]:
return s.onProbe(msg)
case controlMsg[packetLocalDiscovery]:
return s
case pingTimerMsg:
return s.onPingTimer()
default:
s.logf("Unexpected message: %#v", raw)
return s
}
}
func (s *stateServer) onInit(msg controlMsg[packetInit]) peerState {
s.staged.Up = false
s.staged.Direct = msg.Packet.Direct
s.staged.DirectAddr = msg.SrcAddr
s.publish(s.staged)
init := packetInit{
TraceID: msg.Packet.TraceID,
Direct: s.staged.Direct,
Version: version,
}
s.Send(s.staged, init)
return s
}
func (s *stateServer) onSyn(msg controlMsg[packetSyn]) peerState {
s.lastSeen = time.Now()
p := msg.Packet
// Before we can respond to this packet, we need to make sure the
// route is setup properly.
//
// The client will update the syn's TraceID whenever there's a change.
// The server will follow the client's request.
if p.TraceID != s.synTraceID || !s.staged.Up {
s.synTraceID = p.TraceID
s.staged.Up = true
s.staged.Direct = p.Direct
s.staged.DataCipher = newDataCipherFromKey(p.SharedKey)
s.staged.DirectAddr = msg.SrcAddr
s.publish(s.staged)
s.logf("Got SYN.")
}
// Always respond.
s.Send(s.staged, packetAck{
TraceID: p.TraceID,
ToAddr: s.staged.DirectAddr,
PossibleAddrs: s.pubAddrs.Get(),
})
if p.Direct {
return s
}
// Send probes if not a direct connection.
for _, addr := range msg.Packet.PossibleAddrs {
if !addr.IsValid() {
break
}
s.logf("Probing %v...", addr)
s.SendTo(packetProbe{TraceID: newTraceID()}, addr)
}
return s
}
func (s *stateServer) onProbe(msg controlMsg[packetProbe]) peerState {
if msg.SrcAddr.IsValid() {
s.logf("Probe response %v...", msg.SrcAddr)
s.SendTo(packetProbe{TraceID: msg.Packet.TraceID}, msg.SrcAddr)
}
return s
}
func (s *stateServer) onPingTimer() peerState {
if time.Since(s.lastSeen) > timeoutInterval && s.staged.Up {
s.staged.Up = false
s.publish(s.staged)
s.logf("Timeout.")
}
return s
}

View File

@@ -1,164 +0,0 @@
package peer
import (
"testing"
"time"
)
func TestStateServer_peerUpdate(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Public(t)
h.PeerUpdate(nil)
assertType[*stateDisconnected](t, h.State)
}
func TestStateServer_onInit(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Public(t)
msg := controlMsg[packetInit]{
SrcIP: 3,
SrcAddr: addrPort4(1, 2, 3, 4, 1000),
Packet: packetInit{
TraceID: newTraceID(),
Direct: true,
Version: 4,
},
}
h.OnInit(msg)
assertEqual(t, len(h.Sent), 1)
assertEqual(t, h.Sent[0].Peer.DirectAddr, msg.SrcAddr)
resp := assertType[packetInit](t, h.Sent[0].Packet)
assertEqual(t, msg.Packet.TraceID, resp.TraceID)
assertEqual(t, resp.Version, version)
}
func TestStateServer_onSynDirect(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Public(t)
msg := controlMsg[packetSyn]{
SrcIP: 3,
SrcAddr: addrPort4(1, 2, 3, 4, 1000),
Packet: packetSyn{
TraceID: newTraceID(),
Direct: true,
},
}
msg.Packet.PossibleAddrs[0] = addrPort4(1, 1, 1, 1, 1000)
msg.Packet.PossibleAddrs[1] = addrPort4(1, 1, 1, 2, 2000)
h.OnSyn(msg)
assertEqual(t, len(h.Sent), 1)
assertEqual(t, h.Sent[0].Peer.DirectAddr, msg.SrcAddr)
resp := assertType[packetAck](t, h.Sent[0].Packet)
assertEqual(t, msg.Packet.TraceID, resp.TraceID)
}
func TestStateServer_onSynRelayed(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
msg := controlMsg[packetSyn]{
SrcIP: 3,
SrcAddr: addrPort4(1, 2, 3, 4, 1000),
Packet: packetSyn{
TraceID: newTraceID(),
},
}
msg.Packet.PossibleAddrs[0] = addrPort4(1, 1, 1, 1, 1000)
msg.Packet.PossibleAddrs[1] = addrPort4(1, 1, 1, 2, 2000)
h.OnSyn(msg)
assertEqual(t, len(h.Sent), 3)
assertEqual(t, h.Sent[0].Peer.DirectAddr, msg.SrcAddr)
resp := assertType[packetAck](t, h.Sent[0].Packet)
assertEqual(t, msg.Packet.TraceID, resp.TraceID)
for i, pkt := range h.Sent[1:] {
assertEqual(t, pkt.Peer.DirectAddr, msg.Packet.PossibleAddrs[i])
assertType[packetProbe](t, pkt.Packet)
}
}
func TestStateServer_onProbe(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
msg := controlMsg[packetProbe]{
SrcIP: 3,
Packet: packetProbe{
TraceID: newTraceID(),
},
}
h.Sent = h.Sent[:0]
h.OnProbe(msg)
assertEqual(t, len(h.Sent), 0)
}
func TestStateServer_onProbe_valid(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
msg := controlMsg[packetProbe]{
SrcIP: 3,
SrcAddr: addrPort4(1, 2, 3, 4, 100),
Packet: packetProbe{
TraceID: newTraceID(),
},
}
h.Sent = h.Sent[:0]
h.OnProbe(msg)
assertEqual(t, len(h.Sent), 1)
assertType[packetProbe](t, h.Sent[0].Packet)
assertEqual(t, h.Sent[0].Peer.DirectAddr, msg.SrcAddr)
}
func TestStateServer_onPing(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
h.Sent = h.Sent[:0]
h.OnPingTimer()
assertEqual(t, len(h.Sent), 0)
assertType[*stateServer](t, h.State)
}
func TestStateServer_onPing_timeout(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
h.Sent = h.Sent[:0]
state := assertType[*stateServer](t, h.State)
state.staged.Up = true
state.lastSeen = time.Now().Add(-2 * timeoutInterval)
h.OnPingTimer()
state = assertType[*stateServer](t, h.State)
assertEqual(t, len(h.Sent), 0)
assertEqual(t, state.staged.Up, false)
}
func TestStateServer_onLocalDiscovery(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
msg := controlMsg[packetLocalDiscovery]{
SrcIP: 3,
SrcAddr: addrPort4(1, 2, 3, 4, 100),
}
h.OnLocalDiscovery(msg)
assertType[*stateServer](t, h.State)
}
func TestStateServer_onAck(t *testing.T) {
h := NewPeerStateTestHarness()
h.ConfigServer_Relayed(t)
msg := controlMsg[packetAck]{}
h.OnAck(msg)
assertType[*stateServer](t, h.State)
}

View File

@@ -1,151 +0,0 @@
package peer
import (
"net/netip"
"testing"
"time"
"vppn/m"
"git.crumpington.com/lib/go/ratelimiter"
)
type PeerStateControlMsg struct {
Peer remotePeer
Packet any
}
type PeerStateTestHarness struct {
data *peerData
State peerState
Published remotePeer
Sent []PeerStateControlMsg
}
func NewPeerStateTestHarness() *PeerStateTestHarness {
h := &PeerStateTestHarness{}
keys := generateKeys()
state := &peerData{
publish: func(rp remotePeer) {
h.Published = rp
},
sendControlPacket: func(rp remotePeer, pkt marshaller) {
h.Sent = append(h.Sent, PeerStateControlMsg{rp, pkt})
},
pingTimer: time.NewTicker(pingInterval),
localIP: 2,
remoteIP: 3,
privKey: keys.PrivKey,
pubAddrs: newPubAddrStore(netip.AddrPort{}),
limiter: ratelimiter.New(ratelimiter.Config{
FillPeriod: 20 * time.Millisecond,
MaxWaitCount: 1,
}),
}
h.data = state
h.State = enterStateDisconnected(state)
return h
}
func (h *PeerStateTestHarness) PeerUpdate(p *m.Peer) {
h.State = h.State.OnMsg(peerUpdateMsg{p})
}
func (h *PeerStateTestHarness) OnInit(msg controlMsg[packetInit]) {
h.State = h.State.OnMsg(msg)
}
func (h *PeerStateTestHarness) OnSyn(msg controlMsg[packetSyn]) {
h.State = h.State.OnMsg(msg)
}
func (h *PeerStateTestHarness) OnAck(msg controlMsg[packetAck]) {
h.State = h.State.OnMsg(msg)
}
func (h *PeerStateTestHarness) OnProbe(msg controlMsg[packetProbe]) {
h.State = h.State.OnMsg(msg)
}
func (h *PeerStateTestHarness) OnLocalDiscovery(msg controlMsg[packetLocalDiscovery]) {
h.State = h.State.OnMsg(msg)
}
func (h *PeerStateTestHarness) OnPingTimer() {
h.State = h.State.OnMsg(pingTimerMsg{})
}
func (h *PeerStateTestHarness) ConfigServer_Public(t *testing.T) *stateServer {
keys := generateKeys()
state := h.State.(*stateDisconnected)
state.localAddr = addrPort4(1, 1, 1, 2, 200)
peer := &m.Peer{
PeerIP: 3,
PublicIP: []byte{1, 1, 1, 3},
Port: 456,
PubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
}
h.PeerUpdate(peer)
assertEqual(t, h.Published.Up, false)
return assertType[*stateServer](t, h.State)
}
func (h *PeerStateTestHarness) ConfigServer_Relayed(t *testing.T) *stateServer {
keys := generateKeys()
peer := &m.Peer{
PeerIP: 3,
Port: 456,
PubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
}
h.PeerUpdate(peer)
assertEqual(t, h.Published.Up, false)
return assertType[*stateServer](t, h.State)
}
func (h *PeerStateTestHarness) ConfigClientInit(t *testing.T) *stateClientInit {
// Remote IP should be less than local IP.
h.data.localIP = 4
keys := generateKeys()
peer := &m.Peer{
PeerIP: 3,
PublicIP: []byte{1, 2, 3, 4},
Port: 456,
PubKey: keys.PubKey,
PubSignKey: keys.PubSignKey,
}
h.PeerUpdate(peer)
assertEqual(t, h.Published.Up, false)
return assertType[*stateClientInit](t, h.State)
}
func (h *PeerStateTestHarness) ConfigClientDirect(t *testing.T) *stateClient {
h.ConfigClientInit(t)
init := assertType[packetInit](t, h.Sent[0].Packet)
h.OnInit(controlMsg[packetInit]{
Packet: init,
})
return assertType[*stateClient](t, h.State)
}
func (h *PeerStateTestHarness) ConfigClientRelayed(t *testing.T) *stateClient {
h.ConfigClientInit(t)
state := assertType[*stateClientInit](t, h.State)
state.peer.PublicIP = nil // Force relay.
init := assertType[packetInit](t, h.Sent[0].Packet)
h.OnInit(controlMsg[packetInit]{
Packet: init,
})
return assertType[*stateClient](t, h.State)
}

View File

@@ -1,109 +0,0 @@
package peer
import (
"fmt"
"log"
"net/netip"
"strings"
"time"
"vppn/m"
"git.crumpington.com/lib/go/ratelimiter"
)
type peerState interface {
OnMsg(raw any) peerState
}
// ----------------------------------------------------------------------------
type peerData struct {
// Output.
publish func(remotePeer)
sendControlPacket func(remotePeer, marshaller)
pingTimer *time.Ticker
// Immutable data.
localIP byte
remoteIP byte
privKey []byte
localAddr netip.AddrPort // If valid, then local peer is publicly accessible.
pubAddrs *pubAddrStore
// The purpose of this state machine is to manage the RemotePeer object,
// publishing it as necessary.
staged remotePeer // Local copy of shared data. See publish().
// Mutable peer data.
peer *m.Peer
// We rate limit per remote endpoint because if we don't we tend to lose
// packets.
limiter *ratelimiter.Limiter
}
func (s *peerData) logf(format string, args ...any) {
b := strings.Builder{}
name := ""
if s.peer != nil {
name = s.peer.Name
}
b.WriteString(fmt.Sprintf("%03d", s.remoteIP))
b.WriteString(fmt.Sprintf("%30s: ", name))
if s.staged.Direct {
b.WriteString("DIRECT | ")
} else {
b.WriteString("RELAYED | ")
}
if s.staged.Up {
b.WriteString("UP | ")
} else {
b.WriteString("DOWN | ")
}
log.Printf(b.String()+format, args...)
}
// ----------------------------------------------------------------------------
func (s *peerData) SendTo(pkt marshaller, addr netip.AddrPort) {
if !addr.IsValid() {
return
}
route := s.staged
route.Direct = true
route.DirectAddr = addr
s.Send(route, pkt)
}
func (s *peerData) Send(peer remotePeer, pkt marshaller) {
if err := s.limiter.Limit(); err != nil {
s.logf("Rate limited.")
return
}
s.sendControlPacket(peer, pkt)
}
func initPeerState(data *peerData, peer *m.Peer) peerState {
data.peer = peer
if peer == nil {
return enterStateDisconnected(data)
}
if _, isValid := netip.AddrFromSlice(peer.PublicIP); isValid {
if data.localAddr.IsValid() && data.localIP < data.remoteIP {
return enterStateServer(data)
}
return enterStateClientInit(data)
}
if data.localAddr.IsValid() || data.localIP < data.remoteIP {
return enterStateServer(data)
}
return enterStateClientInit(data)
}

50
peer/statusserver.go Normal file
View File

@@ -0,0 +1,50 @@
package peer
import (
"encoding/json"
"log"
"net"
"net/http"
"net/netip"
"os"
)
type StatusReport struct {
LocalPeerIP byte
Network []byte
RelayPeerIP byte
Remotes []RemoteStatus
}
type RemoteStatus struct {
PeerIP byte
Up bool
Name string
PublicIP []byte
Port uint16
Relay bool
Server bool
Direct bool
DirectAddr netip.AddrPort
}
func runStatusServer(g Globals, socketPath string) {
_ = os.RemoveAll(socketPath)
handler := func(w http.ResponseWriter, r *http.Request) {
report := StatusReport{
LocalPeerIP: g.LocalPeerIP,
Network: g.Network,
}
json.NewEncoder(w).Encode(report)
}
server := http.Server{Handler: http.HandlerFunc(handler)}
unixListener, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalf("Failed to bind to unix socket: %v", err)
}
if err := server.Serve(unixListener); err != nil {
log.Fatalf("Failed to serve on unix socket: %v", err)
}
}

View File

@@ -1,26 +0,0 @@
package peer
import (
"net/netip"
"testing"
)
func addrPort4(a, b, c, d byte, port uint16) netip.AddrPort {
return netip.AddrPortFrom(netip.AddrFrom4([4]byte{a, b, c, d}), port)
}
func assertType[T any](t *testing.T, obj any) T {
t.Helper()
x, ok := obj.(T)
if !ok {
t.Fatalf("invalid type: %#v", obj)
}
return x
}
func assertEqual[T comparable](t *testing.T, a, b T) {
t.Helper()
if a != b {
t.Fatal(a, " != ", b)
}
}

View File

@@ -0,0 +1,224 @@
// Package wginterface demonstrates creating and destroying a WireGuard network
// interface using only raw system calls — no netlink library.
//
// Creating a typed interface (kind = "wireguard") requires the NETLINK_ROUTE
// protocol; there is no ioctl path for it. Everything else — assigning an IP
// address and bringing the link up — can be done with the older AF_INET ioctl
// interface, exactly as one would for a TUN device.
//
// The package requires CAP_NET_ADMIN and the wireguard kernel module.
package wginterface
import (
"encoding/binary"
"fmt"
"net"
"slices"
"golang.org/x/sys/unix"
)
// Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to
// it, and brings it up.
func Create(name string, vpnIP net.IP, prefixLen int) error {
if err := nlNewLink(name); err != nil {
return fmt.Errorf("failed to create wireguard link: %w", err)
}
if err := ioctlSetAddr(name, vpnIP, prefixLen); err != nil {
_ = Delete(name)
return fmt.Errorf("assign address: %w", err)
}
if err := ioctlLinkUp(name); err != nil {
_ = Delete(name)
return fmt.Errorf("link up: %w", err)
}
return nil
}
// Delete removes the named interface.
func Delete(name string) error {
return nlDelLink(name)
}
// ---------------------------------------------------------------------------
// Netlink link management
//
// Creating a WireGuard interface requires an RTM_NEWLINK message with a nested
// IFLA_LINKINFO attribute whose IFLA_INFO_KIND is "wireguard". The full
// message layout is:
//
// nlmsghdr (16 bytes)
// ifinfomsg (16 bytes, all zeros for a new link)
// rtattr IFLA_IFNAME → name + \0
// rtattr IFLA_LINKINFO
// rtattr IFLA_INFO_KIND → "wireguard" + \0
//
// All multi-byte integers are in native byte order (little-endian on
// x86/arm64). Every attribute is padded to a 4-byte boundary; the len field
// in the header records the unpadded length but the attribute occupies the
// padded size.
const (
nlmsgHdrLen = 16 // sizeof(struct nlmsghdr)
sizeofIfInfo = 16 // sizeof(struct ifinfomsg)
// Attribute types not exposed by the unix package at the level we need.
iflaLinkInfo = 18 // IFLA_LINKINFO — container for link-type attributes
iflaInfoKind = 1 // IFLA_INFO_KIND — link type string, nested inside IFLA_LINKINFO
)
// nlNewLink creates the wireguard interface using Netlink.
func nlNewLink(name string) error {
// Build innermost attribute first, then wrap outward.
infoKind := nlAttr(iflaInfoKind, cstring("wireguard"))
linkInfo := nlAttr(iflaLinkInfo, infoKind)
ifName := nlAttr(unix.IFLA_IFNAME, cstring(name))
// ifinfomsg: all-zero = AF_UNSPEC, no index, no flags (kernel assigns index).
ifInfo := make([]byte, sizeofIfInfo)
payload := slices.Concat(ifInfo, ifName, linkInfo)
flags := uint16(unix.NLM_F_REQUEST | unix.NLM_F_ACK | unix.NLM_F_CREATE | unix.NLM_F_EXCL)
return nlRoundtrip(unix.RTM_NEWLINK, flags, payload)
}
func nlDelLink(name string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
// For RTM_DELLINK the kernel identifies the link by ifi_index. ifi_index
// sits at byte offset 4 in the ifinfomsg struct.
ifInfo := make([]byte, sizeofIfInfo)
binary.NativeEndian.PutUint32(ifInfo[4:8], uint32(iface.Index))
return nlRoundtrip(unix.RTM_DELLINK, uint16(unix.NLM_F_REQUEST|unix.NLM_F_ACK), ifInfo)
}
// nlRoundtrip opens a NETLINK_ROUTE socket, sends one request, reads the
// NLMSG_ERROR acknowledgement, and closes the socket.
func nlRoundtrip(msgType uint16, flags uint16, payload []byte) error {
fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE)
if err != nil {
return fmt.Errorf("socket: %w", err)
}
defer unix.Close(fd)
if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
return fmt.Errorf("bind: %w", err)
}
msg := nlMsg(msgType, flags, payload)
if err := unix.Sendto(fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
return fmt.Errorf("sendto: %w", err)
}
resp := make([]byte, 4096)
n, _, err := unix.Recvfrom(fd, resp, 0)
if err != nil {
return fmt.Errorf("recvfrom: %w", err)
}
return nlAckErr(resp[:n])
}
// nlMsg prepends an nlmsghdr to payload.
func nlMsg(msgType uint16, flags uint16, payload []byte) []byte {
buf := make([]byte, nlmsgHdrLen+len(payload))
binary.NativeEndian.PutUint32(buf[0:4], uint32(len(buf))) // nlmsg_len
binary.NativeEndian.PutUint16(buf[4:6], msgType) // nlmsg_type
binary.NativeEndian.PutUint16(buf[6:8], flags) // nlmsg_flags
binary.NativeEndian.PutUint32(buf[8:12], 1) // nlmsg_seq
binary.NativeEndian.PutUint32(buf[12:16], 0) // nlmsg_pid (0 = kernel)
copy(buf[nlmsgHdrLen:], payload)
return buf
}
// nlAckErr parses an NLMSG_ERROR response. The error field is a negated errno
// (0 = success, -EEXIST = interface exists, etc.).
func nlAckErr(resp []byte) error {
if len(resp) < nlmsgHdrLen+4 {
return fmt.Errorf("netlink response too short (%d bytes)", len(resp))
}
if binary.NativeEndian.Uint16(resp[4:6]) != unix.NLMSG_ERROR {
return fmt.Errorf("unexpected nlmsg_type %d", binary.NativeEndian.Uint16(resp[4:6]))
}
// Error code follows the nlmsghdr; it is a signed int32 holding -errno.
code := int32(binary.NativeEndian.Uint32(resp[nlmsgHdrLen:]))
if code != 0 {
return unix.Errno(-code)
}
return nil
}
// nlAttr encodes one netlink attribute: [len:u16][type:u16][data][pad to 4
// bytes]. The len field counts the header + data (before padding); the
// allocation is padded so that the next attribute starts on a 4-byte boundary.
func nlAttr(attrType uint16, data []byte) []byte {
const hdr = 4
attrLen := hdr + len(data)
padded := (attrLen + 3) &^ 3
buf := make([]byte, padded)
binary.NativeEndian.PutUint16(buf[0:2], uint16(attrLen))
binary.NativeEndian.PutUint16(buf[2:4], attrType)
copy(buf[hdr:], data)
return buf
}
// ---------------------------------------------------------------------------
// ioctl-based address assignment and link-up
//
// These operations could also be done via RTM_NEWADDR / RTM_NEWLINK netlink
// messages, but the AF_INET ioctl interface is simpler.
func ioctlSetAddr(name string, ip net.IP, prefixLen int) error {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return err
}
defer unix.Close(fd)
req, err := unix.NewIfreq(name)
if err != nil {
return err
}
if err := req.SetInet4Addr(ip.To4()); err != nil {
return err
}
if err := unix.IoctlIfreq(fd, unix.SIOCSIFADDR, req); err != nil {
return err
}
req, err = unix.NewIfreq(name)
if err != nil {
return err
}
mask := net.CIDRMask(prefixLen, 32)
if err := req.SetInet4Addr([]byte(mask)); err != nil {
return err
}
return unix.IoctlIfreq(fd, unix.SIOCSIFNETMASK, req)
}
func ioctlLinkUp(name string) error {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
if err != nil {
return err
}
defer unix.Close(fd)
req, err := unix.NewIfreq(name)
if err != nil {
return err
}
if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, req); err != nil {
return err
}
req.SetUint16(req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING)
return unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, req)
}
// cstring returns b as a null-terminated byte slice.
func cstring(s string) []byte {
return append([]byte(s), 0)
}

165
peer/wginterface/manage.go Normal file
View File

@@ -0,0 +1,165 @@
package wginterface
import (
"fmt"
"net"
"net/netip"
"os"
"time"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
// RekeyTimeout is the WireGuard session lifetime before a new handshake
// is initiated. Sessions older than this but younger than SessionTimeout
// remain valid.
RekeyTimeout = 120 * time.Second
// SessionTimeout is the WireGuard session lifetime after which sessions
// are rejected. A peer with LastHandshakeTime older than this is
// effectively disconnected.
SessionTimeout = 180 * time.Second
)
var (
probeKeepalive = 5 * time.Second
zeroKeepalive = time.Duration(0)
)
// Device wraps a wgctrl client bound to a named WireGuard interface.
type Device struct {
client *wgctrl.Client
name string
}
// Open attaches to an existing WireGuard interface.
func Open(name string) (*Device, error) {
client, err := wgctrl.New()
if err != nil {
return nil, fmt.Errorf("wgctrl: %w", err)
}
return &Device{client: client, name: name}, nil
}
// Close releases the underlying wgctrl client.
func (d *Device) Close() error {
return d.client.Close()
}
// Configure sets the device's private key and UDP listen port.
func (d *Device) Configure(privKey wgtypes.Key, listenPort int) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
PrivateKey: &privKey,
ListenPort: &listenPort,
})
}
// Peers returns the current state of all peers on the device.
func (d *Device) Peers() ([]wgtypes.Peer, error) {
dev, err := d.client.Device(d.name)
if err != nil {
return nil, fmt.Errorf("get device %q: %w", d.name, err)
}
return dev.Peers, nil
}
// Peer returns the current state of a single peer by public key.
func (d *Device) Peer(pubKey wgtypes.Key) (wgtypes.Peer, error) {
peers, err := d.Peers()
if err != nil {
return wgtypes.Peer{}, err
}
for _, p := range peers {
if p.PublicKey == pubKey {
return p, nil
}
}
return wgtypes.Peer{}, fmt.Errorf("peer %v not found in %q", pubKey, d.name)
}
// SetRelay configures the relay peer with AllowedIPs covering the entire VPN
// network prefix. This is the fallback route for all VPN traffic.
func (d *Device) SetRelay(pubKey wgtypes.Key, endpoint netip.AddrPort, network netip.Prefix) error {
masked := network.Masked()
a4 := masked.Addr().As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(masked.Bits(), 32),
}},
ReplaceAllowedIPs: true,
}},
})
}
// AddProbe adds a peer with no AllowedIPs and a 5s keepalive. WireGuard will
// attempt handshakes without routing any traffic through this peer yet.
func (d *Device) AddProbe(pubKey wgtypes.Key, endpoint netip.AddrPort) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &probeKeepalive,
}},
})
}
// Promote upgrades a probe entry to a /32 AllowedIPs and removes the probe
// keepalive, causing WireGuard to prefer this peer's direct path over the
// relay's wider route.
func (d *Device) Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &zeroKeepalive,
}},
})
}
// AddDirect adds a peer with a known endpoint and /32 AllowedIPs in one step,
// for peers with a stable public endpoint reported by the hub.
func (d *Device) AddDirect(pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &zeroKeepalive,
}},
})
}
// RemovePeer removes a peer from the device.
func (d *Device) RemovePeer(pubKey wgtypes.Key) error {
return d.client.ConfigureDevice(d.name, wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Remove: true,
}},
})
}
// EnableForwarding enables IPv4 forwarding on the interface, required for
// relay peers that forward traffic between VPN peers.
func (d *Device) EnableForwarding() error {
path := fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", d.name)
return os.WriteFile(path, []byte("1\n"), 0644)
}

View File

@@ -0,0 +1,303 @@
//go:build integration
package wginterface_test
import (
"fmt"
"log"
"net"
"net/netip"
"os"
"strings"
"testing"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"vppn/peer/wginterface"
)
const (
testBasePort = 59100
)
func TestMain(m *testing.M) {
if os.Getuid() != 0 {
fmt.Fprintln(os.Stderr, "wginterface integration tests require root; skipping")
os.Exit(0)
}
os.Exit(m.Run())
}
type testPeer struct {
Name string
VpnIP netip.Addr
Port int
PrivKey wgtypes.Key
PubKey wgtypes.Key
Dev *wginterface.Device
}
func (p *testPeer) Endpoint() netip.AddrPort {
return netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), uint16(p.Port))
}
func newTestPeer(t *testing.T, name string, vpnIP netip.Addr, port int) *testPeer {
t.Helper()
privKey, err := wgtypes.GenerateKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
a4 := vpnIP.As4()
if err := wginterface.Create(name, net.IP(a4[:]), 24); err != nil {
t.Fatalf("create %s: %v", name, err)
}
t.Cleanup(func() {
if err := wginterface.Delete(name); err != nil {
log.Printf("Failed to delete interface %s: %v", name, err)
}
})
dev, err := wginterface.Open(name)
if err != nil {
t.Fatalf("open %s: %v", name, err)
}
t.Cleanup(func() { dev.Close() })
if err := dev.Configure(privKey, port); err != nil {
t.Fatalf("configure %s: %v", name, err)
}
return &testPeer{
Name: name,
VpnIP: vpnIP,
Port: port,
PrivKey: privKey,
PubKey: privKey.PublicKey(),
Dev: dev,
}
}
// waitHandshake polls until the named peer has completed a handshake or the timeout elapses.
func waitHandshake(t *testing.T, dev *wginterface.Device, pubKey wgtypes.Key, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
p, err := dev.Peer(pubKey)
if err != nil {
t.Fatalf("peer lookup: %v", err)
}
if !p.LastHandshakeTime.IsZero() {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("no handshake within %v", timeout)
}
func TestDirectHandshake(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil {
t.Fatalf("p1 AddDirect: %v", err)
}
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
waitHandshake(t, p2.Dev, p1.PubKey, 30*time.Second)
}
func TestProbeAndPromote(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
// p2 needs a peer entry for p1 so it can respond to the handshake initiation.
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil {
t.Fatalf("AddProbe: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil {
t.Fatalf("Promote: %v", err)
}
peer, err := p1.Dev.Peer(p2.PubKey)
if err != nil {
t.Fatalf("Peer: %v", err)
}
checkAllowedIP(t, peer, p2.VpnIP, 32)
}
func TestRelayHandshakes(t *testing.T) {
vpnNetwork := netip.MustParsePrefix("192.168.99.0/24")
relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2)
if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil {
t.Fatalf("relay AddDirect peer1: %v", err)
}
if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil {
t.Fatalf("relay AddDirect peer2: %v", err)
}
if err := peer1.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil {
t.Fatalf("peer1 SetRelay: %v", err)
}
if err := peer2.Dev.SetRelay(relay.PubKey, relay.Endpoint(), vpnNetwork); err != nil {
t.Fatalf("peer2 SetRelay: %v", err)
}
waitHandshake(t, relay.Dev, peer1.PubKey, 30*time.Second)
waitHandshake(t, relay.Dev, peer2.PubKey, 30*time.Second)
waitHandshake(t, peer1.Dev, relay.PubKey, 30*time.Second)
waitHandshake(t, peer2.Dev, relay.PubKey, 30*time.Second)
// relay has /32 entries for each peer
p, err := relay.Dev.Peer(peer1.PubKey)
if err != nil {
t.Fatalf("relay peer1: %v", err)
}
checkAllowedIP(t, p, peer1.VpnIP, 32)
p, err = relay.Dev.Peer(peer2.PubKey)
if err != nil {
t.Fatalf("relay peer2: %v", err)
}
checkAllowedIP(t, p, peer2.VpnIP, 32)
// peers have /24 fallback route via relay
p, err = peer1.Dev.Peer(relay.PubKey)
if err != nil {
t.Fatalf("peer1 relay: %v", err)
}
checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24)
p, err = peer2.Dev.Peer(relay.PubKey)
if err != nil {
t.Fatalf("peer2 relay: %v", err)
}
checkAllowedIP(t, p, vpnNetwork.Masked().Addr(), 24)
}
func TestRemovePeer(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p1.Dev.AddDirect(p2.PubKey, p2.Endpoint(), p2.VpnIP); err != nil {
t.Fatalf("AddDirect: %v", err)
}
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("AddDirect: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.RemovePeer(p2.PubKey); err != nil {
t.Fatalf("RemovePeer: %v", err)
}
if _, err := p1.Dev.Peer(p2.PubKey); err == nil {
t.Fatal("expected error after RemovePeer, got nil")
}
}
func TestEnableForwarding(t *testing.T) {
p := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
if err := p.Dev.EnableForwarding(); err != nil {
t.Fatalf("EnableForwarding: %v", err)
}
data, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", p.Name))
if err != nil {
t.Fatalf("read forwarding: %v", err)
}
if strings.TrimSpace(string(data)) != "1" {
t.Fatalf("expected forwarding=1, got %q", string(data))
}
}
func TestPromoteKeepalive(t *testing.T) {
p1 := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
p2 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
if err := p2.Dev.AddDirect(p1.PubKey, p1.Endpoint(), p1.VpnIP); err != nil {
t.Fatalf("p2 AddDirect: %v", err)
}
if err := p1.Dev.AddProbe(p2.PubKey, p2.Endpoint()); err != nil {
t.Fatalf("AddProbe: %v", err)
}
waitHandshake(t, p1.Dev, p2.PubKey, 30*time.Second)
if err := p1.Dev.Promote(p2.PubKey, p2.VpnIP); err != nil {
t.Fatalf("Promote: %v", err)
}
peer, err := p1.Dev.Peer(p2.PubKey)
if err != nil {
t.Fatalf("Peer: %v", err)
}
if peer.PersistentKeepaliveInterval != 0 {
t.Fatalf("expected keepalive disabled after promote, got %v", peer.PersistentKeepaliveInterval)
}
}
func TestPeersCount(t *testing.T) {
relay := newTestPeer(t, "wgtest0", netip.MustParseAddr("192.168.99.1"), testBasePort)
peer1 := newTestPeer(t, "wgtest1", netip.MustParseAddr("192.168.99.2"), testBasePort+1)
peer2 := newTestPeer(t, "wgtest2", netip.MustParseAddr("192.168.99.3"), testBasePort+2)
if err := relay.Dev.AddDirect(peer1.PubKey, peer1.Endpoint(), peer1.VpnIP); err != nil {
t.Fatalf("AddDirect peer1: %v", err)
}
if err := relay.Dev.AddDirect(peer2.PubKey, peer2.Endpoint(), peer2.VpnIP); err != nil {
t.Fatalf("AddDirect peer2: %v", err)
}
peers, err := relay.Dev.Peers()
if err != nil {
t.Fatalf("Peers: %v", err)
}
if len(peers) != 2 {
t.Fatalf("expected 2 peers, got %d", len(peers))
}
if err := relay.Dev.RemovePeer(peer1.PubKey); err != nil {
t.Fatalf("RemovePeer: %v", err)
}
peers, err = relay.Dev.Peers()
if err != nil {
t.Fatalf("Peers after remove: %v", err)
}
if len(peers) != 1 {
t.Fatalf("expected 1 peer after remove, got %d", len(peers))
}
if peers[0].PublicKey != peer2.PubKey {
t.Fatal("wrong peer remained after remove")
}
}
// checkAllowedIP asserts that a peer has exactly one AllowedIP matching addr/bits.
func checkAllowedIP(t *testing.T, p wgtypes.Peer, addr netip.Addr, bits int) {
t.Helper()
if len(p.AllowedIPs) != 1 {
t.Fatalf("expected 1 AllowedIP, got %d", len(p.AllowedIPs))
}
ones, _ := p.AllowedIPs[0].Mask.Size()
if ones != bits {
t.Fatalf("expected /%d, got /%d", bits, ones)
}
got := netip.AddrFrom4([4]byte(p.AllowedIPs[0].IP.To4()))
if got != addr {
t.Fatalf("expected AllowedIP %v, got %v", addr, got)
}
}