WIP
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"vppn/peer"
|
||||
)
|
||||
import "log"
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
peer.Main2()
|
||||
// TODO: construct peer.App and call Run (audit C1).
|
||||
log.Fatal("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -344,15 +344,15 @@ type Peer struct {
|
||||
Version int64
|
||||
APIKey string
|
||||
Name string
|
||||
PublicIP1 []byte
|
||||
Port1 uint16
|
||||
PublicIP2 []byte
|
||||
Port2 uint16
|
||||
Addr4 []byte
|
||||
Port4 uint16
|
||||
Addr6 []byte
|
||||
Port6 uint16
|
||||
Relay bool
|
||||
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(
|
||||
tx TX,
|
||||
@@ -363,7 +363,7 @@ func Peer_Insert(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ func Peer_Update(
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -404,7 +404,7 @@ func Peer_UpdateFull(
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -456,8 +456,8 @@ func Peer_Get(
|
||||
err error,
|
||||
) {
|
||||
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)
|
||||
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 {
|
||||
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.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil {
|
||||
row = nil
|
||||
}
|
||||
return
|
||||
@@ -473,7 +473,7 @@ func Peer_GetWhere(
|
||||
) {
|
||||
row = &Peer{}
|
||||
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
|
||||
}
|
||||
return
|
||||
@@ -495,7 +495,7 @@ func Peer_Iterate(
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,39 +59,39 @@ func Network_Validate(c *Network) error {
|
||||
|
||||
func Peer_Sanitize(p *Peer) {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
if len(p.PublicIP1) != 0 {
|
||||
if addr, ok := netip.AddrFromSlice(p.PublicIP1); ok {
|
||||
p.PublicIP1 = addr.AsSlice()
|
||||
if len(p.Addr4) != 0 {
|
||||
if addr, ok := netip.AddrFromSlice(p.Addr4); ok {
|
||||
p.Addr4 = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
if len(p.PublicIP2) != 0 {
|
||||
if addr, ok := netip.AddrFromSlice(p.PublicIP2); ok {
|
||||
p.PublicIP2 = addr.AsSlice()
|
||||
if len(p.Addr6) != 0 {
|
||||
if addr, ok := netip.AddrFromSlice(p.Addr6); ok {
|
||||
p.Addr6 = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
if p.Port1 == 0 {
|
||||
p.Port1 = 456
|
||||
if p.Port4 == 0 {
|
||||
p.Port4 = 456
|
||||
}
|
||||
if len(p.PublicIP2) != 0 && p.Port2 == 0 {
|
||||
p.Port2 = 456
|
||||
if len(p.Addr6) != 0 && p.Port6 == 0 {
|
||||
p.Port6 = 456
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Validate(p *Peer) error {
|
||||
if len(p.PublicIP1) > 0 {
|
||||
if _, ok := netip.AddrFromSlice(p.PublicIP1); !ok {
|
||||
if len(p.Addr4) > 0 {
|
||||
if _, ok := netip.AddrFromSlice(p.Addr4); !ok {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
}
|
||||
if len(p.PublicIP2) > 0 {
|
||||
if _, ok := netip.AddrFromSlice(p.PublicIP2); !ok {
|
||||
if len(p.Addr6) > 0 {
|
||||
if _, ok := netip.AddrFromSlice(p.Addr6); !ok {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
if p.Port2 == 0 {
|
||||
if p.Port6 == 0 {
|
||||
return ErrInvalidPort
|
||||
}
|
||||
}
|
||||
if p.Port1 == 0 {
|
||||
if p.Port4 == 0 {
|
||||
return ErrInvalidPort
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ TABLE peers OF Peer (
|
||||
Version int64,
|
||||
APIKey string NoUpdate,
|
||||
Name string,
|
||||
PublicIP1 []byte,
|
||||
Port1 uint16,
|
||||
PublicIP2 []byte,
|
||||
Port2 uint16,
|
||||
Addr4 []byte,
|
||||
Port4 uint16,
|
||||
Addr6 []byte,
|
||||
Port6 uint16,
|
||||
Relay bool,
|
||||
WGPubKey []byte NoUpdate
|
||||
);
|
||||
|
||||
@@ -15,10 +15,10 @@ CREATE TABLE peers (
|
||||
Version INTEGER NOT NULL, -- Changes when updated.
|
||||
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
|
||||
Name TEXT NOT NULL UNIQUE, -- For humans.
|
||||
PublicIP1 BLOB NOT NULL,
|
||||
Port1 INTEGER NOT NULL,
|
||||
PublicIP2 BLOB NOT NULL,
|
||||
Port2 INTEGER NOT NULL,
|
||||
Addr4 BLOB NOT NULL,
|
||||
Port4 INTEGER NOT NULL,
|
||||
Addr6 BLOB NOT NULL,
|
||||
Port6 INTEGER NOT NULL,
|
||||
Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
|
||||
WGPubKey BLOB NOT NULL,
|
||||
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 {
|
||||
var ip1Str, ip2Str string
|
||||
var addr4Str, addr6Str 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("Addr4", &addr4Str).
|
||||
Scan("Port4", &p.Port4).
|
||||
Scan("Addr6", &addr6Str).
|
||||
Scan("Port6", &p.Port6).
|
||||
Scan("Relay", &p.Relay).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.PublicIP1, err = stringToIP(ip1Str); err != nil {
|
||||
if p.Addr4, err = stringToIP(addr4Str); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.PublicIP2, err = stringToIP(ip2Str); err != nil {
|
||||
if p.Addr6, err = stringToIP(addr6Str); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -204,24 +204,24 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt
|
||||
return err
|
||||
}
|
||||
|
||||
var ip1Str, ip2Str string
|
||||
var addr4Str, addr6Str 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("Addr4", &addr4Str).
|
||||
Scan("Port4", &peer.Port4).
|
||||
Scan("Addr6", &addr6Str).
|
||||
Scan("Port6", &peer.Port6).
|
||||
Scan("Relay", &peer.Relay).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if peer.PublicIP1, err = stringToIP(ip1Str); err != nil {
|
||||
if peer.Addr4, err = stringToIP(addr4Str); err != nil {
|
||||
return err
|
||||
}
|
||||
if peer.PublicIP2, err = stringToIP(ip2Str); err != nil {
|
||||
if peer.Addr6, err = stringToIP(addr6Str); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -355,15 +355,15 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
|
||||
for _, p := range l {
|
||||
if len(p.WGPubKey) != 0 {
|
||||
peers[p.PeerIP] = &m.Peer{
|
||||
PeerIP: p.PeerIP,
|
||||
Version: p.Version,
|
||||
Name: p.Name,
|
||||
PublicIP1: p.PublicIP1,
|
||||
Port1: p.Port1,
|
||||
PublicIP2: p.PublicIP2,
|
||||
Port2: p.Port2,
|
||||
Relay: p.Relay,
|
||||
WGPubKey: p.WGPubKey,
|
||||
PeerIP: p.PeerIP,
|
||||
Version: p.Version,
|
||||
Name: p.Name,
|
||||
Addr4: p.Addr4,
|
||||
Port4: p.Port4,
|
||||
Addr6: p.Addr6,
|
||||
Port6: p.Port6,
|
||||
Relay: p.Relay,
|
||||
WGPubKey: p.WGPubKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
m/models.go
18
m/models.go
@@ -12,15 +12,15 @@ type PeerInitResp struct {
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PeerIP byte
|
||||
Version int64
|
||||
Name string
|
||||
PublicIP1 []byte
|
||||
Port1 uint16
|
||||
PublicIP2 []byte
|
||||
Port2 uint16
|
||||
Relay bool
|
||||
WGPubKey []byte
|
||||
PeerIP byte
|
||||
Version int64
|
||||
Name string
|
||||
Addr4 []byte
|
||||
Port4 uint16
|
||||
Addr6 []byte
|
||||
Port6 uint16
|
||||
Relay bool
|
||||
WGPubKey []byte
|
||||
}
|
||||
|
||||
type NetworkState struct {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -56,7 +55,7 @@ type App struct {
|
||||
|
||||
// Infrastructure
|
||||
dev WGDevice
|
||||
controlConn *net.UDPConn
|
||||
controlConn ControlConn
|
||||
|
||||
// Peer state
|
||||
relay *Peer
|
||||
@@ -67,9 +66,6 @@ type App struct {
|
||||
selfV4 netip.AddrPort
|
||||
selfV6 netip.AddrPort
|
||||
|
||||
// Monotonically increasing ID for outbound pings (client role only)
|
||||
nextPingID int64 // TODO: Remove
|
||||
|
||||
// Event channels fed by background goroutines
|
||||
hubAddCh <-chan HubPeer
|
||||
hubRemoveCh <-chan wgtypes.Key
|
||||
|
||||
@@ -25,10 +25,10 @@ func addRelayPeer(t *testing.T, a *App, vpnIP string, ep netip.AddrPort) *Peer {
|
||||
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").
|
||||
// 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()
|
||||
privKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
@@ -36,6 +36,7 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
||||
}
|
||||
ip := netip.MustParseAddr(vpnIP)
|
||||
dev := &fakeWGDevice{}
|
||||
cc := &fakeControlConn{}
|
||||
a := &App{
|
||||
vpnIP: ip,
|
||||
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,
|
||||
isRelay: isRelay,
|
||||
dev: dev,
|
||||
controlConn: cc,
|
||||
peersByKey: make(map[wgtypes.Key]*Peer),
|
||||
peersByIP: make(map[netip.Addr]*Peer),
|
||||
hubAddCh: make(chan HubPeer),
|
||||
@@ -51,5 +53,5 @@ func newTestApp(t *testing.T, vpnIP string, isPublic, isRelay bool) (*App, *fake
|
||||
pingCh: make(chan PingEvent),
|
||||
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 (
|
||||
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.
|
||||
//
|
||||
// In each peer pair, the peer with the lower VPN IP is the client: it sets ID
|
||||
// and PingTS and sends pings on a timer. The server echoes ID and PingTS back
|
||||
// in its response, allowing the client to compute RTT = now - PingTS.
|
||||
// In each peer pair, the peer with the lower VPN IP is the client: it sets
|
||||
// PingTS and sends pings on a timer. The server echoes PingTS back in its
|
||||
// response, allowing the client to compute RTT = now - PingTS.
|
||||
//
|
||||
// Both client and server populate SrcV4, SrcV6, and Dst on every packet so
|
||||
// endpoint information flows in both directions.
|
||||
@@ -27,50 +27,47 @@ const (
|
||||
// WireGuard handshake source. Zero if the sender has not observed a handshake
|
||||
// from the recipient.
|
||||
type Ping struct {
|
||||
ID int64 // Client ping ID.
|
||||
PingTS int64 // Client ping send time in nanoseconds.
|
||||
SrcV4 netip.AddrPort // Sender's discovered IPv4 address and port.
|
||||
SrcV6 netip.AddrPort // Sender's discovered IPv6 address and port.
|
||||
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 {
|
||||
var buf [Size]byte
|
||||
buf[0] = version
|
||||
binary.BigEndian.PutUint64(buf[1:9], uint64(p.ID))
|
||||
binary.BigEndian.PutUint64(buf[9:17], uint64(p.PingTS))
|
||||
binary.BigEndian.PutUint64(buf[1:9], uint64(p.PingTS))
|
||||
if p.SrcV4.IsValid() {
|
||||
a4 := p.SrcV4.Addr().As4()
|
||||
copy(buf[17:21], a4[:])
|
||||
binary.BigEndian.PutUint16(buf[21:23], p.SrcV4.Port())
|
||||
copy(buf[9:13], a4[:])
|
||||
binary.BigEndian.PutUint16(buf[13:15], p.SrcV4.Port())
|
||||
}
|
||||
a16 := p.SrcV6.Addr().As16()
|
||||
copy(buf[23:39], a16[:])
|
||||
binary.BigEndian.PutUint16(buf[39:41], p.SrcV6.Port())
|
||||
copy(buf[15:31], a16[:])
|
||||
binary.BigEndian.PutUint16(buf[31:33], p.SrcV6.Port())
|
||||
a16 = p.Dst.Addr().As16()
|
||||
copy(buf[41:57], a16[:])
|
||||
binary.BigEndian.PutUint16(buf[57:59], p.Dst.Port())
|
||||
copy(buf[33:49], a16[:])
|
||||
binary.BigEndian.PutUint16(buf[49:51], p.Dst.Port())
|
||||
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) {
|
||||
if buf[0] != version {
|
||||
return Ping{}, fmt.Errorf("unknown ping version %d", buf[0])
|
||||
}
|
||||
p := Ping{
|
||||
ID: int64(binary.BigEndian.Uint64(buf[1:9])),
|
||||
PingTS: int64(binary.BigEndian.Uint64(buf[9:17])),
|
||||
PingTS: int64(binary.BigEndian.Uint64(buf[1:9])),
|
||||
}
|
||||
if addr := netip.AddrFrom4([4]byte(buf[17:21])); !addr.IsUnspecified() {
|
||||
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[21:23]))
|
||||
if addr := netip.AddrFrom4([4]byte(buf[9:13])); !addr.IsUnspecified() {
|
||||
p.SrcV4 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[13:15]))
|
||||
}
|
||||
if addr := netip.AddrFrom16([16]byte(buf[23:39])); !addr.IsUnspecified() {
|
||||
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[39:41]))
|
||||
if addr := netip.AddrFrom16([16]byte(buf[15:31])); !addr.IsUnspecified() {
|
||||
p.SrcV6 = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[31:33]))
|
||||
}
|
||||
if addr := netip.AddrFrom16([16]byte(buf[41:57])).Unmap(); !addr.IsUnspecified() {
|
||||
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[57:59]))
|
||||
if addr := netip.AddrFrom16([16]byte(buf[33:49])).Unmap(); !addr.IsUnspecified() {
|
||||
p.Dst = netip.AddrPortFrom(addr, binary.BigEndian.Uint16(buf[49:51]))
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ func TestRoundTrip(t *testing.T) {
|
||||
{
|
||||
name: "client ping",
|
||||
ping: control.Ping{
|
||||
ID: 42,
|
||||
PingTS: 1234567890,
|
||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||
Dst: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||
@@ -28,7 +27,6 @@ func TestRoundTrip(t *testing.T) {
|
||||
{
|
||||
name: "server response",
|
||||
ping: control.Ping{
|
||||
ID: 42,
|
||||
PingTS: 1234567890,
|
||||
SrcV4: netip.MustParseAddrPort("5.6.7.8:51820"),
|
||||
Dst: netip.MustParseAddrPort("1.2.3.4:9999"),
|
||||
@@ -37,7 +35,6 @@ func TestRoundTrip(t *testing.T) {
|
||||
{
|
||||
name: "IPv6 only",
|
||||
ping: control.Ping{
|
||||
ID: 1,
|
||||
PingTS: 999,
|
||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||
Dst: netip.MustParseAddrPort("[2001:db8::2]:51820"),
|
||||
@@ -46,7 +43,6 @@ func TestRoundTrip(t *testing.T) {
|
||||
{
|
||||
name: "dual stack",
|
||||
ping: control.Ping{
|
||||
ID: 7,
|
||||
PingTS: 555,
|
||||
SrcV4: netip.MustParseAddrPort("1.2.3.4:51820"),
|
||||
SrcV6: netip.MustParseAddrPort("[2001:db8::1]:51820"),
|
||||
@@ -56,7 +52,6 @@ func TestRoundTrip(t *testing.T) {
|
||||
{
|
||||
name: "no src known",
|
||||
ping: control.Ping{
|
||||
ID: 3,
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
peers, err := a.dev.Peers()
|
||||
if err != nil {
|
||||
@@ -16,37 +38,25 @@ func (a *App) devPeers() []wgtypes.Peer {
|
||||
}
|
||||
|
||||
func (a *App) devAddPeer(p *Peer) {
|
||||
if err := a.dev.AddPeer(p.PubKey()); err != nil {
|
||||
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "AddPeer", func() error { return a.dev.AddPeer(p.PubKey()) })
|
||||
}
|
||||
|
||||
func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) {
|
||||
if err := a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP); err != nil {
|
||||
log.Fatalf("Failed to add peer %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "AddDirect", func() error { return a.dev.AddDirect(p.PubKey(), endpoint, p.VPNIP) })
|
||||
}
|
||||
|
||||
func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) {
|
||||
if err := a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet); err != nil {
|
||||
log.Fatalf("Failed to add relay %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "SetRelay", func() error { return a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet) })
|
||||
}
|
||||
|
||||
func (a *App) devPromote(p *Peer) {
|
||||
if err := a.dev.Promote(p.PubKey(), p.VPNIP); err != nil {
|
||||
log.Fatalf("Failed to promote peer %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "Promote", func() error { return a.dev.Promote(p.PubKey(), p.VPNIP) })
|
||||
}
|
||||
|
||||
func (a *App) devAddProbe(p *Peer, endpoint netip.AddrPort) {
|
||||
if err := a.dev.AddProbe(p.PubKey(), endpoint); err != nil {
|
||||
log.Fatalf("Failed to add probe %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "AddProbe", func() error { return a.dev.AddProbe(p.PubKey(), endpoint) })
|
||||
}
|
||||
|
||||
func (a *App) devRemove(p *Peer) {
|
||||
if err := a.dev.RemovePeer(p.PubKey()); err != nil {
|
||||
log.Fatalf("Failed to remove peer %v: %v", p.VPNIP, err)
|
||||
}
|
||||
devRetry(p.VPNIP, "RemovePeer", func() error { return a.dev.RemovePeer(p.PubKey()) })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
@@ -48,7 +50,7 @@ func storeJson(x any, outPath string) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -94,4 +96,3 @@ func loadPeerConfig(netName string) (pc LocalConfig, err error) {
|
||||
func loadNetworkState(netName string) (ps m.NetworkState, err error) {
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package peer
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
@@ -14,17 +16,13 @@ import (
|
||||
)
|
||||
|
||||
type HubPoller struct {
|
||||
Globals
|
||||
holePunch *HolePunch
|
||||
client *http.Client
|
||||
req *http.Request
|
||||
versions [256]int64
|
||||
netName string
|
||||
client *http.Client
|
||||
req *http.Request
|
||||
versions [256]int64
|
||||
netName string
|
||||
}
|
||||
|
||||
func NewHubPoller(
|
||||
g Globals,
|
||||
hp *HolePunch,
|
||||
netName,
|
||||
hubURL,
|
||||
apiKey string,
|
||||
@@ -118,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
|
||||
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
|
||||
return
|
||||
}
|
||||
if len(peer.PublicIP1) == 0 || peer.Port1 == 0 {
|
||||
if len(peer.Addr4) == 0 || peer.Port4 == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,12 +126,12 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
|
||||
return
|
||||
}
|
||||
|
||||
ip, ok := netip.AddrFromSlice(peer.PublicIP1)
|
||||
ip, ok := netip.AddrFromSlice(peer.Addr4)
|
||||
if !ok {
|
||||
hp.logf("Invalid public IP for peer %d", peer.PeerIP)
|
||||
return
|
||||
}
|
||||
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port1)
|
||||
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4)
|
||||
|
||||
if peer.Relay {
|
||||
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package peer
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"vppn/peer/control"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
@@ -17,3 +18,9 @@ type WGDevice interface {
|
||||
Promote(pubKey wgtypes.Key, vpnIP netip.Addr) 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
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package peer
|
||||
|
||||
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 (
|
||||
"log"
|
||||
"math"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
@@ -19,6 +21,7 @@ func (a *App) onAddPeer(p HubPeer) {
|
||||
IsPublic: p.IsPublic,
|
||||
Endpoint4: p.EndpointV4,
|
||||
Endpoint6: p.EndpointV6,
|
||||
RTT: time.Duration(math.MaxInt64) * time.Nanosecond,
|
||||
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.
|
||||
func (a *App) switchActiveRelay() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -68,7 +73,7 @@ func (a *App) switchActiveRelay() {
|
||||
continue
|
||||
}
|
||||
|
||||
if best == nil || betterRelay(p, best) {
|
||||
if best == nil || p.RTT < best.RTT {
|
||||
best = p
|
||||
}
|
||||
}
|
||||
@@ -77,23 +82,10 @@ func (a *App) switchActiveRelay() {
|
||||
return
|
||||
}
|
||||
|
||||
a.devSetRelay(best, best.WGEndpoint())
|
||||
a.devSetRelay(best, best.PreferredEndpoint())
|
||||
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 {
|
||||
if v4.IsValid() {
|
||||
return v4
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestOnAddPeer(t *testing.T) {
|
||||
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 {
|
||||
return HubPeer{PubKey: k, VPNIP: peerVPNIP}
|
||||
},
|
||||
@@ -41,14 +41,14 @@ func TestOnAddPeer(t *testing.T) {
|
||||
if a.peersByIP[peerVPNIP] == nil {
|
||||
t.Fatal("not in peersByIP")
|
||||
}
|
||||
if p.State != StateRelayed {
|
||||
t.Fatalf("state = %v, want StateRelayed", p.State)
|
||||
if p.State() != StateRelayed {
|
||||
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 {
|
||||
return HubPeer{PubKey: k, VPNIP: peerVPNIP, IsPublic: true, EndpointV4: ep1}
|
||||
},
|
||||
@@ -57,13 +57,7 @@ func TestOnAddPeer(t *testing.T) {
|
||||
if p == nil {
|
||||
t.Fatal("not in peersByKey")
|
||||
}
|
||||
if p.State != StateDirect {
|
||||
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)
|
||||
dev.AssertAddDirect(t, 0, p.PubKey(), ep1, p.VPNIP)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -104,7 +98,7 @@ func TestOnAddPeer(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
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)
|
||||
if tc.setup != nil {
|
||||
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 {
|
||||
key := mustKey(t)
|
||||
a.onAddPeer(HubPeer{PubKey: key, VPNIP: netip.MustParseAddr("10.0.0.2")})
|
||||
return key
|
||||
},
|
||||
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 {
|
||||
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 {
|
||||
relay := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||
a.relay = relay
|
||||
return relay.PubKey
|
||||
return relay.PubKey()
|
||||
},
|
||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||
if len(dev.Calls) != 1 {
|
||||
@@ -191,7 +188,7 @@ func TestOnRemovePeer(t *testing.T) {
|
||||
relay1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||
addRelayPeer(t, a, "10.0.0.11", ep2)
|
||||
a.relay = relay1
|
||||
return relay1.PubKey
|
||||
return relay1.PubKey()
|
||||
},
|
||||
check: func(t *testing.T, a *App, dev *fakeWGDevice) {
|
||||
if len(dev.Calls) != 2 {
|
||||
@@ -208,7 +205,7 @@ func TestOnRemovePeer(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
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)
|
||||
dev.Calls = nil
|
||||
a.onRemovePeer(key)
|
||||
@@ -256,7 +253,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
||||
setup: func(t *testing.T, a *App) {
|
||||
r1 := addRelayPeer(t, a, "10.0.0.10", ep1)
|
||||
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) {
|
||||
if len(dev.Calls) != 1 {
|
||||
@@ -284,7 +281,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
||||
name: "stale relay demoted to direct before backup elected",
|
||||
setup: func(t *testing.T, a *App) {
|
||||
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
|
||||
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])
|
||||
}
|
||||
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")
|
||||
}
|
||||
},
|
||||
@@ -305,7 +302,7 @@ func TestSwitchActiveRelay(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
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)
|
||||
dev.Calls = nil
|
||||
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
|
||||
// know if peers are up or down.
|
||||
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.
|
||||
@@ -50,11 +50,9 @@ func (a *App) onPing(e PingEvent) {
|
||||
|
||||
func (a *App) addProbe(peer *Peer, v4, v6 netip.AddrPort) {
|
||||
endpoint := preferredEndpoint(v4, v6)
|
||||
if !endpoint.IsValid() || endpoint == peer.WGEndpoint() {
|
||||
if !endpoint.IsValid() || endpoint == peer.PreferredEndpoint() {
|
||||
return
|
||||
}
|
||||
|
||||
peer.Endpoint4 = v4
|
||||
peer.Endpoint6 = v6
|
||||
peer.UpdateEndpoints(v4, v6)
|
||||
a.devAddProbe(peer, endpoint)
|
||||
}
|
||||
|
||||
@@ -11,19 +11,20 @@ import (
|
||||
func (a *App) onTick() {
|
||||
wgPeers := a.devPeers()
|
||||
|
||||
a.nextPingID++
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
for _, wgPeer := range wgPeers {
|
||||
p, ok := a.peersByKey[wgPeer.PublicKey]
|
||||
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
|
||||
|
||||
// Send pings to peers where we're the client.
|
||||
if p.Role == control.Client {
|
||||
a.sendPing(p, a.nextPingID, now)
|
||||
a.sendPing(p, now)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import "net/netip"
|
||||
import (
|
||||
"log"
|
||||
"net/netip"
|
||||
|
||||
func (a *App) sendPing(p *Peer, id, ts int64) {
|
||||
_ = id
|
||||
_ = ts
|
||||
_ = netip.AddrPort{}
|
||||
"vppn/peer/control"
|
||||
)
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user