mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
555 lines
12 KiB
Go
555 lines
12 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
//go:build linux && !js
|
|
|
|
package interfaces
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"sync"
|
|
|
|
"github.com/mdlayher/vsock"
|
|
|
|
"quad4/reticulum-go/pkg/common"
|
|
"quad4/reticulum-go/pkg/debug"
|
|
"quad4/reticulum-go/pkg/protect"
|
|
)
|
|
|
|
const vsockBitrateGuess int64 = 1_000_000_000
|
|
|
|
// ParseVSOCKContextID converts a config integer to a vsock context ID.
|
|
// Negative values and values above math.MaxUint32 are rejected.
|
|
func ParseVSOCKContextID(v int) (uint32, error) {
|
|
if v < 0 {
|
|
return 0, fmt.Errorf("vsock context ID must be non-negative, got %d", v)
|
|
}
|
|
if uint64(v) > math.MaxUint32 {
|
|
return 0, fmt.Errorf("vsock context ID overflows uint32, got %d", v)
|
|
}
|
|
return uint32(v), nil
|
|
}
|
|
|
|
// VSOCKClientInterface dials an AF_VSOCK peer and carries HDLC frames.
|
|
type VSOCKClientInterface struct {
|
|
BaseInterface
|
|
conn net.Conn
|
|
contextID uint32
|
|
port uint32
|
|
maxReconnectTries int
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
reconnect *reconnectDriver
|
|
txFrame []byte
|
|
txMu sync.Mutex
|
|
readBuf []byte
|
|
}
|
|
|
|
// NewVSOCKClientInterface constructs a VSOCK client with unlimited reconnect by default.
|
|
func NewVSOCKClientInterface(name string, contextID uint32, port uint32, enabled bool) (*VSOCKClientInterface, error) {
|
|
return NewVSOCKClientInterfaceWithRetries(name, contextID, port, enabled, 0)
|
|
}
|
|
|
|
// NewVSOCKClientInterfaceWithRetries constructs a VSOCK client with a reconnect limit.
|
|
func NewVSOCKClientInterfaceWithRetries(name string, contextID uint32, port uint32, enabled bool, maxTries int) (*VSOCKClientInterface, error) {
|
|
vc := &VSOCKClientInterface{
|
|
BaseInterface: NewBaseInterface(name, common.IFTypeVSOCK, enabled),
|
|
contextID: contextID,
|
|
port: port,
|
|
maxReconnectTries: NormalizeMaxReconnectTries(maxTries),
|
|
done: make(chan struct{}),
|
|
txFrame: make([]byte, 0, DefaultMTU*2+4),
|
|
}
|
|
vc.MTU = DefaultMTU
|
|
vc.Bitrate = vsockBitrateGuess
|
|
vc.In = true
|
|
vc.Out = true
|
|
vc.initReconnectDriver()
|
|
if enabled {
|
|
vc.startReconnect()
|
|
}
|
|
return vc, nil
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) initReconnectDriver() {
|
|
label := fmt.Sprintf("vsock:%d:%d", vc.contextID, vc.port)
|
|
vc.reconnect = newReconnectDriver(label, vc.maxReconnectTries, vc.done, vc.dialSession, vc.onConnected)
|
|
vc.reconnect.setOnExhausted(func() {
|
|
_ = vc.Stop()
|
|
})
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) dialSession() (net.Conn, error) {
|
|
return vsock.Dial(vc.contextID, vc.port, nil)
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) onConnected(conn net.Conn) {
|
|
if !vc.adoptConn(conn) {
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
go vc.readLoop()
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) adoptConn(conn net.Conn) bool {
|
|
vc.Mutex.Lock()
|
|
defer vc.Mutex.Unlock()
|
|
if vc.Detached {
|
|
return false
|
|
}
|
|
select {
|
|
case <-vc.done:
|
|
return false
|
|
default:
|
|
}
|
|
vc.conn = conn
|
|
vc.Online = true
|
|
return true
|
|
}
|
|
|
|
// SetConnectivityHooks registers reconnect up/down callbacks.
|
|
func (vc *VSOCKClientInterface) SetConnectivityHooks(onDown, onUp func()) {
|
|
vc.Mutex.Lock()
|
|
reconnect := vc.reconnect
|
|
vc.Mutex.Unlock()
|
|
if reconnect != nil {
|
|
reconnect.setHooks(onDown, onUp)
|
|
}
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) startReconnect() {
|
|
vc.Mutex.Lock()
|
|
reconnect := vc.reconnect
|
|
vc.Mutex.Unlock()
|
|
if reconnect != nil {
|
|
reconnect.start()
|
|
}
|
|
}
|
|
|
|
// Start begins or resumes the reconnect dial loop.
|
|
func (vc *VSOCKClientInterface) Start() error {
|
|
vc.Mutex.Lock()
|
|
if !vc.Enabled || vc.Detached {
|
|
vc.Mutex.Unlock()
|
|
return fmt.Errorf("interface not enabled or detached")
|
|
}
|
|
if vc.conn != nil {
|
|
vc.Online = true
|
|
go vc.readLoop()
|
|
vc.Mutex.Unlock()
|
|
return nil
|
|
}
|
|
select {
|
|
case <-vc.done:
|
|
vc.done = make(chan struct{})
|
|
vc.stopOnce = sync.Once{}
|
|
vc.initReconnectDriver()
|
|
default:
|
|
if vc.done == nil {
|
|
vc.done = make(chan struct{})
|
|
vc.stopOnce = sync.Once{}
|
|
}
|
|
if vc.reconnect == nil {
|
|
vc.initReconnectDriver()
|
|
}
|
|
}
|
|
vc.Mutex.Unlock()
|
|
vc.startReconnect()
|
|
return nil
|
|
}
|
|
|
|
// Stop closes the session and stops reconnect.
|
|
func (vc *VSOCKClientInterface) Stop() error {
|
|
vc.Mutex.Lock()
|
|
vc.Enabled = false
|
|
vc.Online = false
|
|
if vc.conn != nil {
|
|
_ = vc.conn.Close()
|
|
vc.conn = nil
|
|
}
|
|
vc.Mutex.Unlock()
|
|
vc.stopOnce.Do(func() {
|
|
if vc.done != nil {
|
|
close(vc.done)
|
|
}
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// ProcessOutgoing writes an HDLC-framed payload on the VSOCK connection.
|
|
func (vc *VSOCKClientInterface) ProcessOutgoing(data []byte) error {
|
|
vc.Mutex.RLock()
|
|
online := vc.Online
|
|
conn := vc.conn
|
|
vc.Mutex.RUnlock()
|
|
if !online || conn == nil {
|
|
return fmt.Errorf("interface offline")
|
|
}
|
|
vc.txMu.Lock()
|
|
frame := appendFrameHDLC(vc.txFrame[:0], data)
|
|
vc.txFrame = frame
|
|
out := append([]byte(nil), frame...)
|
|
vc.txMu.Unlock()
|
|
_, err := conn.Write(out)
|
|
if err != nil {
|
|
vc.Mutex.Lock()
|
|
vc.Online = false
|
|
detached := vc.Detached
|
|
reconnect := vc.reconnect
|
|
vc.Mutex.Unlock()
|
|
if !detached && reconnect != nil {
|
|
vc.teardownConn()
|
|
reconnect.notifyFailure()
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Send applies IFAC then ProcessOutgoing.
|
|
func (vc *VSOCKClientInterface) Send(data []byte, address string) error {
|
|
_ = address
|
|
if err := common.RejectReceiveOnly(vc); err != nil {
|
|
return err
|
|
}
|
|
masked, err := common.ApplyIFACOutbound(vc, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := vc.ProcessOutgoing(masked); err != nil {
|
|
return err
|
|
}
|
|
vc.updateBandwidthStats(uint64(len(masked)))
|
|
return nil
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) readLoop() {
|
|
decoder := newHDLCToggleStreamDecoder(vc.MTU, func(payload []byte) {
|
|
if len(payload) == 0 {
|
|
return
|
|
}
|
|
vc.ProcessIncoming(payload)
|
|
})
|
|
n := streamReadSize(vc.MTU)
|
|
if cap(vc.readBuf) < n {
|
|
vc.readBuf = make([]byte, n)
|
|
}
|
|
buffer := vc.readBuf[:n]
|
|
for {
|
|
vc.Mutex.RLock()
|
|
conn := vc.conn
|
|
done := vc.done
|
|
vc.Mutex.RUnlock()
|
|
if conn == nil {
|
|
return
|
|
}
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
n, err := conn.Read(buffer)
|
|
if n > 0 {
|
|
decoder.feed(buffer[:n])
|
|
}
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
debug.Log(debug.DebugVerbose, "VSOCK client read ended", "name", vc.Name, "error", err)
|
|
}
|
|
vc.Mutex.Lock()
|
|
vc.Online = false
|
|
detached := vc.Detached
|
|
reconnect := vc.reconnect
|
|
vc.Mutex.Unlock()
|
|
if !detached && reconnect != nil {
|
|
vc.teardownConn()
|
|
reconnect.notifyFailure()
|
|
} else {
|
|
vc.teardownConn()
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (vc *VSOCKClientInterface) teardownConn() {
|
|
vc.Mutex.Lock()
|
|
if vc.conn != nil {
|
|
_ = vc.conn.Close()
|
|
vc.conn = nil
|
|
}
|
|
vc.Mutex.Unlock()
|
|
}
|
|
|
|
// ContextID returns the peer context ID this client dials.
|
|
func (vc *VSOCKClientInterface) ContextID() uint32 {
|
|
return vc.contextID
|
|
}
|
|
|
|
// Port returns the peer port this client dials.
|
|
func (vc *VSOCKClientInterface) Port() uint32 {
|
|
return vc.port
|
|
}
|
|
|
|
// VSOCKServerInterface listens for AF_VSOCK peers and fans out HDLC frames.
|
|
type VSOCKServerInterface struct {
|
|
BaseInterface
|
|
connections map[string]net.Conn
|
|
listener net.Listener
|
|
listenCID uint32
|
|
port uint32
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
txFrame []byte
|
|
txMu sync.Mutex
|
|
acceptWg sync.WaitGroup
|
|
}
|
|
|
|
// NewVSOCKServerInterface constructs a VSOCK server that listens on Local CID.
|
|
// Local (1) enables same-host loopback without a guest VM.
|
|
func NewVSOCKServerInterface(name string, port uint32) (*VSOCKServerInterface, error) {
|
|
vs := &VSOCKServerInterface{
|
|
BaseInterface: NewBaseInterface(name, common.IFTypeVSOCK, true),
|
|
connections: make(map[string]net.Conn),
|
|
listenCID: vsock.Local,
|
|
port: port,
|
|
done: make(chan struct{}),
|
|
txFrame: make([]byte, 0, DefaultMTU*2+4),
|
|
}
|
|
vs.MTU = DefaultMTU
|
|
vs.Bitrate = vsockBitrateGuess
|
|
vs.In = true
|
|
vs.Out = true
|
|
return vs, nil
|
|
}
|
|
|
|
// SetListenContextID sets the CID used by Start. Call before Start.
|
|
// Use vsock.Local for same-host tests, or the machine CID for guest/host binds.
|
|
func (vs *VSOCKServerInterface) SetListenContextID(cid uint32) {
|
|
vs.Mutex.Lock()
|
|
vs.listenCID = cid
|
|
vs.Mutex.Unlock()
|
|
}
|
|
|
|
// Start listens and accepts VSOCK connections.
|
|
func (vs *VSOCKServerInterface) Start() error {
|
|
vs.acceptWg.Wait()
|
|
vs.Mutex.Lock()
|
|
if vs.listener != nil {
|
|
vs.Mutex.Unlock()
|
|
return fmt.Errorf("VSOCK server already started")
|
|
}
|
|
select {
|
|
case <-vs.done:
|
|
vs.done = make(chan struct{})
|
|
vs.stopOnce = sync.Once{}
|
|
default:
|
|
if vs.done == nil {
|
|
vs.done = make(chan struct{})
|
|
vs.stopOnce = sync.Once{}
|
|
}
|
|
}
|
|
listenCID := vs.listenCID
|
|
port := vs.port
|
|
vs.Mutex.Unlock()
|
|
|
|
ln, err := vsock.ListenContextID(listenCID, port, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start VSOCK server: %w", err)
|
|
}
|
|
|
|
vs.Mutex.Lock()
|
|
vs.listener = ln
|
|
if addr, ok := ln.Addr().(*vsock.Addr); ok {
|
|
vs.port = addr.Port
|
|
vs.listenCID = addr.ContextID
|
|
}
|
|
vs.Online = true
|
|
vs.Mutex.Unlock()
|
|
|
|
vs.acceptWg.Go(func() {
|
|
vs.acceptLoop(ln)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (vs *VSOCKServerInterface) acceptLoop(ln net.Listener) {
|
|
for {
|
|
vs.Mutex.RLock()
|
|
done := vs.done
|
|
vs.Mutex.RUnlock()
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
vs.Mutex.RLock()
|
|
online := vs.Online
|
|
vs.Mutex.RUnlock()
|
|
if !online {
|
|
return
|
|
}
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
debug.Log(debug.DebugVerbose, "VSOCK accept error", "name", vs.Name, "error", err)
|
|
continue
|
|
}
|
|
d, release := protect.AdmitConn(vs.Name)
|
|
if !d.Allow {
|
|
_ = conn.Close()
|
|
continue
|
|
}
|
|
go func(c net.Conn, rel func()) {
|
|
defer rel()
|
|
vs.handleConn(c)
|
|
}(conn, release)
|
|
}
|
|
}
|
|
|
|
func (vs *VSOCKServerInterface) handleConn(conn net.Conn) {
|
|
addr := conn.RemoteAddr().String()
|
|
vs.Mutex.Lock()
|
|
vs.connections[addr] = conn
|
|
vs.Mutex.Unlock()
|
|
defer func() {
|
|
vs.Mutex.Lock()
|
|
delete(vs.connections, addr)
|
|
vs.Mutex.Unlock()
|
|
_ = conn.Close()
|
|
}()
|
|
vs.readHDLCLoop(conn)
|
|
}
|
|
|
|
// SessionCount returns the number of active VSOCK sessions.
|
|
func (vs *VSOCKServerInterface) SessionCount() int {
|
|
vs.Mutex.RLock()
|
|
defer vs.Mutex.RUnlock()
|
|
return len(vs.connections)
|
|
}
|
|
|
|
func (vs *VSOCKServerInterface) readHDLCLoop(conn net.Conn) {
|
|
peerKey := conn.RemoteAddr().String()
|
|
decoder := newHDLCToggleStreamDecoder(vs.MTU, func(payload []byte) {
|
|
if len(payload) == 0 {
|
|
return
|
|
}
|
|
vs.ProcessIncomingFrom(payload, peerKey)
|
|
})
|
|
buf := make([]byte, streamReadSize(vs.MTU))
|
|
for {
|
|
vs.Mutex.RLock()
|
|
done := vs.done
|
|
vs.Mutex.RUnlock()
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
n, err := conn.Read(buf)
|
|
if n > 0 {
|
|
decoder.feed(buf[:n])
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop closes the listener and all sessions.
|
|
func (vs *VSOCKServerInterface) Stop() error {
|
|
vs.Mutex.Lock()
|
|
vs.Online = false
|
|
ln := vs.listener
|
|
vs.listener = nil
|
|
conns := make([]net.Conn, 0, len(vs.connections))
|
|
for addr, conn := range vs.connections {
|
|
conns = append(conns, conn)
|
|
delete(vs.connections, addr)
|
|
}
|
|
vs.Mutex.Unlock()
|
|
if ln != nil {
|
|
_ = ln.Close()
|
|
}
|
|
for _, conn := range conns {
|
|
_ = conn.Close()
|
|
}
|
|
vs.stopOnce.Do(func() {
|
|
if vs.done != nil {
|
|
close(vs.done)
|
|
}
|
|
})
|
|
vs.acceptWg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// ProcessOutgoing fans out an HDLC frame to all sessions.
|
|
func (vs *VSOCKServerInterface) ProcessOutgoing(data []byte) error {
|
|
vs.Mutex.RLock()
|
|
online := vs.Online
|
|
vs.Mutex.RUnlock()
|
|
if !online {
|
|
return fmt.Errorf("interface offline")
|
|
}
|
|
vs.txMu.Lock()
|
|
frame := appendFrameHDLC(vs.txFrame[:0], data)
|
|
vs.txFrame = frame
|
|
out := append([]byte(nil), frame...)
|
|
vs.txMu.Unlock()
|
|
vs.Mutex.Lock()
|
|
conns := make([]net.Conn, 0, len(vs.connections))
|
|
for _, c := range vs.connections {
|
|
conns = append(conns, c)
|
|
}
|
|
vs.Mutex.Unlock()
|
|
if len(conns) == 0 {
|
|
return fmt.Errorf("no VSOCK sessions")
|
|
}
|
|
var lastErr error
|
|
for _, c := range conns {
|
|
if _, err := c.Write(out); err != nil {
|
|
debug.Log(debug.DebugVerbose, "VSOCK server write failed", "address", c.RemoteAddr(), "error", err)
|
|
lastErr = err
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// Send applies IFAC then ProcessOutgoing.
|
|
func (vs *VSOCKServerInterface) Send(data []byte, address string) error {
|
|
_ = address
|
|
if err := common.RejectReceiveOnly(vs); err != nil {
|
|
return err
|
|
}
|
|
masked, err := common.ApplyIFACOutbound(vs, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := vs.ProcessOutgoing(masked); err != nil {
|
|
return err
|
|
}
|
|
vs.updateBandwidthStats(uint64(len(masked)))
|
|
return nil
|
|
}
|
|
|
|
// ListenAddr returns the bound address after Start.
|
|
func (vs *VSOCKServerInterface) ListenAddr() net.Addr {
|
|
vs.Mutex.RLock()
|
|
defer vs.Mutex.RUnlock()
|
|
if vs.listener == nil {
|
|
return nil
|
|
}
|
|
return vs.listener.Addr()
|
|
}
|
|
|
|
// Port returns the bound port after Start (or the configured port before).
|
|
func (vs *VSOCKServerInterface) Port() uint32 {
|
|
vs.Mutex.RLock()
|
|
defer vs.Mutex.RUnlock()
|
|
return vs.port
|
|
}
|