initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
103
internal/vl1/noise.go
Normal file
103
internal/vl1/noise.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package vl1
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
type NoiseCipher struct {
|
||||
sendCipher cipher.AEAD
|
||||
recvCipher cipher.AEAD
|
||||
sendNonce uint64
|
||||
recvNonce uint64
|
||||
}
|
||||
|
||||
func NewNoiseCipher(sendKey, recvKey [32]byte) *NoiseCipher {
|
||||
send, err := chacha20poly1305.New(sendKey[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
recv, err := chacha20poly1305.New(recvKey[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &NoiseCipher{
|
||||
sendCipher: send,
|
||||
recvCipher: recv,
|
||||
}
|
||||
}
|
||||
|
||||
func DeriveKeysFromPSK(psk string, localPub, remotePub []byte) ([32]byte, [32]byte) {
|
||||
h := sha3.New256()
|
||||
h.Write([]byte(psk))
|
||||
h.Write(localPub)
|
||||
h.Write(remotePub)
|
||||
sum := h.Sum(nil)
|
||||
|
||||
var sendKey, recvKey [32]byte
|
||||
copy(sendKey[:], sum[:32])
|
||||
|
||||
h.Reset()
|
||||
h.Write(sum)
|
||||
h.Write([]byte("reverse"))
|
||||
rev := h.Sum(nil)
|
||||
copy(recvKey[:], rev[:32])
|
||||
|
||||
return sendKey, recvKey
|
||||
}
|
||||
|
||||
func (nc *NoiseCipher) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
nonce := make([]byte, 12)
|
||||
binary.BigEndian.PutUint64(nonce[4:], nc.sendNonce)
|
||||
nc.sendNonce++
|
||||
|
||||
ciphertext := nc.sendCipher.Seal(nil, nonce, plaintext, nil)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (nc *NoiseCipher) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
nonce := make([]byte, 12)
|
||||
binary.BigEndian.PutUint64(nonce[4:], nc.recvNonce)
|
||||
nc.recvNonce++
|
||||
|
||||
plaintext, err := nc.recvCipher.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func (nc *NoiseCipher) EncryptTo(buf []byte, plaintext []byte) (int, error) {
|
||||
nonce := make([]byte, 12)
|
||||
binary.BigEndian.PutUint64(nonce[4:], nc.sendNonce)
|
||||
nc.sendNonce++
|
||||
|
||||
ciphertext := nc.sendCipher.Seal(buf[:0], nonce, plaintext, nil)
|
||||
return len(ciphertext), nil
|
||||
}
|
||||
|
||||
func (nc *NoiseCipher) DecryptTo(buf []byte, ciphertext []byte) ([]byte, error) {
|
||||
nonce := make([]byte, 12)
|
||||
binary.BigEndian.PutUint64(nonce[4:], nc.recvNonce)
|
||||
nc.recvNonce++
|
||||
|
||||
plaintext, err := nc.recvCipher.Open(buf[:0], nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func generateSessionKey() [32]byte {
|
||||
var key [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, key[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
117
internal/vl1/packet.go
Normal file
117
internal/vl1/packet.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package vl1
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = 1
|
||||
MaxPacketSize = 65535
|
||||
HeaderSize = 8
|
||||
MaxFrameSize = 65535
|
||||
MinFrameSize = 14
|
||||
|
||||
PacketTypeHandshake = byte(1)
|
||||
PacketTypeData = byte(2)
|
||||
PacketTypeKeepalive = byte(3)
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
Version byte
|
||||
Type byte
|
||||
NetworkID uint32
|
||||
Length uint16
|
||||
}
|
||||
|
||||
func (h *Header) Encode(buf []byte) {
|
||||
buf[0] = h.Version
|
||||
buf[1] = h.Type
|
||||
binary.BigEndian.PutUint32(buf[2:6], h.NetworkID)
|
||||
binary.BigEndian.PutUint16(buf[6:8], h.Length)
|
||||
}
|
||||
|
||||
func (h *Header) Decode(buf []byte) error {
|
||||
if len(buf) < HeaderSize {
|
||||
return fmt.Errorf("header too short: %d < %d", len(buf), HeaderSize)
|
||||
}
|
||||
h.Version = buf[0]
|
||||
h.Type = buf[1]
|
||||
h.NetworkID = binary.BigEndian.Uint32(buf[2:6])
|
||||
h.Length = binary.BigEndian.Uint16(buf[6:8])
|
||||
return nil
|
||||
}
|
||||
|
||||
type Packet struct {
|
||||
Header Header
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func NewHandshakePacket(payload []byte) Packet {
|
||||
return Packet{
|
||||
Header: Header{
|
||||
Version: Version,
|
||||
Type: PacketTypeHandshake,
|
||||
Length: uint16(len(payload)),
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDataPacket(networkID uint32, payload []byte) Packet {
|
||||
return Packet{
|
||||
Header: Header{
|
||||
Version: Version,
|
||||
Type: PacketTypeData,
|
||||
NetworkID: networkID,
|
||||
Length: uint16(len(payload)),
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
func NewKeepalivePacket() Packet {
|
||||
return Packet{
|
||||
Header: Header{
|
||||
Version: Version,
|
||||
Type: PacketTypeKeepalive,
|
||||
Length: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) Encode() []byte {
|
||||
total := HeaderSize + len(p.Payload)
|
||||
buf := make([]byte, total)
|
||||
p.Header.Length = uint16(len(p.Payload))
|
||||
p.Header.Encode(buf[:HeaderSize])
|
||||
copy(buf[HeaderSize:], p.Payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func DecodePacket(data []byte) (*Packet, error) {
|
||||
var p Packet
|
||||
if err := p.Header.Decode(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen := int(p.Header.Length)
|
||||
if HeaderSize+payloadLen > len(data) {
|
||||
return nil, fmt.Errorf("packet truncated: header claims %d + %d > %d", HeaderSize, payloadLen, len(data))
|
||||
}
|
||||
p.Payload = make([]byte, payloadLen)
|
||||
copy(p.Payload, data[HeaderSize:HeaderSize+payloadLen])
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func DecodePacketInto(p *Packet, data []byte) error {
|
||||
if err := p.Header.Decode(data); err != nil {
|
||||
return err
|
||||
}
|
||||
payloadLen := int(p.Header.Length)
|
||||
if HeaderSize+payloadLen > len(data) {
|
||||
return fmt.Errorf("packet truncated")
|
||||
}
|
||||
p.Payload = make([]byte, payloadLen)
|
||||
copy(p.Payload, data[HeaderSize:HeaderSize+payloadLen])
|
||||
return nil
|
||||
}
|
||||
193
internal/vl1/peer.go
Normal file
193
internal/vl1/peer.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package vl1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"zeromesh/internal/identity"
|
||||
)
|
||||
|
||||
type Peer struct {
|
||||
Address identity.Address
|
||||
PublicKey [32]byte
|
||||
Endpoint *net.UDPAddr
|
||||
Cipher *NoiseCipher
|
||||
LastSeen time.Time
|
||||
LastSend time.Time
|
||||
Connected bool
|
||||
mu sync.RWMutex
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (p *Peer) Touch() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.LastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (p *Peer) SetCipher(c *NoiseCipher) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.Cipher = c
|
||||
p.Connected = true
|
||||
}
|
||||
|
||||
func (p *Peer) IsConnected() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.Connected
|
||||
}
|
||||
|
||||
func (p *Peer) IsAlive() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return time.Since(p.LastSeen) < 90*time.Second
|
||||
}
|
||||
|
||||
func (p *Peer) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
if p.Cipher == nil {
|
||||
return nil, ErrNoCipher
|
||||
}
|
||||
return p.Cipher.Encrypt(plaintext)
|
||||
}
|
||||
|
||||
func (p *Peer) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
if p.Cipher == nil {
|
||||
return nil, ErrNoCipher
|
||||
}
|
||||
return p.Cipher.Decrypt(ciphertext)
|
||||
}
|
||||
|
||||
func (p *Peer) EncryptTo(buf []byte, plaintext []byte) (int, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
if p.Cipher == nil {
|
||||
return 0, ErrNoCipher
|
||||
}
|
||||
return p.Cipher.EncryptTo(buf, plaintext)
|
||||
}
|
||||
|
||||
func (p *Peer) DecryptTo(buf []byte, ciphertext []byte) ([]byte, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
if p.Cipher == nil {
|
||||
return nil, ErrNoCipher
|
||||
}
|
||||
return p.Cipher.DecryptTo(buf, ciphertext)
|
||||
}
|
||||
|
||||
func (p *Peer) NeedsKeepalive() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.Connected && time.Since(p.LastSend) > 25*time.Second
|
||||
}
|
||||
|
||||
var ErrNoCipher = errNoCipher()
|
||||
|
||||
func errNoCipher() error {
|
||||
return &noCipherError{}
|
||||
}
|
||||
|
||||
type noCipherError struct{}
|
||||
|
||||
func (e *noCipherError) Error() string {
|
||||
return "no cipher established"
|
||||
}
|
||||
|
||||
type PeerManager struct {
|
||||
peers map[identity.Address]*Peer
|
||||
mu sync.RWMutex
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPeerManager(log *slog.Logger) *PeerManager {
|
||||
return &PeerManager{
|
||||
peers: make(map[identity.Address]*Peer),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *PeerManager) AddPeer(addr identity.Address, pubKey [32]byte, endpoint *net.UDPAddr) *Peer {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
peer := &Peer{
|
||||
Address: addr,
|
||||
PublicKey: pubKey,
|
||||
Endpoint: endpoint,
|
||||
LastSeen: time.Now(),
|
||||
LastSend: time.Now(),
|
||||
log: pm.log.With("peer", addr.String()),
|
||||
}
|
||||
pm.peers[addr] = peer
|
||||
return peer
|
||||
}
|
||||
|
||||
func (pm *PeerManager) GetPeer(addr identity.Address) *Peer {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
return pm.peers[addr]
|
||||
}
|
||||
|
||||
func (pm *PeerManager) GetPeerByEndpoint(endpoint *net.UDPAddr) *Peer {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
for _, p := range pm.peers {
|
||||
if p.Endpoint != nil && p.Endpoint.IP.Equal(endpoint.IP) && p.Endpoint.Port == endpoint.Port {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PeerManager) RemovePeer(addr identity.Address) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
delete(pm.peers, addr)
|
||||
}
|
||||
|
||||
func (pm *PeerManager) UpdatePeerEndpoint(addr identity.Address, endpoint *net.UDPAddr) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
if p, ok := pm.peers[addr]; ok {
|
||||
p.Endpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *PeerManager) ConnectedPeers() []*Peer {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
var result []*Peer
|
||||
for _, p := range pm.peers {
|
||||
if p.IsConnected() {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (pm *PeerManager) AllPeers() []*Peer {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
result := make([]*Peer, 0, len(pm.peers))
|
||||
for _, p := range pm.peers {
|
||||
result = append(result, p)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (pm *PeerManager) CleanDead() {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
for addr, p := range pm.peers {
|
||||
if p.Connected && !p.IsAlive() {
|
||||
p.log.Warn("peer timed out, removing")
|
||||
delete(pm.peers, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
112
internal/vl1/transport.go
Normal file
112
internal/vl1/transport.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package vl1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
conn *net.UDPConn
|
||||
port int
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewTransport(port int, log *slog.Logger) (*Transport, error) {
|
||||
addr := &net.UDPAddr{Port: port}
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bind UDP port %d: %w", port, err)
|
||||
}
|
||||
actualPort := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
log.Info("VL1 transport listening", "port", actualPort)
|
||||
return &Transport{
|
||||
conn: conn,
|
||||
port: actualPort,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Port() int {
|
||||
return t.port
|
||||
}
|
||||
|
||||
func (t *Transport) ReadFrom(buf []byte) (int, *net.UDPAddr, error) {
|
||||
n, addr, err := t.conn.ReadFromUDP(buf)
|
||||
return n, addr, err
|
||||
}
|
||||
|
||||
func (t *Transport) SendTo(data []byte, addr *net.UDPAddr) error {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
if t.closed {
|
||||
return fmt.Errorf("transport closed")
|
||||
}
|
||||
_, err := t.conn.WriteToUDP(data, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Transport) SendPacket(pkt *Packet, addr *net.UDPAddr) error {
|
||||
return t.SendTo(pkt.Encode(), addr)
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.closed = true
|
||||
return t.conn.Close()
|
||||
}
|
||||
|
||||
func (t *Transport) SetSocketBuffers(rcvBuf, sndBuf int) error {
|
||||
rawConn, err := t.conn.SyscallConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get raw conn: %w", err)
|
||||
}
|
||||
var setErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
if rcvBuf > 0 {
|
||||
if e := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, rcvBuf); e != nil {
|
||||
setErr = fmt.Errorf("set SO_RCVBUF=%d: %w", rcvBuf, e)
|
||||
return
|
||||
}
|
||||
}
|
||||
if sndBuf > 0 {
|
||||
if e := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUF, sndBuf); e != nil {
|
||||
setErr = fmt.Errorf("set SO_SNDBUF=%d: %w", sndBuf, e)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return setErr
|
||||
}
|
||||
|
||||
func (t *Transport) LocalAddr() net.Addr {
|
||||
return t.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (t *Transport) SetReadDeadline(deadline time.Time) error {
|
||||
return t.conn.SetReadDeadline(deadline)
|
||||
}
|
||||
|
||||
var packetBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, MaxPacketSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
func GetPacketBuf() *[]byte {
|
||||
return packetBufPool.Get().(*[]byte)
|
||||
}
|
||||
|
||||
func PutPacketBuf(buf *[]byte) {
|
||||
packetBufPool.Put(buf)
|
||||
}
|
||||
Reference in New Issue
Block a user