Files
Reticulum-Go/cmd/reticulum-go/main.go
Sudo-Ivan 1d3a969742
Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m8s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m6s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m4s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
chore: add SPDX license identifier and copyright notice
2025-12-31 20:44:58 -06:00

643 lines
20 KiB
Go

// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package main
import (
"encoding/binary"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"git.quad4.io/Networks/Reticulum-Go/internal/config"
"git.quad4.io/Networks/Reticulum-Go/internal/storage"
"git.quad4.io/Networks/Reticulum-Go/pkg/buffer"
"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)
var (
interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
)
const (
ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour)
ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate
ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces
APP_NAME = "Reticulum-Go Test Node"
APP_ASPECT = "node" // Always use "node" for node announces
)
type Reticulum struct {
config *common.ReticulumConfig
transport *transport.Transport
interfaces []interfaces.Interface
channels map[string]*channel.Channel
buffers map[string]*buffer.Buffer
pathRequests map[string]*common.PathRequest
announceHistory map[string]announceRecord
announceHistoryMu sync.RWMutex
identity *identity.Identity
destination *destination.Destination
storage *storage.Manager
// Node-specific information
maxTransferSize int16 // Max transfer size in KB
nodeEnabled bool // Whether this node is enabled
nodeTimestamp int64 // Last node announcement timestamp
}
type announceRecord struct {
timestamp int64
appData []byte
}
func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
if cfg == nil {
cfg = config.DefaultConfig()
}
// Set default app name and aspect if not provided
if cfg.AppName == "" {
cfg.AppName = APP_NAME
}
if cfg.AppAspect == "" {
cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
}
if err := initializeDirectories(); err != nil {
return nil, fmt.Errorf("failed to initialize directories: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Directories initialized")
// Initialize storage manager
storageMgr, err := storage.NewManager()
if err != nil {
return nil, fmt.Errorf("failed to initialize storage manager: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Storage manager initialized")
t := transport.NewTransport(cfg)
debug.Log(debug.DEBUG_INFO, "Transport initialized")
// Load or create identity
identityPath := storageMgr.GetIdentityPath()
var ident *identity.Identity
if _, err := os.Stat(identityPath); err == nil {
// Identity file exists, load it
ident, err = identity.FromFile(identityPath)
if err != nil {
return nil, fmt.Errorf("failed to load identity: %v", err)
}
debug.Log(debug.DEBUG_ERROR, "Loaded existing identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
} else {
// Create new identity
ident, err = identity.NewIdentity()
if err != nil {
return nil, fmt.Errorf("failed to create identity: %v", err)
}
debug.Log(debug.DEBUG_ERROR, "Created new identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
// Save it to disk
if err := ident.ToFile(identityPath); err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to save identity to file", common.STR_ERROR, err)
} else {
debug.Log(debug.DEBUG_INFO, "Identity saved to file", "path", identityPath)
}
}
// Create destination
debug.Log(debug.DEBUG_INFO, "Creating destination...")
dest, err := destination.New(
ident,
destination.IN,
destination.SINGLE,
"nomadnetwork",
t,
"node",
)
if err != nil {
return nil, fmt.Errorf("failed to create destination: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Created destination with hash", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, dest.GetHash()))
// Set node metadata
nodeTimestamp := time.Now().Unix()
r := &Reticulum{
config: cfg,
transport: t,
interfaces: make([]interfaces.Interface, 0),
channels: make(map[string]*channel.Channel),
buffers: make(map[string]*buffer.Buffer),
pathRequests: make(map[string]*common.PathRequest),
announceHistory: make(map[string]announceRecord),
identity: ident,
destination: dest,
storage: storageMgr,
// Node-specific information
maxTransferSize: common.NUM_500, // Default 500KB
nodeEnabled: true, // Enabled by default
nodeTimestamp: nodeTimestamp,
}
// Enable destination features
dest.AcceptsLinks(true)
// Enable ratchets and point to a file for persistence.
// The actual path should probably be configurable.
ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash()
dest.EnableRatchets(ratchetPath)
dest.SetProofStrategy(destination.PROVE_APP)
debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")
// Initialize interfaces from config
for name, ifaceConfig := range cfg.Interfaces {
if !ifaceConfig.Enabled {
continue
}
var iface interfaces.Interface
var err error
switch ifaceConfig.Type {
case common.STR_TCP_CLIENT:
iface, err = interfaces.NewTCPClientInterface(
name,
ifaceConfig.TargetHost,
ifaceConfig.TargetPort,
ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled,
ifaceConfig.Enabled,
)
case "UDPInterface":
iface, err = interfaces.NewUDPInterface(
name,
ifaceConfig.Address,
ifaceConfig.TargetHost,
ifaceConfig.Enabled,
)
case "AutoInterface":
iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
case "WebSocketInterface":
wsURL := ifaceConfig.Address
if wsURL == "" {
wsURL = ifaceConfig.TargetHost
}
debug.Log(debug.DEBUG_INFO, "Creating WebSocket interface", common.STR_NAME, name, "url", wsURL, "enabled", ifaceConfig.Enabled)
iface, err = interfaces.NewWebSocketInterface(name, wsURL, ifaceConfig.Enabled)
if err != nil {
debug.Log(debug.DEBUG_ERROR, "Failed to create WebSocket interface", common.STR_NAME, name, common.STR_ERROR, err)
} else {
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
}
default:
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
continue
}
if err != nil {
if cfg.PanicOnInterfaceErr {
return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
}
debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", common.STR_NAME, name, common.STR_ERROR, err)
continue
}
// Set packet callback
iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", common.STR_NAME, ni.GetName(), "data_len", len(data))
if r.transport != nil {
r.transport.HandlePacket(data, ni)
} else {
debug.Log(debug.DEBUG_CRITICAL, "Transport is nil in packet callback")
}
})
debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type)
r.interfaces = append(r.interfaces, iface)
debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name)
}
return r, nil
}
func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
debug.Log(debug.DEBUG_INFO, "Setting up interface", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("%T", iface))
ch := channel.NewChannel(&transportWrapper{r.transport})
r.channels[iface.GetName()] = ch
rw := buffer.CreateBidirectionalBuffer(
1,
2,
ch,
func(size int) {
data := make([]byte, size)
debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size)
iface.ProcessIncoming(data)
if len(data) > common.ZERO {
debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0]))
r.transport.HandlePacket(data, iface)
}
},
)
r.buffers[iface.GetName()] = &buffer.Buffer{
ReadWriter: rw,
}
}
func (r *Reticulum) monitorInterfaces() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
for _, iface := range r.interfaces {
if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
iface.GetName(),
tcpClient.IsConnected(),
tcpClient.GetTxBytes(),
float64(tcpClient.GetTxBytes()*8)/(5*1024),
tcpClient.GetRxBytes(),
float64(tcpClient.GetRxBytes()*8)/(5*1024),
)
if runtime.GOOS != "windows" {
stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
}
debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats)
}
}
}
}
func main() {
flag.Parse()
debug.Init()
debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel())
cfg, err := config.InitConfig()
if err != nil {
debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err)
os.Exit(1)
}
debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)
r, err := NewReticulum(cfg)
if err != nil {
debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
os.Exit(1)
}
// Start monitoring interfaces
go r.monitorInterfaces()
// Register announce handler
handler := NewAnnounceHandler(r, []string{"*"})
r.transport.RegisterAnnounceHandler(handler)
// Start Reticulum
if err := r.Start(); err != nil {
debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err)
os.Exit(1)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
if err := r.Stop(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err)
}
debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
}
type transportWrapper struct {
*transport.Transport
}
func (tw *transportWrapper) GetRTT() float64 {
return 0.1
}
func (tw *transportWrapper) RTT() float64 {
return tw.GetRTT()
}
func (tw *transportWrapper) GetStatus() byte {
return transport.STATUS_ACTIVE
}
func (tw *transportWrapper) Send(data []byte) interface{} {
p := &packet.Packet{
PacketType: packet.PacketTypeData,
Hops: 0,
Data: data,
HeaderType: packet.HeaderType1,
}
err := tw.Transport.SendPacket(p)
if err != nil {
return nil
}
return p
}
func (tw *transportWrapper) Resend(p interface{}) error {
if pkt, ok := p.(*packet.Packet); ok {
return tw.Transport.SendPacket(pkt)
}
return fmt.Errorf("invalid packet type")
}
func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) {
time.AfterFunc(timeout, func() {
callback(packet)
})
}
func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) {
callback(packet)
}
func (tw *transportWrapper) GetLinkID() []byte {
return nil
}
func (tw *transportWrapper) HandleInbound(pkt *packet.Packet) error {
return nil
}
func (tw *transportWrapper) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
return nil
}
func initializeDirectories() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %v", err)
}
basePath := filepath.Join(homeDir, ".reticulum-go")
dirs := []string{
basePath,
filepath.Join(basePath, common.STR_STORAGE),
filepath.Join(basePath, common.STR_STORAGE, "destinations"),
filepath.Join(basePath, common.STR_STORAGE, "identities"),
filepath.Join(basePath, common.STR_STORAGE, "ratchets"),
filepath.Join(basePath, common.STR_STORAGE, "cache"),
filepath.Join(basePath, common.STR_STORAGE, "cache", "announces"),
filepath.Join(basePath, common.STR_STORAGE, "resources"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301
return fmt.Errorf("failed to create directory %s: %v", dir, err)
}
}
return nil
}
func (r *Reticulum) Start() error {
debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...")
if err := r.transport.Start(); err != nil {
return fmt.Errorf("failed to start transport: %v", err)
}
debug.Log(debug.DEBUG_INFO, "Transport started successfully")
// Start interfaces
for _, iface := range r.interfaces {
debug.Log(debug.DEBUG_ERROR, "Starting interface", "name", iface.GetName())
if err := iface.Start(); err != nil {
if r.config.PanicOnInterfaceErr {
return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
}
debug.Log(debug.DEBUG_CRITICAL, "Error starting interface", "name", iface.GetName(), "error", err)
continue
}
if netIface, ok := iface.(common.NetworkInterface); ok {
// Register interface with transport
if err := r.transport.RegisterInterface(iface.GetName(), netIface); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to register interface with transport", "name", iface.GetName(), "error", err)
} else {
debug.Log(debug.DEBUG_INFO, "Registered interface with transport", "name", iface.GetName())
}
r.handleInterface(netIface)
}
debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName())
}
// Wait for interfaces to initialize
time.Sleep(2 * time.Second)
// Send initial announce
debug.Log(debug.DEBUG_ERROR, "Sending initial announce")
nodeName := "Reticulum-Go Test Node"
r.destination.SetDefaultAppData([]byte(nodeName))
if err := r.destination.Announce(false, nil, nil); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err)
}
// Start periodic announce goroutine
go func() {
// Wait a bit before the first announce
time.Sleep(5 * time.Second)
for {
debug.Log(debug.DEBUG_INFO, "Announcing destination...")
err := r.destination.Announce(false, nil, nil)
if err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err)
}
time.Sleep(60 * time.Second)
}
}()
go r.monitorInterfaces()
debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully")
return nil
}
func (r *Reticulum) Stop() error {
debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...")
for _, buf := range r.buffers {
if err := buf.Close(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err)
}
}
for _, ch := range r.channels {
if err := ch.Close(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err)
}
}
for _, iface := range r.interfaces {
if err := iface.Stop(); err != nil {
debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err)
}
}
if err := r.transport.Close(); err != nil {
return fmt.Errorf("failed to close transport: %v", err)
}
debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully")
return nil
}
type AnnounceHandler struct {
aspectFilter []string
reticulum *Reticulum
}
func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
return &AnnounceHandler{
aspectFilter: aspectFilter,
reticulum: r,
}
}
func (h *AnnounceHandler) AspectFilter() []string {
return h.aspectFilter
}
func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error {
debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops)
debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData), "hops", hops)
var isNode bool
var nodeEnabled bool
var nodeTimestamp int64
var nodeMaxSize int16
// Parse msgpack appData from transport announce format
if len(appData) > common.ZERO {
// appData is msgpack array [name, customData]
if appData[0] == common.HEX_0x92 { // array of 2 elements
// Skip array header and first element (name)
pos := common.ONE
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
nameLen := int(appData[pos+1])
pos += common.TWO + nameLen
if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
dataLen := int(appData[pos+1])
if pos+2+dataLen <= len(appData) {
customData := appData[pos+2 : pos+2+dataLen]
nodeName := string(customData)
debug.Log(debug.DEBUG_INFO, "Parsed node name", "name", nodeName)
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
}
}
}
} else {
// Fallback: treat as raw node name
nodeName := string(appData)
debug.Log(debug.DEBUG_INFO, "Raw node name", "name", nodeName)
debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
}
} else {
debug.Log(debug.DEBUG_INFO, "No appData (empty announce)")
}
// Type assert and log identity details
if identity, ok := id.(*identity.Identity); ok {
debug.Log(debug.DEBUG_ALL, "Identity details")
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", identity.GetHexHash())
debug.Log(debug.DEBUG_ALL, "Identity public key", "key", fmt.Sprintf("%x", identity.GetPublicKey()))
ratchets := identity.GetRatchets()
debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets))
if len(ratchets) > 0 {
ratchetKey := identity.GetCurrentRatchetKey()
if ratchetKey != nil {
ratchetID := identity.GetRatchetID(ratchetKey)
debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID))
}
}
// Create a better record with more info
recordType := "peer"
if isNode {
recordType = "node"
debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize))
}
h.reticulum.announceHistoryMu.Lock()
h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
timestamp: time.Now().Unix(),
appData: appData,
}
h.reticulum.announceHistoryMu.Unlock()
debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash())
}
return nil
}
func (h *AnnounceHandler) ReceivePathResponses() bool {
return true
}
func (r *Reticulum) GetDestination() *destination.Destination {
return r.destination
}
func (r *Reticulum) createNodeAppData() []byte {
// Create a msgpack array with 3 elements
// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
appData := []byte{common.HEX_0x93} // Array with 3 elements
// Element 0: Boolean for enable/disable peer
if r.nodeEnabled {
appData = append(appData, common.HEX_0xC3) // true
} else {
appData = append(appData, common.HEX_0xC2) // false
}
// Element 1: Int32 timestamp (current time)
r.nodeTimestamp = time.Now().Unix()
appData = append(appData, common.HEX_0xD2) // int32 format
timeBytes := make([]byte, common.FOUR)
binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
appData = append(appData, timeBytes...)
// Element 2: Int16 max transfer size in KB
appData = append(appData, common.HEX_0xD1) // int16 format
sizeBytes := make([]byte, common.TWO)
binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
appData = append(appData, sizeBytes...)
debug.Log(debug.DEBUG_ALL, "Created node appData", "enable", r.nodeEnabled, "timestamp", r.nodeTimestamp, "maxsize", r.maxTransferSize, "data", fmt.Sprintf("%x", appData))
return appData
}