WIP
This commit is contained in:
@@ -1,11 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "log"
|
||||||
"log"
|
|
||||||
"vppn/peer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(0)
|
// TODO: construct peer.App and call Run (audit C1).
|
||||||
peer.Main2()
|
log.Fatal("not yet implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,15 +344,15 @@ type Peer struct {
|
|||||||
Version int64
|
Version int64
|
||||||
APIKey string
|
APIKey string
|
||||||
Name string
|
Name string
|
||||||
PublicIP1 []byte
|
Addr4 []byte
|
||||||
Port1 uint16
|
Port4 uint16
|
||||||
PublicIP2 []byte
|
Addr6 []byte
|
||||||
Port2 uint16
|
Port6 uint16
|
||||||
Relay bool
|
Relay bool
|
||||||
WGPubKey []byte
|
WGPubKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers"
|
const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey FROM peers"
|
||||||
|
|
||||||
func Peer_Insert(
|
func Peer_Insert(
|
||||||
tx TX,
|
tx TX,
|
||||||
@@ -363,7 +363,7 @@ func Peer_Insert(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, 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)
|
_, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@ func Peer_Update(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.NetworkID, row.PeerIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -404,7 +404,7 @@ func Peer_UpdateFull(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -456,8 +456,8 @@ func Peer_Get(
|
|||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
row = &Peer{}
|
row = &Peer{}
|
||||||
r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,PublicIP1,Port1,PublicIP2,Port2,Relay,WGPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP)
|
r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,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 {
|
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil {
|
||||||
row = nil
|
row = nil
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -473,7 +473,7 @@ func Peer_GetWhere(
|
|||||||
) {
|
) {
|
||||||
row = &Peer{}
|
row = &Peer{}
|
||||||
r := tx.QueryRow(query, args...)
|
r := tx.QueryRow(query, args...)
|
||||||
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 {
|
if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil {
|
||||||
row = nil
|
row = nil
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -495,7 +495,7 @@ func Peer_Iterate(
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
row := &Peer{}
|
row := &Peer{}
|
||||||
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)
|
err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey)
|
||||||
if !yield(row, err) {
|
if !yield(row, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,39 +59,39 @@ func Network_Validate(c *Network) error {
|
|||||||
|
|
||||||
func Peer_Sanitize(p *Peer) {
|
func Peer_Sanitize(p *Peer) {
|
||||||
p.Name = strings.TrimSpace(p.Name)
|
p.Name = strings.TrimSpace(p.Name)
|
||||||
if len(p.PublicIP1) != 0 {
|
if len(p.Addr4) != 0 {
|
||||||
if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok {
|
if addr, ok := netip.AddrFromSlice(p.Addr4); ok {
|
||||||
p.PublicIP1 = addr.AsSlice()
|
p.Addr4 = addr.AsSlice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(p.PublicIP2) != 0 {
|
if len(p.Addr6) != 0 {
|
||||||
if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok {
|
if addr, ok := netip.AddrFromSlice(p.Addr6); ok {
|
||||||
p.PublicIP2 = addr.AsSlice()
|
p.Addr6 = addr.AsSlice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.Port1 == 0 {
|
if p.Port4 == 0 {
|
||||||
p.Port1 = 456
|
p.Port4 = 456
|
||||||
}
|
}
|
||||||
if len(p.PublicIP2) != 0 && p.Port2 == 0 {
|
if len(p.Addr6) != 0 && p.Port6 == 0 {
|
||||||
p.Port2 = 456
|
p.Port6 = 456
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Peer_Validate(p *Peer) error {
|
func Peer_Validate(p *Peer) error {
|
||||||
if len(p.PublicIP1) > 0 {
|
if len(p.Addr4) > 0 {
|
||||||
if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok {
|
if _, ok := netip.AddrFromSlice(p.Addr4); !ok {
|
||||||
return ErrInvalidIP
|
return ErrInvalidIP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(p.PublicIP2) > 0 {
|
if len(p.Addr6) > 0 {
|
||||||
if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok {
|
if _, ok := netip.AddrFromSlice(p.Addr6); !ok {
|
||||||
return ErrInvalidIP
|
return ErrInvalidIP
|
||||||
}
|
}
|
||||||
if p.Port2 == 0 {
|
if p.Port6 == 0 {
|
||||||
return ErrInvalidPort
|
return ErrInvalidPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.Port1 == 0 {
|
if p.Port4 == 0 {
|
||||||
return ErrInvalidPort
|
return ErrInvalidPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ TABLE peers OF Peer (
|
|||||||
Version int64,
|
Version int64,
|
||||||
APIKey string NoUpdate,
|
APIKey string NoUpdate,
|
||||||
Name string,
|
Name string,
|
||||||
PublicIP1 []byte,
|
Addr4 []byte,
|
||||||
Port1 uint16,
|
Port4 uint16,
|
||||||
PublicIP2 []byte,
|
Addr6 []byte,
|
||||||
Port2 uint16,
|
Port6 uint16,
|
||||||
Relay bool,
|
Relay bool,
|
||||||
WGPubKey []byte NoUpdate
|
WGPubKey []byte NoUpdate
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ CREATE TABLE peers (
|
|||||||
Version INTEGER NOT NULL, -- Changes when updated.
|
Version INTEGER NOT NULL, -- Changes when updated.
|
||||||
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
|
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
|
||||||
Name TEXT NOT NULL UNIQUE, -- For humans.
|
Name TEXT NOT NULL UNIQUE, -- For humans.
|
||||||
PublicIP1 BLOB NOT NULL,
|
Addr4 BLOB NOT NULL,
|
||||||
Port1 INTEGER NOT NULL,
|
Port4 INTEGER NOT NULL,
|
||||||
PublicIP2 BLOB NOT NULL,
|
Addr6 BLOB NOT NULL,
|
||||||
Port2 INTEGER NOT NULL,
|
Port6 INTEGER NOT NULL,
|
||||||
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
|
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
|
||||||
WGPubKey BLOB NOT NULL,
|
WGPubKey BLOB NOT NULL,
|
||||||
PRIMARY KEY(NetworkID, PeerIP)
|
PRIMARY KEY(NetworkID, PeerIP)
|
||||||
|
|||||||
@@ -142,27 +142,27 @@ func (a *App) _adminPeerCreate(s *api.Session, w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||||
var ip1Str, ip2Str string
|
var addr4Str, addr6Str string
|
||||||
|
|
||||||
p := &api.Peer{}
|
p := &api.Peer{}
|
||||||
err := webutil.NewFormScanner(r.Form).
|
err := webutil.NewFormScanner(r.Form).
|
||||||
Scan("NetworkID", &p.NetworkID).
|
Scan("NetworkID", &p.NetworkID).
|
||||||
Scan("IP", &p.PeerIP).
|
Scan("IP", &p.PeerIP).
|
||||||
Scan("Name", &p.Name).
|
Scan("Name", &p.Name).
|
||||||
Scan("PublicIP1", &ip1Str).
|
Scan("Addr4", &addr4Str).
|
||||||
Scan("Port1", &p.Port1).
|
Scan("Port4", &p.Port4).
|
||||||
Scan("PublicIP2", &ip2Str).
|
Scan("Addr6", &addr6Str).
|
||||||
Scan("Port2", &p.Port2).
|
Scan("Port6", &p.Port6).
|
||||||
Scan("Relay", &p.Relay).
|
Scan("Relay", &p.Relay).
|
||||||
Error()
|
Error()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.PublicIP1, err = stringToIP(ip1Str); err != nil {
|
if p.Addr4, err = stringToIP(addr4Str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if p.PublicIP2, err = stringToIP(ip2Str); err != nil {
|
if p.Addr6, err = stringToIP(addr6Str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,24 +204,24 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ip1Str, ip2Str string
|
var addr4Str, addr6Str string
|
||||||
|
|
||||||
err = webutil.NewFormScanner(r.Form).
|
err = webutil.NewFormScanner(r.Form).
|
||||||
Scan("Name", &peer.Name).
|
Scan("Name", &peer.Name).
|
||||||
Scan("PublicIP1", &ip1Str).
|
Scan("Addr4", &addr4Str).
|
||||||
Scan("Port1", &peer.Port1).
|
Scan("Port4", &peer.Port4).
|
||||||
Scan("PublicIP2", &ip2Str).
|
Scan("Addr6", &addr6Str).
|
||||||
Scan("Port2", &peer.Port2).
|
Scan("Port6", &peer.Port6).
|
||||||
Scan("Relay", &peer.Relay).
|
Scan("Relay", &peer.Relay).
|
||||||
Error()
|
Error()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if peer.PublicIP1, err = stringToIP(ip1Str); err != nil {
|
if peer.Addr4, err = stringToIP(addr4Str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if peer.PublicIP2, err = stringToIP(ip2Str); err != nil {
|
if peer.Addr6, err = stringToIP(addr6Str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,15 +355,15 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
|
|||||||
for _, p := range l {
|
for _, p := range l {
|
||||||
if len(p.WGPubKey) != 0 {
|
if len(p.WGPubKey) != 0 {
|
||||||
peers[p.PeerIP] = &m.Peer{
|
peers[p.PeerIP] = &m.Peer{
|
||||||
PeerIP: p.PeerIP,
|
PeerIP: p.PeerIP,
|
||||||
Version: p.Version,
|
Version: p.Version,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
PublicIP1: p.PublicIP1,
|
Addr4: p.Addr4,
|
||||||
Port1: p.Port1,
|
Port4: p.Port4,
|
||||||
PublicIP2: p.PublicIP2,
|
Addr6: p.Addr6,
|
||||||
Port2: p.Port2,
|
Port6: p.Port6,
|
||||||
Relay: p.Relay,
|
Relay: p.Relay,
|
||||||
WGPubKey: p.WGPubKey,
|
WGPubKey: p.WGPubKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
m/models.go
18
m/models.go
@@ -12,15 +12,15 @@ type PeerInitResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
PeerIP byte
|
PeerIP byte
|
||||||
Version int64
|
Version int64
|
||||||
Name string
|
Name string
|
||||||
PublicIP1 []byte
|
Addr4 []byte
|
||||||
Port1 uint16
|
Port4 uint16
|
||||||
PublicIP2 []byte
|
Addr6 []byte
|
||||||
Port2 uint16
|
Port6 uint16
|
||||||
Relay bool
|
Relay bool
|
||||||
WGPubKey []byte
|
WGPubKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkState struct {
|
type NetworkState struct {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -56,7 +55,7 @@ type App struct {
|
|||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
dev WGDevice
|
dev WGDevice
|
||||||
controlConn *net.UDPConn
|
controlConn ControlConn
|
||||||
|
|
||||||
// Peer state
|
// Peer state
|
||||||
relay *Peer
|
relay *Peer
|
||||||
@@ -67,9 +66,6 @@ type App struct {
|
|||||||
selfV4 netip.AddrPort
|
selfV4 netip.AddrPort
|
||||||
selfV6 netip.AddrPort
|
selfV6 netip.AddrPort
|
||||||
|
|
||||||
// Monotonically increasing ID for outbound pings (client role only)
|
|
||||||
nextPingID int64 // TODO: Remove
|
|
||||||
|
|
||||||
// Event channels fed by background goroutines
|
// Event channels fed by background goroutines
|
||||||
hubAddCh <-chan HubPeer
|
hubAddCh <-chan HubPeer
|
||||||
hubRemoveCh <-chan wgtypes.Key
|
hubRemoveCh <-chan wgtypes.Key
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ func addRelayPeer(t *testing.T, a *App, vpnIP string, ep netip.AddrPort) *Peer {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTestApp returns a minimal App wired to a fakeWGDevice.
|
// newTestApp returns a minimal App wired to a fakeWGDevice and fakeControlConn.
|
||||||
// vpnIP is the local VPN address (e.g. "10.0.0.1").
|
// vpnIP is the local VPN address (e.g. "10.0.0.1").
|
||||||
// isPublic / isRelay describe the local node's role.
|
// isPublic / isRelay describe the local node's role.
|
||||||
func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fakeWGDevice) {
|
func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fakeWGDevice, *fakeControlConn) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
privKey, err := wgtypes.GeneratePrivateKey()
|
privKey, err := wgtypes.GeneratePrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,6 +36,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
|||||||
}
|
}
|
||||||
ip := netip.MustParseAddr(vpnIP)
|
ip := netip.MustParseAddr(vpnIP)
|
||||||
dev := &fakeWGDevice{}
|
dev := &fakeWGDevice{}
|
||||||
|
cc := &fakeControlConn{}
|
||||||
a := &App{
|
a := &App{
|
||||||
vpnIP: ip,
|
vpnIP: ip,
|
||||||
vpnNet: netip.MustParsePrefix("10.0.0.0/24"),
|
vpnNet: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
@@ -44,6 +45,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
|||||||
isPublic: isPublic,
|
isPublic: isPublic,
|
||||||
isRelay: isRelay,
|
isRelay: isRelay,
|
||||||
dev: dev,
|
dev: dev,
|
||||||
|
controlConn: cc,
|
||||||
peersByKey: make(map[wgtypes.Key]*Peer),
|
peersByKey: make(map[wgtypes.Key]*Peer),
|
||||||
peersByIP: make(map[netip.Addr]*Peer),
|
peersByIP: make(map[netip.Addr]*Peer),
|
||||||
hubAddCh: make(chan HubPeer),
|
hubAddCh: make(chan HubPeer),
|
||||||
@@ -51,5 +53,5 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
|||||||
pingCh: make(chan PingEvent),
|
pingCh: make(chan PingEvent),
|
||||||
multicastCh: make(chan MulticastEvent),
|
multicastCh: make(chan MulticastEvent),
|
||||||
}
|
}
|
||||||
return a, dev
|
return a, dev, cc
|
||||||
}
|
}
|
||||||
|
|||||||
150
peer/control.go
150
peer/control.go
@@ -1,150 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
controlPort = uint16(4561)
|
|
||||||
controlMsgLen = 7 // 1 type + 4 IPv4 + 2 port
|
|
||||||
msgYourEndpt = uint8(1)
|
|
||||||
msgMyEndpt = uint8(2)
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlServer struct {
|
|
||||||
localPeerIP byte
|
|
||||||
network []byte
|
|
||||||
conn *net.UDPConn
|
|
||||||
hp *HolePunch
|
|
||||||
netName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewControlServer(g Globals, hp *HolePunch, netName string) (*ControlServer, error) {
|
|
||||||
vpnIP := netip.AddrFrom4([4]byte{
|
|
||||||
g.Network[0], g.Network[1], g.Network[2], g.LocalPeerIP,
|
|
||||||
})
|
|
||||||
listenAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(vpnIP, controlPort))
|
|
||||||
conn, err := net.ListenUDP("udp4", listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("listen on control port: %w", err)
|
|
||||||
}
|
|
||||||
return &ControlServer{
|
|
||||||
localPeerIP: g.LocalPeerIP,
|
|
||||||
network: g.Network,
|
|
||||||
conn: conn,
|
|
||||||
hp: hp,
|
|
||||||
netName: netName,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs *ControlServer) Run() {
|
|
||||||
buf := make([]byte, 64)
|
|
||||||
for {
|
|
||||||
n, src, err := cs.conn.ReadFromUDPAddrPort(buf)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Control] read: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n < controlMsgLen {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cs.handle(buf[:n], src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs *ControlServer) handle(msg []byte, src netip.AddrPort) {
|
|
||||||
msgType, ep, ok := decodeControlMsg(msg)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch msgType {
|
|
||||||
case msgYourEndpt:
|
|
||||||
cs.onYourEndpoint(ep)
|
|
||||||
case msgMyEndpt:
|
|
||||||
cs.onMyEndpoint(src, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// onYourEndpoint is called when the relay tells us our external WG endpoint.
|
|
||||||
// We broadcast MsgMyEndpoint to all known peers so they can attempt direct
|
|
||||||
// connections to us.
|
|
||||||
func (cs *ControlServer) onYourEndpoint(ourEndpoint netip.AddrPort) {
|
|
||||||
log.Printf("[Control] external endpoint: %v", ourEndpoint)
|
|
||||||
|
|
||||||
state, err := loadNetworkState(cs.netName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Control] load state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range state.Peers {
|
|
||||||
if p == nil || p.PeerIP == cs.localPeerIP || len(p.WGPubKey) != wgtypes.KeyLen {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peerVPNIP := netip.AddrFrom4([4]byte{
|
|
||||||
cs.network[0], cs.network[1], cs.network[2], p.PeerIP,
|
|
||||||
})
|
|
||||||
cs.sendMsg(msgMyEndpt, peerVPNIP, ourEndpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// onMyEndpoint is called when a peer tells us their external WG endpoint.
|
|
||||||
// We start a probe-before-commit attempt to reach them directly.
|
|
||||||
func (cs *ControlServer) onMyEndpoint(src netip.AddrPort, theirEndpoint netip.AddrPort) {
|
|
||||||
peerIPByte := src.Addr().As4()[3]
|
|
||||||
|
|
||||||
state, err := loadNetworkState(cs.netName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Control] load state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
peer := state.Peers[peerIPByte]
|
|
||||||
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKey, err := wgtypes.NewKey(peer.WGPubKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.hp.OnEndpointLearned(peerIPByte, pubKey, theirEndpoint, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendYourEndpoint sends MsgYourEndpoint to a peer, informing them of their
|
|
||||||
// own external WG endpoint. Used by the relay's endpoint reporter (Phase 6).
|
|
||||||
func (cs *ControlServer) SendYourEndpoint(peerVPNIP netip.Addr, theirEndpoint netip.AddrPort) {
|
|
||||||
cs.sendMsg(msgYourEndpt, peerVPNIP, theirEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs *ControlServer) sendMsg(msgType uint8, peerVPNIP netip.Addr, ep netip.AddrPort) {
|
|
||||||
dst := net.UDPAddrFromAddrPort(netip.AddrPortFrom(peerVPNIP, controlPort))
|
|
||||||
if _, err := cs.conn.WriteTo(encodeControlMsg(msgType, ep), dst); err != nil {
|
|
||||||
log.Printf("[Control] send to %v: %v", peerVPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeControlMsg(msgType uint8, ep netip.AddrPort) []byte {
|
|
||||||
msg := make([]byte, controlMsgLen)
|
|
||||||
msg[0] = msgType
|
|
||||||
a4 := ep.Addr().Unmap().As4()
|
|
||||||
copy(msg[1:5], a4[:])
|
|
||||||
binary.BigEndian.PutUint16(msg[5:7], ep.Port())
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeControlMsg(msg []byte) (msgType uint8, ep netip.AddrPort, ok bool) {
|
|
||||||
if len(msg) < controlMsgLen {
|
|
||||||
return 0, netip.AddrPort{}, false
|
|
||||||
}
|
|
||||||
ip := netip.AddrFrom4([4]byte(msg[1:5]))
|
|
||||||
port := binary.BigEndian.Uint16(msg[5:7])
|
|
||||||
return msg[0], netip.AddrPortFrom(ip, port), true
|
|
||||||
}
|
|
||||||
@@ -11,14 +11,14 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
version = 1
|
version = 1
|
||||||
Size = 59 // 1 version + 8 ID + 8 PingTS + 6 SrcV4 + 18 SrcV6 + 18 Dst
|
Size = 51 // 1 version + 8 PingTS + 6 SrcV4 + 18 SrcV6 + 18 Dst
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ping is the single control packet type exchanged between VPN peers.
|
// Ping is the single control packet type exchanged between VPN peers.
|
||||||
//
|
//
|
||||||
// In each peer pair, the peer with the lower VPN IP is the client: it sets ID
|
// In each peer pair, the peer with the lower VPN IP is the client: it sets
|
||||||
// and PingTS and sends pings on a timer. The server echoes ID and PingTS back
|
// PingTS and sends pings on a timer. The server echoes PingTS back in its
|
||||||
// in its response, allowing the client to compute RTT = now - PingTS.
|
// response, allowing the client to compute RTT = now - PingTS.
|
||||||
//
|
//
|
||||||
// Both client and server populate SrcV4, SrcV6, and Dst on every packet so
|
// Both client and server populate SrcV4, SrcV6, and Dst on every packet so
|
||||||
// endpoint information flows in both directions.
|
// endpoint information flows in both directions.
|
||||||
@@ -27,50 +27,47 @@ const (
|
|||||||
// WireGuard handshake source. Zero if the sender has not observed a handshake
|
// WireGuard handshake source. Zero if the sender has not observed a handshake
|
||||||
// from the recipient.
|
// from the recipient.
|
||||||
type Ping struct {
|
type Ping struct {
|
||||||
ID int64 // Client ping ID.
|
|
||||||
PingTS int64 // Client ping send time in nanoseconds.
|
PingTS int64 // Client ping send time in nanoseconds.
|
||||||
SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port.
|
SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port.
|
||||||
SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port.
|
SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port.
|
||||||
Dst netip.AddrPort
|
Dst netip.AddrPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal encodes p into a fixed-size 59-byte array.
|
// Marshal encodes p into a fixed-size 51-byte array.
|
||||||
func (p Ping) Marshal() [Size]byte {
|
func (p Ping) Marshal() [Size]byte {
|
||||||
var buf [Size]byte
|
var buf [Size]byte
|
||||||
buf[0] = version
|
buf[0] = version
|
||||||
binary.BigEndian.PutUint64(buf[1:9], uint64(p.ID))
|
binary.BigEndian.PutUint64(buf[1:9], uint64(p.PingTS))
|
||||||
binary.BigEndian.PutUint64(buf[9:17], uint64(p.PingTS))
|
|
||||||
if p.SrcV4.IsValid() {
|
if p.SrcV4.IsValid() {
|
||||||
a4 := p.SrcV4.Addr().As4()
|
a4 := p.SrcV4.Addr().As4()
|
||||||
copy(buf[17:21], a4[:])
|
copy(buf[9:13], a4[:])
|
||||||
binary.BigEndian.PutUint16(buf[21:23], p.SrcV4.Port())
|
binary.BigEndian.PutUint16(buf[13:15], p.SrcV4.Port())
|
||||||
}
|
}
|
||||||
a16 := p.SrcV6.Addr().As16()
|
a16 := p.SrcV6.Addr().As16()
|
||||||
copy(buf[23:39], a16[:])
|
copy(buf[15:31], a16[:])
|
||||||
binary.BigEndian.PutUint16(buf[39:41], p.SrcV6.Port())
|
binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port())
|
||||||
a16 = p.Dst.Addr().As16()
|
a16 = p.Dst.Addr().As16()
|
||||||
copy(buf[41:57], a16[:])
|
copy(buf[33:49], a16[:])
|
||||||
binary.BigEndian.PutUint16(buf[57:59], p.Dst.Port())
|
binary.BigEndian.PutUint16(buf[49:51], p.Dst.Port())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes a Ping from a fixed-size 59-byte array.
|
// Unmarshal decodes a Ping from a fixed-size 51-byte array.
|
||||||
func Unmarshal(buf [Size]byte) (Ping, error) {
|
func Unmarshal(buf [Size]byte) (Ping, error) {
|
||||||
if buf[0] != version {
|
if buf[0] != version {
|
||||||
return Ping{}, fmt.Errorf("unknown ping version %d", buf[0])
|
return Ping{}, fmt.Errorf("unknown ping version %d", buf[0])
|
||||||
}
|
}
|
||||||
p := Ping{
|
p := Ping{
|
||||||
ID: int64(binary.BigEndian.Uint64(buf[1:9])),
|
PingTS: int64(binary.BigEndian.Uint64(buf[1:9])),
|
||||||
PingTS: int64(binary.BigEndian.Uint64(buf[9:17])),
|
|
||||||
}
|
}
|
||||||
if addr := netip.AddrFrom4([4]byte(buf[17:21])); !addr.IsUnspecified() {
|
if addr := netip.AddrFrom4([4]byte(buf[9:13])); !addr.IsUnspecified() {
|
||||||
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[21:23]))
|
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15]))
|
||||||
}
|
}
|
||||||
if addr := netip.AddrFrom16([16]byte(buf[23:39])); !addr.IsUnspecified() {
|
if addr := netip.AddrFrom16([16]byte(buf[15:31])); !addr.IsUnspecified() {
|
||||||
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[39:41]))
|
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33]))
|
||||||
}
|
}
|
||||||
if addr := netip.AddrFrom16([16]byte(buf[41:57])).Unmap(); !addr.IsUnspecified() {
|
if addr := netip.AddrFrom16([16]byte(buf[33:49])).Unmap(); !addr.IsUnspecified() {
|
||||||
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[57:59]))
|
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51]))
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "client ping",
|
name: "client ping",
|
||||||
ping: control.Ping{
|
ping: control.Ping{
|
||||||
ID: 42,
|
|
||||||
PingTS: 1234567890,
|
PingTS: 1234567890,
|
||||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||||
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||||
@@ -28,7 +27,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "server response",
|
name: "server response",
|
||||||
ping: control.Ping{
|
ping: control.Ping{
|
||||||
ID: 42,
|
|
||||||
PingTS: 1234567890,
|
PingTS: 1234567890,
|
||||||
SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"),
|
SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||||
Dst: netip.MustParseAddrPort("1.2.3.4:9999"),
|
Dst: netip.MustParseAddrPort("1.2.3.4:9999"),
|
||||||
@@ -37,7 +35,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "IPv6 only",
|
name: "IPv6 only",
|
||||||
ping: control.Ping{
|
ping: control.Ping{
|
||||||
ID: 1,
|
|
||||||
PingTS: 999,
|
PingTS: 999,
|
||||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||||
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
|
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
|
||||||
@@ -46,7 +43,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "dual stack",
|
name: "dual stack",
|
||||||
ping: control.Ping{
|
ping: control.Ping{
|
||||||
ID: 7,
|
|
||||||
PingTS: 555,
|
PingTS: 555,
|
||||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||||
@@ -56,7 +52,6 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "no src known",
|
name: "no src known",
|
||||||
ping: control.Ping{
|
ping: control.Ping{
|
||||||
ID: 3,
|
|
||||||
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
61
peer/control_conn.go
Normal file
61
peer/control_conn.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"vppn/peer/control"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ ControlConn = (*udpControlConn)(nil)
|
||||||
|
|
||||||
|
type udpControlConn struct {
|
||||||
|
conn *net.UDPConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUDPControlConn opens a UDP socket bound to localIP:port.
|
||||||
|
func newUDPControlConn(localIP netip.Addr, port uint16) (*udpControlConn, error) {
|
||||||
|
addr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(localIP, port))
|
||||||
|
conn, err := net.ListenUDP("udp4", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &udpControlConn{conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpControlConn) SendPing(dst netip.AddrPort, ping control.Ping) error {
|
||||||
|
buf := ping.Marshal()
|
||||||
|
_, err := c.conn.WriteToUDP(buf[:], net.UDPAddrFromAddrPort(dst))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run reads incoming ping packets and forwards them to ch until ctx is done.
|
||||||
|
// Call this in a goroutine before starting the App event loop.
|
||||||
|
func (c *udpControlConn) run(ch chan<- PingEvent) {
|
||||||
|
var buf [control.Size]byte
|
||||||
|
for {
|
||||||
|
n, src, err := c.conn.ReadFromUDP(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("control read: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != control.Size {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ping, err := control.Unmarshal(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("control unmarshal: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIP, ok := netip.AddrFromSlice(src.IP)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ch <- PingEvent{srcVPNIP: srcIP.Unmap(), ping: ping}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func generateWGKey() wgtypes.Key {
|
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to generate WireGuard private key: %v", err)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,34 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// devRetry calls fn up to 6 times with exponential backoff, retrying on EBUSY
|
||||||
|
// (transient netlink contention during WireGuard handshake/rekey). Fatal on any other error.
|
||||||
|
func devRetry(vpnIP netip.Addr, op string, fn func() error) {
|
||||||
|
const attempts = 6
|
||||||
|
timeout := 10 * time.Millisecond
|
||||||
|
for i := range attempts {
|
||||||
|
err := fn()
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, syscall.EBUSY) && i < attempts-1 {
|
||||||
|
time.Sleep(timeout)
|
||||||
|
timeout *= 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Fatalf("%s %v: %v", op, vpnIP, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) devPeers() []wgtypes.Peer {
|
func (a *App) devPeers() []wgtypes.Peer {
|
||||||
peers, err := a.dev.Peers()
|
peers, err := a.dev.Peers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -16,37 +38,25 @@ func (a *App) devPeers() []wgtypes.Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devAddPeer(p *Peer) {
|
func (a *App) devAddPeer(p *Peer) {
|
||||||
if err := a.dev.AddPeer(p.PubKey()); err != nil {
|
devRetry(p.VPNIP, "AddPeer", func() error { return a.dev.AddPeer(p.PubKey()) })
|
||||||
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) {
|
func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) {
|
||||||
if err := a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP); err != nil {
|
devRetry(p.VPNIP, "AddDirect", func() error { return a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP) })
|
||||||
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) {
|
func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) {
|
||||||
if err := a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet); err != nil {
|
devRetry(p.VPNIP, "SetRelay", func() error { return a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet) })
|
||||||
log.Fatalf("Failed to add relay %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devPromote(p *Peer) {
|
func (a *App) devPromote(p *Peer) {
|
||||||
if err := a.dev.Promote(p.PubKey(), p.VPNIP); err != nil {
|
devRetry(p.VPNIP, "Promote", func() error { return a.dev.Promote(p.PubKey(), p.VPNIP) })
|
||||||
log.Fatalf("Failed to promote peer %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) {
|
func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) {
|
||||||
if err := a.dev.AddProbe(p.PubKey(), endpoint); err != nil {
|
devRetry(p.VPNIP, "AddProbe", func() error { return a.dev.AddProbe(p.PubKey(), endpoint) })
|
||||||
log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) devRemove(p *Peer) {
|
func (a *App) devRemove(p *Peer) {
|
||||||
if err := a.dev.RemovePeer(p.PubKey()); err != nil {
|
devRetry(p.VPNIP, "RemovePeer", func() error { return a.dev.RemovePeer(p.PubKey()) })
|
||||||
log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
const reporterInterval = 5 * time.Second
|
|
||||||
|
|
||||||
// EndpointReporter runs on relay peers only. It polls wgctrl every 5s and,
|
|
||||||
// for each peer whose LastHandshakeTime has changed, sends MsgYourEndpoint to
|
|
||||||
// that peer's VPN IP so it learns its own external WG endpoint.
|
|
||||||
type EndpointReporter struct {
|
|
||||||
client *wgctrl.Client
|
|
||||||
devName string
|
|
||||||
network []byte
|
|
||||||
netName string
|
|
||||||
control *ControlServer
|
|
||||||
lastTimes map[wgtypes.Key]time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEndpointReporter(g Globals, cs *ControlServer, netName string) *EndpointReporter {
|
|
||||||
return &EndpointReporter{
|
|
||||||
client: g.WGClient,
|
|
||||||
devName: g.WGDevName,
|
|
||||||
network: g.Network,
|
|
||||||
netName: netName,
|
|
||||||
control: cs,
|
|
||||||
lastTimes: make(map[wgtypes.Key]time.Time),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (er *EndpointReporter) Run() {
|
|
||||||
for range time.Tick(reporterInterval) {
|
|
||||||
er.poll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (er *EndpointReporter) poll() {
|
|
||||||
dev, err := er.client.Device(er.devName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[EndpointReporter] get device: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := loadNetworkState(er.netName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[EndpointReporter] load state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build WGPubKey → VPN IP byte index from current network state.
|
|
||||||
keyToIP := make(map[wgtypes.Key]byte, len(dev.Peers))
|
|
||||||
for _, p := range state.Peers {
|
|
||||||
if p == nil || len(p.WGPubKey) != wgtypes.KeyLen {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key, err := wgtypes.NewKey(p.WGPubKey)
|
|
||||||
if err == nil {
|
|
||||||
keyToIP[key] = p.PeerIP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range dev.Peers {
|
|
||||||
if p.Endpoint == nil || p.LastHandshakeTime.IsZero() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p.LastHandshakeTime == er.lastTimes[p.PublicKey] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
peerIPByte, ok := keyToIP[p.PublicKey]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
er.lastTimes[p.PublicKey] = p.LastHandshakeTime
|
|
||||||
|
|
||||||
peerVPNIP := netip.AddrFrom4([4]byte{
|
|
||||||
er.network[0], er.network[1], er.network[2], peerIPByte,
|
|
||||||
})
|
|
||||||
er.control.SendYourEndpoint(peerVPNIP, p.Endpoint.AddrPort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
peer/fake_control_conn_test.go
Normal file
43
peer/fake_control_conn_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"vppn/peer/control"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sentPing struct {
|
||||||
|
Dst netip.AddrPort
|
||||||
|
Ping control.Ping
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeControlConn struct {
|
||||||
|
Sent []sentPing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeControlConn) SendPing(dst netip.AddrPort, ping control.Ping) error {
|
||||||
|
f.Sent = append(f.Sent, sentPing{Dst: dst, Ping: ping})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeControlConn) AssertNone(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(f.Sent) != 0 {
|
||||||
|
t.Fatalf("expected no pings sent, got %d: %v", len(f.Sent), f.Sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeControlConn) AssertSent(t *testing.T, i int, dst netip.AddrPort, ping control.Ping) {
|
||||||
|
t.Helper()
|
||||||
|
if i >= len(f.Sent) {
|
||||||
|
t.Fatalf("no ping at index %d (have %d)", i, len(f.Sent))
|
||||||
|
}
|
||||||
|
got := f.Sent[i]
|
||||||
|
if got.Dst != dst {
|
||||||
|
t.Errorf("ping[%d].Dst = %v, want %v", i, got.Dst, dst)
|
||||||
|
}
|
||||||
|
if got.Ping != ping {
|
||||||
|
t.Errorf("ping[%d].Ping = %+v, want %+v", i, got.Ping, ping)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -48,7 +50,7 @@ func storeJson(x any, outPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(tmpPath)
|
f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -94,4 +96,3 @@ func loadPeerConfig(netName string) (pc LocalConfig, err error) {
|
|||||||
func loadNetworkState(netName string) (ps m.NetworkState, err error) {
|
func loadNetworkState(netName string) (ps m.NetworkState, err error) {
|
||||||
return ps, loadJson(peerStatePath(netName), &ps)
|
return ps, loadJson(peerStatePath(netName), &ps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFilePaths(t *testing.T) {
|
|
||||||
confDir := configDir("netName")
|
|
||||||
if filepath.Base(confDir) != "netName" {
|
|
||||||
t.Fatal(confDir)
|
|
||||||
}
|
|
||||||
if filepath.Base(filepath.Dir(confDir)) != ".vppn" {
|
|
||||||
t.Fatal(confDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
path := peerConfigPath("netName")
|
|
||||||
if path != filepath.Join(confDir, "config.json") {
|
|
||||||
t.Fatal(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
path = peerStatePath("netName")
|
|
||||||
if path != filepath.Join(confDir, "state.json") {
|
|
||||||
t.Fatal(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreLoadJson(t *testing.T) {
|
|
||||||
type Object struct {
|
|
||||||
Name string
|
|
||||||
Age int
|
|
||||||
Price float64
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
outPath := filepath.Join(tmpDir, "object.json")
|
|
||||||
|
|
||||||
obj := Object{
|
|
||||||
Name: "Jason",
|
|
||||||
Age: 22,
|
|
||||||
Price: 123.534,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := storeJson(obj, outPath); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
obj2 := Object{}
|
|
||||||
if err := loadJson(outPath, &obj2); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(obj, obj2) {
|
|
||||||
t.Fatal(obj, obj2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
149
peer/hub_poller.go
Normal file
149
peer/hub_poller.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
"vppn/m"
|
||||||
|
)
|
||||||
|
|
||||||
|
const hubPollInterval = 64 * time.Second
|
||||||
|
|
||||||
|
type HubPoller struct {
|
||||||
|
selfVPNIP netip.Addr
|
||||||
|
vpnNet netip.Prefix
|
||||||
|
hubURL string
|
||||||
|
apiKey string
|
||||||
|
addCh chan<- HubPeer
|
||||||
|
removeCh chan<- wgtypes.Key
|
||||||
|
known map[wgtypes.Key]int64 // pubKey → last seen version
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHubPoller(
|
||||||
|
selfVPNIP netip.Addr,
|
||||||
|
vpnNet netip.Prefix,
|
||||||
|
hubURL, apiKey string,
|
||||||
|
addCh chan<- HubPeer,
|
||||||
|
removeCh chan<- wgtypes.Key,
|
||||||
|
) (*HubPoller, error) {
|
||||||
|
u, err := url.Parse(hubURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.Path = "/peer/fetch-state/"
|
||||||
|
|
||||||
|
return &HubPoller{
|
||||||
|
selfVPNIP: selfVPNIP,
|
||||||
|
vpnNet: vpnNet,
|
||||||
|
hubURL: u.String(),
|
||||||
|
apiKey: apiKey,
|
||||||
|
addCh: addCh,
|
||||||
|
removeCh: removeCh,
|
||||||
|
known: make(map[wgtypes.Key]int64),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp *HubPoller) Run() {
|
||||||
|
hp.poll()
|
||||||
|
for range time.Tick(hubPollInterval) {
|
||||||
|
hp.poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp *HubPoller) poll() {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, hp.hubURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[HubPoller] build request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetBasicAuth("", hp.apiKey)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 32 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[HubPoller] fetch: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[HubPoller] read body: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state m.NetworkState
|
||||||
|
if err := json.Unmarshal(body, &state); err != nil {
|
||||||
|
log.Printf("[HubPoller] unmarshal: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hp.apply(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp *HubPoller) apply(state m.NetworkState) {
|
||||||
|
seen := make(map[wgtypes.Key]struct{}, len(hp.known))
|
||||||
|
|
||||||
|
netAddr := hp.vpnNet.Addr().As4()
|
||||||
|
|
||||||
|
for _, p := range state.Peers {
|
||||||
|
if p == nil || len(p.WGPubKey) != wgtypes.KeyLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := wgtypes.NewKey(p.WGPubKey)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
octets := netAddr
|
||||||
|
octets[3] = p.PeerIP
|
||||||
|
vpnIP := netip.AddrFrom4(octets)
|
||||||
|
if vpnIP == hp.selfVPNIP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[pubKey] = struct{}{}
|
||||||
|
|
||||||
|
if v, ok := hp.known[pubKey]; ok && v == p.Version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hp.known[pubKey] = p.Version
|
||||||
|
hp.addCh <- hubPeerFrom(pubKey, vpnIP, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range hp.known {
|
||||||
|
if _, ok := seen[key]; !ok {
|
||||||
|
delete(hp.known, key)
|
||||||
|
hp.removeCh <- key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer {
|
||||||
|
var ep4, ep6 netip.AddrPort
|
||||||
|
if len(p.Addr4) > 0 {
|
||||||
|
if addr, ok := netip.AddrFromSlice(p.Addr4); ok {
|
||||||
|
ep4 = netip.AddrPortFrom(addr.Unmap(), p.Port4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(p.Addr6) > 0 {
|
||||||
|
if addr, ok := netip.AddrFromSlice(p.Addr6); ok {
|
||||||
|
ep6 = netip.AddrPortFrom(addr, p.Port6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HubPeer{
|
||||||
|
PubKey: pubKey,
|
||||||
|
VPNIP: vpnIP,
|
||||||
|
IsRelay: p.Relay,
|
||||||
|
IsPublic: ep4.IsValid() || ep6.IsValid(),
|
||||||
|
EndpointV4: ep4,
|
||||||
|
EndpointV6: ep6,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,17 +16,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HubPoller struct {
|
type HubPoller struct {
|
||||||
Globals
|
client *http.Client
|
||||||
holePunch *HolePunch
|
req *http.Request
|
||||||
client *http.Client
|
versions [256]int64
|
||||||
req *http.Request
|
netName string
|
||||||
versions [256]int64
|
|
||||||
netName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHubPoller(
|
func NewHubPoller(
|
||||||
g Globals,
|
|
||||||
hp *HolePunch,
|
|
||||||
netName,
|
netName,
|
||||||
hubURL,
|
hubURL,
|
||||||
apiKey string,
|
apiKey string,
|
||||||
@@ -118,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
|
|||||||
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
|
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(peer.PublicIP1) == 0 || peer.Port1 == 0 {
|
if len(peer.Addr4) == 0 || peer.Port4 == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +126,12 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, ok := netip.AddrFromSlice(peer.PublicIP1)
|
ip, ok := netip.AddrFromSlice(peer.Addr4)
|
||||||
if !ok {
|
if !ok {
|
||||||
hp.logf("Invalid public IP for peer %d", peer.PeerIP)
|
hp.logf("Invalid public IP for peer %d", peer.PeerIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1)
|
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4)
|
||||||
|
|
||||||
if peer.Relay {
|
if peer.Relay {
|
||||||
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {
|
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package peer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"vppn/peer/control"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
@@ -17,3 +18,9 @@ type WGDevice interface {
|
|||||||
Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error
|
Promote(pubKey wgtypes.Key, vpnIP netip.Addr) error
|
||||||
RemovePeer(pubKey wgtypes.Key) error
|
RemovePeer(pubKey wgtypes.Key) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ControlConn sends pings to peers over the VPN control port.
|
||||||
|
// Reading is handled separately via run, which feeds the App's pingCh.
|
||||||
|
type ControlConn interface {
|
||||||
|
SendPing(dst netip.AddrPort, ping control.Ping) error
|
||||||
|
}
|
||||||
|
|||||||
209
peer/main.go
209
peer/main.go
@@ -1,209 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// vppn netName run
|
|
||||||
// vppn netName status
|
|
||||||
func Main2() {
|
|
||||||
printUsage := func() {
|
|
||||||
fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...]
|
|
||||||
|
|
||||||
Available commands:
|
|
||||||
run
|
|
||||||
status
|
|
||||||
hosts
|
|
||||||
`, os.Args[0])
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
101
peer/multicast.go
Normal file
101
peer/multicast.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mcBeaconLen = 35 // 1 VPN IP byte + 32 WG pubkey + 2 WG listen port
|
||||||
|
mcBroadcastInterval = 32 * time.Second
|
||||||
|
mcErrorRetryInterval = 16 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var mcAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(
|
||||||
|
netip.AddrFrom4([4]byte{224, 0, 0, 157}),
|
||||||
|
4560))
|
||||||
|
|
||||||
|
// RunMCWriter broadcasts a beacon on the local multicast group every
|
||||||
|
// mcBroadcastInterval so that LAN peers can discover our WireGuard endpoint.
|
||||||
|
func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16) {
|
||||||
|
conn, err := net.ListenMulticastUDP("udp", nil, mcAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[MCWriter] bind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beacon := buildBeacon(selfVPNIP, pubKey, wgPort)
|
||||||
|
|
||||||
|
for range time.Tick(mcBroadcastInterval) {
|
||||||
|
if _, err := conn.WriteToUDP(beacon, mcAddr); err != nil {
|
||||||
|
log.Printf("[MCWriter] write: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBeacon(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16) []byte {
|
||||||
|
beacon := make([]byte, mcBeaconLen)
|
||||||
|
beacon[0] = selfVPNIP.As4()[3]
|
||||||
|
copy(beacon[1:33], pubKey[:])
|
||||||
|
binary.BigEndian.PutUint16(beacon[33:35], wgPort)
|
||||||
|
return beacon
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMCReader listens for multicast beacons from LAN peers and feeds
|
||||||
|
// MulticastEvents to ch.
|
||||||
|
func RunMCReader(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) {
|
||||||
|
for {
|
||||||
|
if err := runMCReaderInner(vpnNet, selfVPNIP, ch); err != nil {
|
||||||
|
log.Printf("[MCReader] %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(mcErrorRetryInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMCReaderInner(vpnNet netip.Prefix, selfVPNIP netip.Addr, ch chan<- MulticastEvent) error {
|
||||||
|
conn, err := net.ListenMulticastUDP("udp", nil, mcAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bind: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
netAddr := vpnNet.Addr().As4()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn.SetReadDeadline(time.Now().Add(32 * time.Second))
|
||||||
|
n, src, err := conn.ReadFromUDPAddrPort(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read: %w", err)
|
||||||
|
}
|
||||||
|
if n != mcBeaconLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
octets := netAddr
|
||||||
|
octets[3] = buf[0]
|
||||||
|
vpnIP := netip.AddrFrom4(octets)
|
||||||
|
if vpnIP == selfVPNIP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := wgtypes.NewKey(buf[1:33])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wgPort := binary.BigEndian.Uint16(buf[33:35])
|
||||||
|
endpoint := netip.AddrPortFrom(src.Addr().Unmap(), wgPort)
|
||||||
|
|
||||||
|
ch <- MulticastEvent{
|
||||||
|
pubKey: pubKey,
|
||||||
|
vpnIP: vpnIP,
|
||||||
|
endpoint: endpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package peer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ func (a *App) onAddPeer(p HubPeer) {
|
|||||||
IsPublic: p.IsPublic,
|
IsPublic: p.IsPublic,
|
||||||
Endpoint4: p.EndpointV4,
|
Endpoint4: p.EndpointV4,
|
||||||
Endpoint6: p.EndpointV6,
|
Endpoint6: p.EndpointV6,
|
||||||
|
RTT: time.Duration(math.MaxInt64) * time.Nanosecond,
|
||||||
Role: roleFor(a.isPublic, a.vpnIP, p),
|
Role: roleFor(a.isPublic, a.vpnIP, p),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +61,9 @@ func (a *App) onRemovePeer(key wgtypes.Key) {
|
|||||||
// switchActiveRelay promotes the lowest-latency relay peer to active.
|
// switchActiveRelay promotes the lowest-latency relay peer to active.
|
||||||
func (a *App) switchActiveRelay() {
|
func (a *App) switchActiveRelay() {
|
||||||
if a.relay != nil {
|
if a.relay != nil {
|
||||||
a.devAddDirect(a.relay, a.relay.WGEndpoint())
|
// If we have a relay, it's public, so should go back to being a direct
|
||||||
|
// peer - this will convert it's /24 to a /32.
|
||||||
|
a.devAddDirect(a.relay, a.relay.PreferredEndpoint())
|
||||||
a.relay = nil
|
a.relay = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +73,7 @@ func (a *App) switchActiveRelay() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if best == nil || betterRelay(p, best) {
|
if best == nil || p.RTT < best.RTT {
|
||||||
best = p
|
best = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,23 +82,10 @@ func (a *App) switchActiveRelay() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.devSetRelay(best, best.WGEndpoint())
|
a.devSetRelay(best, best.PreferredEndpoint())
|
||||||
a.relay = best
|
a.relay = best
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why not < ??
|
|
||||||
// betterRelay reports whether a is a better relay candidate than b.
|
|
||||||
// Prefers lower RTT; treats zero RTT (no measurement yet) as worst case.
|
|
||||||
func betterRelay(a, b *Peer) bool {
|
|
||||||
if a.RTT == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if b.RTT == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return a.RTT < b.RTT
|
|
||||||
}
|
|
||||||
|
|
||||||
func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort {
|
func preferredEndpoint(v4, v6 netip.AddrPort) netip.AddrPort {
|
||||||
if v4.IsValid() {
|
if v4.IsValid() {
|
||||||
return v4
|
return v4
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestOnAddPeer(t *testing.T) {
|
|||||||
check func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key)
|
check func(t *testing.T, a *App, dev *fakeWGDevice, key wgtypes.Key)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "non-public peer added in StateRelayed with no dev calls",
|
name: "non-public peer registered in WG via AddPeer",
|
||||||
peer: func(k wgtypes.Key) HubPeer {
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
return HubPeer{PubKey: k, VPNIP: peerVPNIP}
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP}
|
||||||
},
|
},
|
||||||
@@ -41,14 +41,14 @@ func TestOnAddPeer(t *testing.T) {
|
|||||||
if a.peersByIP[peerVPNIP] == nil {
|
if a.peersByIP[peerVPNIP] == nil {
|
||||||
t.Fatal("not in peersByIP")
|
t.Fatal("not in peersByIP")
|
||||||
}
|
}
|
||||||
if p.State != StateRelayed {
|
if p.State() != StateRelayed {
|
||||||
t.Fatalf("state = %v, want StateRelayed", p.State)
|
t.Fatalf("state = %v, want StateRelayed", p.State())
|
||||||
}
|
}
|
||||||
dev.AssertNoCalls(t)
|
dev.AssertAddPeer(t, 0, key)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "public peer with endpoint goes to StateDirect via AddDirect",
|
name: "public peer with endpoint registered via AddDirect",
|
||||||
peer: func(k wgtypes.Key) HubPeer {
|
peer: func(k wgtypes.Key) HubPeer {
|
||||||
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1}
|
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1}
|
||||||
},
|
},
|
||||||
@@ -57,13 +57,7 @@ func TestOnAddPeer(t *testing.T) {
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
t.Fatal("not in peersByKey")
|
t.Fatal("not in peersByKey")
|
||||||
}
|
}
|
||||||
if p.State != StateDirect {
|
dev.AssertAddDirect(t, 0, p.PubKey(), ep1, p.VPNIP)
|
||||||
t.Fatalf("state = %v, want StateDirect", p.State)
|
|
||||||
}
|
|
||||||
if p.WGEndpoint != ep1 {
|
|
||||||
t.Fatalf("WGEndpoint = %v, want %v", p.WGEndpoint, ep1)
|
|
||||||
}
|
|
||||||
dev.AssertAddDirect(t, 0, p.PubKey, p.WGEndpoint, p.VPNIP)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +98,7 @@ func TestOnAddPeer(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
a, dev, _ := newTestApp(t, "10.0.0.1", false, false)
|
||||||
key := mustKey(t)
|
key := mustKey(t)
|
||||||
if tc.setup != nil {
|
if tc.setup != nil {
|
||||||
tc.setup(a, key)
|
tc.setup(a, key)
|
||||||
@@ -138,14 +132,17 @@ func TestOnRemovePeer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "StateRelayed peer removed from maps without RemovePeer",
|
name: "StateRelayed peer removed from maps with RemovePeer",
|
||||||
setup: func(t *testing.T, a *App) wgtypes.Key {
|
setup: func(t *testing.T, a *App) wgtypes.Key {
|
||||||
key := mustKey(t)
|
key := mustKey(t)
|
||||||
a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")})
|
a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")})
|
||||||
return key
|
return key
|
||||||
},
|
},
|
||||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
dev.AssertNoCalls(t)
|
if len(dev.Calls) != 1 {
|
||||||
|
t.Fatalf("dev calls = %v, want [RemovePeer]", dev.Calls)
|
||||||
|
}
|
||||||
|
dev.AssertRemovePeer(t, 0, dev.Calls[0].PubKey)
|
||||||
if len(a.peersByKey) != 0 || len(a.peersByIP) != 0 {
|
if len(a.peersByKey) != 0 || len(a.peersByIP) != 0 {
|
||||||
t.Errorf("maps should be empty after remove")
|
t.Errorf("maps should be empty after remove")
|
||||||
}
|
}
|
||||||
@@ -173,7 +170,7 @@ func TestOnRemovePeer(t *testing.T) {
|
|||||||
setup: func(t *testing.T, a *App) wgtypes.Key {
|
setup: func(t *testing.T, a *App) wgtypes.Key {
|
||||||
relay := addRelayPeer(t, a, "10.0.0.10", ep1)
|
relay := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
a.relay = relay
|
a.relay = relay
|
||||||
return relay.PubKey
|
return relay.PubKey()
|
||||||
},
|
},
|
||||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
if len(dev.Calls) != 1 {
|
if len(dev.Calls) != 1 {
|
||||||
@@ -191,7 +188,7 @@ func TestOnRemovePeer(t *testing.T) {
|
|||||||
relay1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
relay1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
addRelayPeer(t, a, "10.0.0.11", ep2)
|
addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||||
a.relay = relay1
|
a.relay = relay1
|
||||||
return relay1.PubKey
|
return relay1.PubKey()
|
||||||
},
|
},
|
||||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
if len(dev.Calls) != 2 {
|
if len(dev.Calls) != 2 {
|
||||||
@@ -208,7 +205,7 @@ func TestOnRemovePeer(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
a, dev, _ := newTestApp(t, "10.0.0.1", false, false)
|
||||||
key := tc.setup(t, a)
|
key := tc.setup(t, a)
|
||||||
dev.Calls = nil
|
dev.Calls = nil
|
||||||
a.onRemovePeer(key)
|
a.onRemovePeer(key)
|
||||||
@@ -256,7 +253,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
|||||||
setup: func(t *testing.T, a *App) {
|
setup: func(t *testing.T, a *App) {
|
||||||
r1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
r1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
r1.RTT = 10 * time.Millisecond
|
r1.RTT = 10 * time.Millisecond
|
||||||
addRelayPeer(t, a, "10.0.0.11", ep2) // RTT stays 0
|
addRelayPeer(t, a, "10.0.0.11", ep2) // RTT stays MaxInt64 (unmeaured)
|
||||||
},
|
},
|
||||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||||
if len(dev.Calls) != 1 {
|
if len(dev.Calls) != 1 {
|
||||||
@@ -284,7 +281,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
|||||||
name: "stale relay demoted to direct before backup elected",
|
name: "stale relay demoted to direct before backup elected",
|
||||||
setup: func(t *testing.T, a *App) {
|
setup: func(t *testing.T, a *App) {
|
||||||
old := addRelayPeer(t, a, "10.0.0.10", ep1)
|
old := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||||
old.Up = false // stale — this is what triggers the switch from onTick
|
old.wgPeer.LastHandshakeTime = time.Time{} // stale — triggers switch from onTick
|
||||||
a.relay = old
|
a.relay = old
|
||||||
addRelayPeer(t, a, "10.0.0.11", ep2)
|
addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||||
},
|
},
|
||||||
@@ -296,7 +293,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
|||||||
t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0])
|
t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0])
|
||||||
}
|
}
|
||||||
dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet)
|
dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet)
|
||||||
if a.relay == nil || a.relay.WGEndpoint != ep2 {
|
if a.relay == nil || a.relay.Endpoint4 != ep2 {
|
||||||
t.Error("relay should be the backup peer")
|
t.Error("relay should be the backup peer")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -305,7 +302,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
a, dev := newTestApp(t, "10.0.0.1", false, false)
|
a, dev, _ := newTestApp(t, "10.0.0.1", false, false)
|
||||||
tc.setup(t, a)
|
tc.setup(t, a)
|
||||||
dev.Calls = nil
|
dev.Calls = nil
|
||||||
a.switchActiveRelay()
|
a.switchActiveRelay()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (a *App) onPing(e PingEvent) {
|
|||||||
// If we're the server, respond - this is always necessary as it's used to
|
// If we're the server, respond - this is always necessary as it's used to
|
||||||
// know if peers are up or down.
|
// know if peers are up or down.
|
||||||
if peer.Role == control.Server {
|
if peer.Role == control.Server {
|
||||||
a.sendPing(peer, e.ping.ID, e.ping.PingTS)
|
a.sendPing(peer, e.ping.PingTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute RTT from server echo.
|
// Compute RTT from server echo.
|
||||||
@@ -50,11 +50,9 @@ func (a *App) onPing(e PingEvent) {
|
|||||||
|
|
||||||
func (a *App) addProbe(peer *Peer, v4, v6 netip.AddrPort) {
|
func (a *App) addProbe(peer *Peer, v4, v6 netip.AddrPort) {
|
||||||
endpoint := preferredEndpoint(v4, v6)
|
endpoint := preferredEndpoint(v4, v6)
|
||||||
if !endpoint.IsValid() || endpoint == peer.WGEndpoint() {
|
if !endpoint.IsValid() || endpoint == peer.PreferredEndpoint() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
peer.UpdateEndpoints(v4, v6)
|
||||||
peer.Endpoint4 = v4
|
|
||||||
peer.Endpoint6 = v6
|
|
||||||
a.devAddProbe(peer, endpoint)
|
a.devAddProbe(peer, endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ import (
|
|||||||
func (a *App) onTick() {
|
func (a *App) onTick() {
|
||||||
wgPeers := a.devPeers()
|
wgPeers := a.devPeers()
|
||||||
|
|
||||||
a.nextPingID++
|
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
|
|
||||||
for _, wgPeer := range wgPeers {
|
for _, wgPeer := range wgPeers {
|
||||||
p, ok := a.peersByKey[wgPeer.PublicKey]
|
p, ok := a.peersByKey[wgPeer.PublicKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Fatalf("Wireguard peer not in index: %v", wgPeer)
|
log.Printf("Wireguard peer not in index, removing: %v", wgPeer)
|
||||||
|
a.devRemove(&Peer{wgPeer: wgPeer})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
p.wgPeer = wgPeer
|
p.wgPeer = wgPeer
|
||||||
|
|
||||||
// Send pings to peers where we're the client.
|
// Send pings to peers where we're the client.
|
||||||
if p.Role == control.Client {
|
if p.Role == control.Client {
|
||||||
a.sendPing(p, a.nextPingID, now)
|
a.sendPing(p, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch p.State() {
|
switch p.State() {
|
||||||
|
|||||||
220
peer/peer.go
220
peer/peer.go
@@ -1,220 +0,0 @@
|
|||||||
package peer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"vppn/m"
|
|
||||||
|
|
||||||
"git.crumpington.com/lib/go/flock"
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
type peerMain struct {
|
|
||||||
Globals
|
|
||||||
netName string
|
|
||||||
holePunch *HolePunch
|
|
||||||
controlServer *ControlServer
|
|
||||||
endpointReporter *EndpointReporter // non-nil on relay peers only
|
|
||||||
hubPoller *HubPoller
|
|
||||||
lockFile *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeerMain(args mainArgs) *peerMain {
|
|
||||||
logf := func(s string, args ...any) {
|
|
||||||
log.Printf("[Main] "+s, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(args)
|
|
||||||
|
|
||||||
config, err = loadPeerConfig(args.NetName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load configuration: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := loadNetworkState(args.NetName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load network state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wgPrivKey, err := wgtypes.ParseKey(config.WGPrivKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to parse WireGuard private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 create WireGuard device: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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("Invalid relay WG key: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
|
||||||
Globals: g,
|
|
||||||
netName: args.NetName,
|
|
||||||
holePunch: holePunch,
|
|
||||||
controlServer: controlServer,
|
|
||||||
endpointReporter: endpointReporter,
|
|
||||||
hubPoller: hubPoller,
|
|
||||||
lockFile: lockFile,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peerMain) Run() {
|
|
||||||
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(args mainArgs) {
|
|
||||||
privKey := generateWGKey()
|
|
||||||
pubKey := privKey.PublicKey()
|
|
||||||
|
|
||||||
initURL, err := url.Parse(args.HubAddress)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to parse hub URL: %v", err)
|
|
||||||
}
|
|
||||||
initURL.Path = "/peer/init/"
|
|
||||||
|
|
||||||
initArgs := m.PeerInitArgs{
|
|
||||||
WGPubKey: pubKey[:],
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if err := json.NewEncoder(buf).Encode(initArgs); err != nil {
|
|
||||||
log.Fatalf("Failed to encode init args: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, initURL.String(), buf)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to construct request: %v", err)
|
|
||||||
}
|
|
||||||
req.SetBasicAuth("", args.APIKey)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to init with hub: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
config := LocalConfig{
|
|
||||||
LocalPeerIP: initResp.PeerIP,
|
|
||||||
Network: initResp.Network,
|
|
||||||
WGPrivKey: privKey.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Print("Initialization successful.")
|
|
||||||
}
|
|
||||||
22
peer/ping.go
22
peer/ping.go
@@ -1,9 +1,21 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import "net/netip"
|
import (
|
||||||
|
"log"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
func (a *App) sendPing(p *Peer, id, ts int64) {
|
"vppn/peer/control"
|
||||||
_ = id
|
)
|
||||||
_ = ts
|
|
||||||
_ = netip.AddrPort{}
|
func (a *App) sendPing(p *Peer, ts int64) {
|
||||||
|
ping := control.Ping{
|
||||||
|
PingTS: ts,
|
||||||
|
SrcV4: a.selfV4,
|
||||||
|
SrcV6: a.selfV6,
|
||||||
|
Dst: p.WGEndpoint(),
|
||||||
|
}
|
||||||
|
dst := netip.AddrPortFrom(p.VPNIP, ControlPort)
|
||||||
|
if err := a.controlConn.SendPing(dst, ping); err != nil {
|
||||||
|
log.Printf("sendPing %v: %v", p.VPNIP, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,3 +71,12 @@ func (p *Peer) CanRelay() bool {
|
|||||||
func (p *Peer) PreferredEndpoint() netip.AddrPort {
|
func (p *Peer) PreferredEndpoint() netip.AddrPort {
|
||||||
return preferredEndpoint(p.Endpoint4, p.Endpoint6)
|
return preferredEndpoint(p.Endpoint4, p.Endpoint6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Peer) UpdateEndpoints(v4, v6 netip.AddrPort) {
|
||||||
|
if v4.IsValid() {
|
||||||
|
p.Endpoint4 = v4
|
||||||
|
}
|
||||||
|
if v6.IsValid() {
|
||||||
|
p.Endpoint6 = v6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user