This commit is contained in:
Sudo-Ivan
2024-12-30 12:58:43 -06:00
parent 7ef7e60a87
commit 7a7ce84778
14 changed files with 919 additions and 207 deletions

266
cmd/client-ftp/main.go Normal file
View File

@@ -0,0 +1,266 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/link"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
"github.com/Sudo-Ivan/reticulum-go/pkg/resource"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
const (
APP_NAME = "example_utilities"
APP_ASPECT = "filetransfer"
)
var (
configPath = flag.String("config", "", "Path to config file")
servePath = flag.String("serve", "", "Directory to serve files from")
)
type FileServer struct {
config *common.ReticulumConfig
transport *transport.Transport
interfaces []common.NetworkInterface
identity *identity.Identity
servePath string
}
func NewFileServer(cfg *common.ReticulumConfig, servePath string) (*FileServer, error) {
if cfg == nil {
var err error
cfg, err = config.InitConfig()
if err != nil {
return nil, fmt.Errorf("failed to initialize config: %v", err)
}
}
t, err := transport.NewTransport(cfg)
if err != nil {
return nil, fmt.Errorf("failed to initialize transport: %v", err)
}
id, err := identity.New()
if err != nil {
return nil, fmt.Errorf("failed to create identity: %v", err)
}
return &FileServer{
config: cfg,
transport: t,
interfaces: make([]common.NetworkInterface, 0),
identity: id,
servePath: servePath,
}, nil
}
func (s *FileServer) OnLinkEstablished(l *link.Link) {
s.handleLinkEstablished(l)
}
func (s *FileServer) Start() error {
dest, err := destination.New(
s.identity,
destination.OUT,
destination.SINGLE,
APP_NAME,
APP_ASPECT,
)
if err != nil {
return fmt.Errorf("failed to create destination: %v", err)
}
callback := func(l interface{}) {
if link, ok := l.(*link.Link); ok {
s.OnLinkEstablished(link)
}
}
dest.SetLinkEstablishedCallback(callback)
log.Printf("File server started. Server hash: %s", s.identity.Hex())
log.Printf("Serving directory: %s", s.servePath)
return nil
}
func (s *FileServer) handleLinkEstablished(l *link.Link) {
log.Printf("Client connected")
l.SetPacketCallback(func(data []byte, p *packet.Packet) {
s.handlePacket(data, l)
})
l.SetResourceCallback(func(r interface{}) bool {
if res, ok := r.(*resource.Resource); ok {
return s.handleResource(res)
}
return false
})
}
func (s *FileServer) handlePacket(data []byte, l *link.Link) {
if string(data) == "LIST" {
files, err := s.getFileList()
if err != nil {
log.Printf("Error getting file list: %v", err)
l.Teardown()
return
}
if err := l.SendPacket(files); err != nil {
log.Printf("Error sending file list: %v", err)
l.Teardown()
}
}
}
func (s *FileServer) handleResource(r *resource.Resource) bool {
filename := filepath.Join(s.servePath, r.GetName())
file, err := os.Create(filename)
if err != nil {
log.Printf("Failed to create file: %v", err)
return false
}
defer file.Close()
written, err := io.Copy(file, r)
if err != nil {
log.Printf("Failed to write file: %v", err)
return false
}
log.Printf("Received file: %s (%d bytes)", filename, written)
return true
}
func (s *FileServer) getFileList() ([]byte, error) {
files, err := os.ReadDir(s.servePath)
if err != nil {
return nil, err
}
var fileList []string
for _, file := range files {
if !file.IsDir() {
fileList = append(fileList, file.Name())
}
}
return []byte(fmt.Sprintf("%v", fileList)), nil
}
func main() {
flag.Parse()
if *servePath == "" {
log.Fatal("Please specify a directory to serve with -serve")
}
var cfg *common.ReticulumConfig
var err error
if *configPath == "" {
cfg, err = config.InitConfig()
} else {
cfg, err = config.LoadConfig(*configPath)
}
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
server, err := NewFileServer(cfg, *servePath)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
if err := server.Start(); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
// Start watching the directory for changes
go server.watchDirectory()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
}
func (s *FileServer) watchDirectory() {
for {
time.Sleep(1 * time.Second)
files, err := os.ReadDir(s.servePath)
if err != nil {
log.Printf("Error reading directory: %v", err)
continue
}
for _, file := range files {
if !file.IsDir() {
// Try to send file to connected peers
filePath := filepath.Join(s.servePath, file.Name())
if err := s.sendFile(filePath); err != nil {
log.Printf("Error sending file %s: %v", file.Name(), err)
}
}
}
}
}
func (s *FileServer) sendFile(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file: %v", err)
}
defer file.Close()
// Create a destination for the file transfer
dest, err := destination.New(
s.identity,
destination.OUT,
destination.SINGLE,
APP_NAME,
APP_ASPECT,
)
if err != nil {
return fmt.Errorf("failed to create destination: %v", err)
}
// Set up link for file transfer
callback := func(l interface{}) {
if link, ok := l.(*link.Link); ok {
// Create a new resource with auto-compression enabled
res, err := resource.New(file, true)
if err != nil {
log.Printf("Error creating resource: %v", err)
return
}
// The filename is automatically set from the file handle
// in resource.New when using an io.ReadWriteSeeker
// Send the resource through the link
if err := link.SendResource(res); err != nil {
log.Printf("Error sending resource: %v", err)
return
}
log.Printf("File %s sent successfully", filepath.Base(filePath))
}
}
dest.SetLinkEstablishedCallback(callback)
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@@ -64,53 +65,90 @@ func NewClient(cfg *common.ReticulumConfig) (*Client, error) {
} }
func (c *Client) Start() error { func (c *Client) Start() error {
// Initialize interfaces log.Printf("Starting Reticulum client...")
for _, ifaceConfig := range c.config.Interfaces { log.Printf("Configuration: %+v", c.config)
// Initialize transport
t, err := transport.NewTransport(c.config)
if err != nil {
return fmt.Errorf("failed to initialize transport: %v", err)
}
c.transport = t
log.Printf("Transport initialized")
log.Printf("Initializing network interfaces...")
for name, ifaceConfig := range c.config.Interfaces {
if !ifaceConfig.Enabled {
log.Printf("Skipping disabled interface %s", name)
continue
}
log.Printf("Configuring interface %s (%s)", name, ifaceConfig.Type)
var iface common.NetworkInterface var iface common.NetworkInterface
switch ifaceConfig.Type { switch ifaceConfig.Type {
case "TCPClientInterface": case "TCPClientInterface":
log.Printf("Connecting to %s:%d via TCP...", ifaceConfig.TargetHost, ifaceConfig.TargetPort)
client, err := interfaces.NewTCPClient( client, err := interfaces.NewTCPClient(
ifaceConfig.Name, name,
ifaceConfig.TargetHost, ifaceConfig.TargetHost,
ifaceConfig.TargetPort, ifaceConfig.TargetPort,
ifaceConfig.KISSFraming, ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled, ifaceConfig.I2PTunneled,
ifaceConfig.Enabled,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create TCP interface %s: %v", ifaceConfig.Name, err) return fmt.Errorf("failed to create TCP interface %s: %v", name, err)
} }
if err := client.Start(); err != nil {
return fmt.Errorf("failed to start TCP interface %s: %v", name, err)
}
iface = client iface = client
log.Printf("Successfully connected to %s:%d", ifaceConfig.TargetHost, ifaceConfig.TargetPort)
case "UDPInterface": case "UDPInterface":
addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port) addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port)
target := fmt.Sprintf("%s:%d", ifaceConfig.TargetHost, ifaceConfig.TargetPort)
log.Printf("Starting UDP interface on %s...", addr)
udp, err := interfaces.NewUDPInterface( udp, err := interfaces.NewUDPInterface(
ifaceConfig.Name, name,
addr, addr,
"", // No target address for client initially target,
ifaceConfig.Enabled,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create UDP interface %s: %v", ifaceConfig.Name, err) return fmt.Errorf("failed to create UDP interface %s: %v", name, err)
} }
iface = udp
default: if err := udp.Start(); err != nil {
return fmt.Errorf("unsupported interface type: %s", ifaceConfig.Type) return fmt.Errorf("failed to start UDP interface %s: %v", name, err)
}
iface = udp
log.Printf("UDP interface listening on %s", addr)
} }
c.interfaces = append(c.interfaces, iface) if iface != nil {
log.Printf("Created interface %s", iface.GetName()) // Set packet callback
iface.SetPacketCallback(c.transport.HandlePacket)
c.interfaces = append(c.interfaces, iface)
log.Printf("Created and started interface %s (type=%v, enabled=%v)",
name, iface.GetType(), iface.IsEnabled())
}
} }
// Start periodic announces // Register announce handler with explicit type
go func() { var handler transport.AnnounceHandler = &ClientAnnounceHandler{client: c}
for { c.transport.RegisterAnnounceHandler(handler)
c.sendAnnounce()
time.Sleep(30 * time.Second) // Send initial announce
} log.Printf("Sending initial announce...")
}() if err := c.sendAnnounce(); err != nil {
log.Printf("Warning: Failed to send initial announce: %v", err)
}
log.Printf("Client started with %d interfaces", len(c.interfaces))
return nil return nil
} }
@@ -119,17 +157,33 @@ func (c *Client) handlePacket(data []byte, p *packet.Packet) {
return return
} }
packetType := data[0] header := data[0]
packetType := header & 0x03 // Extract packet type from header
switch packetType { switch packetType {
case 0x04: // Announce packet case announce.PACKET_TYPE_ANNOUNCE:
c.handleAnnounce(data[1:]) log.Printf("Received announce packet:")
log.Printf(" Raw data: %x", data)
// Create announce instance
a, err := announce.New(c.identity, []byte("RNS.Go.Client"), false)
if err != nil {
log.Printf("Failed to create announce handler: %v", err)
return
}
// Handle the announce
if err := a.HandleAnnounce(data[1:]); err != nil {
log.Printf("Failed to handle announce: %v", err)
}
default: default:
c.transport.HandlePacket(data, p) c.transport.HandlePacket(data, p)
} }
} }
func (c *Client) handleAnnounce(data []byte) { func (c *Client) handleAnnounce(data []byte) {
if len(data) < 42 { // 32 bytes hash + 8 bytes timestamp + 1 byte hops + 1 byte flags if len(data) < 42 {
log.Printf("Received malformed announce packet (too short)") log.Printf("Received malformed announce packet (too short)")
return return
} }
@@ -144,33 +198,41 @@ func (c *Client) handleAnnounce(data []byte) {
log.Printf(" Hops: %d", hops) log.Printf(" Hops: %d", hops)
log.Printf(" Flags: %x", flags) log.Printf(" Flags: %x", flags)
// Extract public key if present (after flags)
if len(data) > 42 { if len(data) > 42 {
pubKeyLen := 32 // Ed25519 public key length
pubKey := data[42 : 42+pubKeyLen]
log.Printf(" Public Key: %x", pubKey)
// Extract app data if present // Extract app data if present
dataLen := binary.BigEndian.Uint16(data[42:44]) var appData []byte
if len(data) >= 44+int(dataLen) { if len(data) > 42+pubKeyLen+2 {
appData := data[44 : 44+dataLen] dataLen := binary.BigEndian.Uint16(data[42+pubKeyLen : 42+pubKeyLen+2])
log.Printf(" App Data: %s", string(appData)) if len(data) >= 42+pubKeyLen+2+int(dataLen) {
appData = data[42+pubKeyLen+2 : 42+pubKeyLen+2+int(dataLen)]
log.Printf(" App Data: %s", string(appData))
}
} }
// Store the identity for future use with all required parameters
if !identity.ValidateAnnounce(data, destHash, pubKey, data[len(data)-64:], appData) {
log.Printf("Failed to validate announce")
return
}
log.Printf("Successfully validated and stored announce")
} }
} }
func (c *Client) sendAnnounce() { func (c *Client) sendAnnounce() error {
// Create announce packet
identityHash := c.identity.Hash()
announceData := make([]byte, 0) announceData := make([]byte, 0)
// Create header // Add header
header := announce.CreateHeader( header := []byte{0x01, 0x00} // Announce packet type
announce.IFAC_NONE,
announce.HEADER_TYPE_1,
0x00,
announce.PROP_TYPE_BROADCAST,
announce.DEST_TYPE_SINGLE,
announce.PACKET_TYPE_ANNOUNCE,
0x00,
)
announceData = append(announceData, header...) announceData = append(announceData, header...)
// Add destination hash (16 bytes truncated) // Add destination hash
identityHash := c.identity.Hash()
announceData = append(announceData, identityHash...) announceData = append(announceData, identityHash...)
// Add context byte // Add context byte
@@ -197,17 +259,32 @@ func (c *Client) sendAnnounce() {
log.Printf(" Packet Length: %d bytes", len(announceData)) log.Printf(" Packet Length: %d bytes", len(announceData))
log.Printf(" Full Packet: %x", announceData) log.Printf(" Full Packet: %x", announceData)
sentCount := 0
// Send on all interfaces // Send on all interfaces
for _, iface := range c.interfaces { for _, iface := range c.interfaces {
log.Printf("Sending on interface %s (%s):", iface.GetName(), iface.GetType()) log.Printf("Attempting to send on interface %s:", iface.GetName())
log.Printf(" Type: %v", iface.GetType())
log.Printf(" MTU: %d bytes", iface.GetMTU()) log.Printf(" MTU: %d bytes", iface.GetMTU())
log.Printf(" Status: enabled=%v", iface.IsEnabled())
if !iface.IsEnabled() {
log.Printf(" Skipping disabled interface")
continue
}
if err := iface.Send(announceData, ""); err != nil { if err := iface.Send(announceData, ""); err != nil {
log.Printf(" Failed to send: %v", err) log.Printf(" Failed to send: %v", err)
} else { } else {
log.Printf(" Successfully sent announce") log.Printf(" Successfully sent announce")
sentCount++
} }
} }
if sentCount == 0 {
return fmt.Errorf("no interfaces available to send announce")
}
return nil
} }
func (c *Client) Stop() { func (c *Client) Stop() {
@@ -265,22 +342,67 @@ func (c *Client) handleLinkClosed(l *link.Link) {
log.Printf("Link closed") log.Printf("Link closed")
} }
type ClientAnnounceHandler struct {
client *Client
}
func (h *ClientAnnounceHandler) AspectFilter() []string {
return []string{"RNS.Go.Client"}
}
func (h *ClientAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error {
log.Printf("=== Received Announce Details ===")
log.Printf("Destination Hash: %x", destinationHash)
log.Printf("App Data: %s", string(appData))
// Type assert the identity
if id, ok := announcedIdentity.(*identity.Identity); ok {
log.Printf("Identity Public Key: %x", id.GetPublicKey())
// Create packet hash for storage
packetHash := identity.TruncatedHash(append(destinationHash, id.GetPublicKey()...))
log.Printf("Generated Packet Hash: %x", packetHash)
// Store the peer identity with all required parameters
identity.Remember(packetHash, destinationHash, id.GetPublicKey(), appData)
log.Printf("Identity stored successfully")
log.Printf("===========================")
return nil
}
log.Printf("Error: Invalid identity type")
log.Printf("===========================")
return fmt.Errorf("invalid identity type")
}
func (h *ClientAnnounceHandler) ReceivePathResponses() bool {
return true
}
func main() { func main() {
flag.Parse() flag.Parse()
log.Printf("Starting Reticulum Go client...")
log.Printf("Config path: %s", *configPath)
log.Printf("Target hash: %s", *targetHash)
var cfg *common.ReticulumConfig var cfg *common.ReticulumConfig
var err error var err error
if *configPath == "" { if *configPath == "" {
log.Printf("No config path specified, using default configuration")
cfg, err = config.InitConfig() cfg, err = config.InitConfig()
} else { } else {
log.Printf("Loading configuration from: %s", *configPath)
cfg, err = config.LoadConfig(*configPath) cfg, err = config.LoadConfig(*configPath)
} }
if err != nil { if err != nil {
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
} }
log.Printf("Configuration loaded successfully")
if *generateIdentity { if *generateIdentity {
log.Printf("Generating new identity...")
id, err := identity.New() id, err := identity.New()
if err != nil { if err != nil {
log.Fatalf("Failed to generate identity: %v", err) log.Fatalf("Failed to generate identity: %v", err)
@@ -299,29 +421,79 @@ func main() {
log.Fatalf("Failed to start client: %v", err) log.Fatalf("Failed to start client: %v", err)
} }
// Wait for interrupt log.Printf("Client running, press Ctrl+C to exit")
// If target is specified, start interactive mode
if *targetHash != "" {
targetBytes, err := identity.HashFromString(*targetHash)
if err != nil {
log.Fatalf("Invalid target hash: %v", err)
}
link, err := client.transport.GetLink(targetBytes)
if err != nil {
log.Fatalf("Failed to get link: %v", err)
}
log.Printf("Starting interactive mode...")
interactiveLoop(link)
return
}
// Wait for interrupt if no target specified
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan <-sigChan
log.Printf("Received interrupt signal, shutting down...")
} }
func interactiveLoop(link *transport.Link) { func interactiveLoop(link *transport.Link) {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
connected := make(chan struct{})
disconnected := make(chan struct{})
// Set up connection status handlers
link.OnConnected(func() {
connected <- struct{}{}
})
link.OnDisconnected(func() {
disconnected <- struct{}{}
})
// Wait for initial connection
select {
case <-connected:
log.Println("Connected to target")
case <-time.After(10 * time.Second):
log.Fatal("Connection timeout")
return
}
// Start input loop
for { for {
fmt.Print("> ") select {
input, err := reader.ReadString('\n') case <-disconnected:
if err != nil { log.Println("Connection lost")
fmt.Printf("Error reading input: %v\n", err)
continue
}
input = strings.TrimSpace(input)
if input == "quit" || input == "exit" {
return return
} default:
fmt.Print("> ")
input, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
return
}
log.Printf("Error reading input: %v", err)
continue
}
if err := link.Send([]byte(input)); err != nil { input = strings.TrimSpace(input)
fmt.Printf("Failed to send: %v\n", err) if input == "quit" || input == "exit" {
return
}
if err := link.Send([]byte(input)); err != nil {
log.Printf("Failed to send: %v", err)
return
}
} }
} }
} }

View File

@@ -7,15 +7,15 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/internal/config" "github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport" "github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces" "github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
) )
type Reticulum struct { type Reticulum struct {
config *common.ReticulumConfig config *common.ReticulumConfig
transport *transport.Transport transport *transport.Transport
interfaces []interfaces.Interface interfaces []interfaces.Interface
} }
@@ -48,11 +48,18 @@ func (r *Reticulum) Start() error {
ifaceConfig.TargetPort, ifaceConfig.TargetPort,
ifaceConfig.KISSFraming, ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled, ifaceConfig.I2PTunneled,
ifaceConfig.Enabled,
) )
if err != nil { if err != nil {
log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err) log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err)
continue continue
} }
if err := client.Start(); err != nil {
log.Printf("Failed to start TCP interface %s: %v", ifaceConfig.Name, err)
continue
}
iface = client iface = client
case "TCPServerInterface": case "TCPServerInterface":
@@ -68,19 +75,33 @@ func (r *Reticulum) Start() error {
log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err) log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err)
continue continue
} }
if err := server.Start(); err != nil {
log.Printf("Failed to start TCP server interface %s: %v", ifaceConfig.Name, err)
continue
}
iface = server iface = server
case "UDPInterface": case "UDPInterface":
addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port) addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port)
udp, err := interfaces.NewUDPInterface( udp, err := interfaces.NewUDPInterface(
ifaceConfig.Name, ifaceConfig.Name,
addr, addr,
"", // No target address for server initially "", // No target address for server initially
ifaceConfig.Enabled,
) )
if err != nil { if err != nil {
log.Printf("Failed to create UDP interface %s: %v", ifaceConfig.Name, err) log.Printf("Failed to create UDP interface %s: %v", ifaceConfig.Name, err)
continue continue
} }
if err := udp.Start(); err != nil {
log.Printf("Failed to start UDP interface %s: %v", ifaceConfig.Name, err)
continue
}
iface = udp iface = udp
case "AutoInterface": case "AutoInterface":
@@ -96,6 +117,8 @@ func (r *Reticulum) Start() error {
// Set packet callback to transport // Set packet callback to transport
iface.SetPacketCallback(r.transport.HandlePacket) iface.SetPacketCallback(r.transport.HandlePacket)
r.interfaces = append(r.interfaces, iface) r.interfaces = append(r.interfaces, iface)
log.Printf("Created and started interface %s (type=%v, enabled=%v)",
iface.GetName(), iface.GetType(), iface.IsEnabled())
} }
} }
@@ -137,4 +160,4 @@ func main() {
if err := r.Stop(); err != nil { if err := r.Stop(); err != nil {
log.Printf("Error during shutdown: %v", err) log.Printf("Error during shutdown: %v", err)
} }
} }

View File

@@ -9,10 +9,14 @@ loglevel = 4
[interfaces."Local TCP"] [interfaces."Local TCP"]
type = "TCPClientInterface" type = "TCPClientInterface"
enabled = true enabled = true
target_host = "127.0.0.1" target_host = "rns.quad4.io"
target_port = 4242 target_port = 4242
[interfaces."Local UDP"] [interfaces."Local UDP"]
type = "UDPInterface" type = "UDPInterface"
enabled = true enabled = true
interface = "lo" interface = "lo"
address = "127.0.0.1"
port = 37428
target_address = "127.0.0.1"
target_port = 37430

View File

@@ -9,10 +9,14 @@ loglevel = 4
[interfaces."Local TCP"] [interfaces."Local TCP"]
type = "TCPClientInterface" type = "TCPClientInterface"
enabled = true enabled = true
target_host = "127.0.0.1" target_host = "rns.quad4.io"
target_port = 4243 target_port = 4243
[interfaces."Local UDP"] [interfaces."Local UDP"]
type = "UDPInterface" type = "UDPInterface"
enabled = true enabled = true
interface = "lo" interface = "lo"
address = "127.0.0.1"
port = 37430
target_address = "127.0.0.1"
target_port = 37428

View File

@@ -93,7 +93,7 @@ func CreateDefaultConfig(path string) error {
Type: "UDPInterface", Type: "UDPInterface",
Enabled: true, Enabled: true,
Address: "0.0.0.0", Address: "0.0.0.0",
Port: 37428, // Default RNS port Port: 37696, // Default RNS port
} }
data, err := toml.Marshal(cfg) data, err := toml.Marshal(cfg)

View File

@@ -93,16 +93,19 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce,
func (a *Announce) Propagate(interfaces []common.NetworkInterface) error { func (a *Announce) Propagate(interfaces []common.NetworkInterface) error {
packet := a.CreatePacket() packet := a.CreatePacket()
log.Printf("Propagating announce:") // Enhanced logging
log.Printf("Creating announce packet:")
log.Printf(" Destination Hash: %x", a.destinationHash) log.Printf(" Destination Hash: %x", a.destinationHash)
log.Printf(" Public Key: %x", a.identity.GetPublicKey()) log.Printf(" Identity Public Key: %x", a.identity.GetPublicKey())
log.Printf(" App Data: %s", string(a.appData)) log.Printf(" App Data: %s", string(a.appData))
log.Printf(" Packet Size: %d bytes", len(packet)) log.Printf(" Signature: %x", a.signature)
log.Printf(" Full Packet: %x", packet) log.Printf(" Total Packet Size: %d bytes", len(packet))
log.Printf(" Raw Packet: %x", packet)
// Propagate to interfaces // Propagate to interfaces
for _, iface := range interfaces { for _, iface := range interfaces {
log.Printf("Propagating on interface %s (%s):", iface.GetName(), iface.GetType()) log.Printf("Propagating on interface %s:", iface.GetName())
log.Printf(" Interface Type: %s", iface.GetType())
log.Printf(" MTU: %d bytes", iface.GetMTU()) log.Printf(" MTU: %d bytes", iface.GetMTU())
if err := iface.Send(packet, ""); err != nil { if err := iface.Send(packet, ""); err != nil {
@@ -136,18 +139,28 @@ func (a *Announce) HandleAnnounce(data []byte) error {
a.mutex.Lock() a.mutex.Lock()
defer a.mutex.Unlock() defer a.mutex.Unlock()
// Enhanced validation logging
log.Printf("Received announce data (%d bytes):", len(data))
log.Printf(" Raw Data: %x", data)
// Validate announce data // Validate announce data
if len(data) < 16+32+1 { // Min size: hash + pubkey + hops if len(data) < 16+32+1 { // Min size: hash + pubkey + hops
log.Printf(" Error: Invalid announce data length (got %d, need at least %d)",
len(data), 16+32+1)
return errors.New("invalid announce data") return errors.New("invalid announce data")
} }
// Extract fields // Extract and log fields
destHash := data[:16] destHash := data[:16]
publicKey := data[16:48] publicKey := data[16:48]
hopCount := data[48] hopCount := data[48]
// Validate hop count log.Printf(" Destination Hash: %x", destHash)
log.Printf(" Public Key: %x", publicKey)
log.Printf(" Hop Count: %d", hopCount)
if hopCount > MAX_HOPS { if hopCount > MAX_HOPS {
log.Printf(" Error: Exceeded maximum hop count (%d > %d)", hopCount, MAX_HOPS)
return errors.New("announce exceeded maximum hop count") return errors.New("announce exceeded maximum hop count")
} }
@@ -155,27 +168,36 @@ func (a *Announce) HandleAnnounce(data []byte) error {
appData := data[49 : len(data)-64] appData := data[49 : len(data)-64]
signature := data[len(data)-64:] signature := data[len(data)-64:]
log.Printf(" App Data (%d bytes): %s", len(appData), string(appData))
log.Printf(" Signature: %x", signature)
// Create announced identity from public key // Create announced identity from public key
announcedIdentity := identity.FromPublicKey(publicKey) announcedIdentity := identity.FromPublicKey(publicKey)
if announcedIdentity == nil { if announcedIdentity == nil {
log.Printf(" Error: Invalid identity public key")
return errors.New("invalid identity public key") return errors.New("invalid identity public key")
} }
// Verify signature // Verify signature
signData := append(destHash, appData...) signData := append(destHash, appData...)
if !announcedIdentity.Verify(signData, signature) { if !announcedIdentity.Verify(signData, signature) {
log.Printf(" Error: Invalid announce signature")
return errors.New("invalid announce signature") return errors.New("invalid announce signature")
} }
log.Printf(" Signature verification successful")
// Process announce with registered handlers // Process announce with registered handlers
for _, handler := range a.handlers { for _, handler := range a.handlers {
if handler.ReceivePathResponses() || !a.pathResponse { if handler.ReceivePathResponses() || !a.pathResponse {
if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil { if err := handler.ReceivedAnnounce(destHash, announcedIdentity, appData); err != nil {
log.Printf(" Handler error: %v", err)
return err return err
} }
} }
} }
log.Printf(" Successfully processed announce")
return nil return nil
} }

View File

@@ -9,17 +9,18 @@ type ConfigProvider interface {
// InterfaceConfig represents interface configuration // InterfaceConfig represents interface configuration
type InterfaceConfig struct { type InterfaceConfig struct {
Type string `toml:"type"` Name string `toml:"name"`
Name string `toml:"name"` Type string `toml:"type"`
Enabled bool `toml:"enabled"` Enabled bool `toml:"enabled"`
TargetHost string `toml:"target_host,omitempty"` Address string `toml:"address"`
TargetPort int `toml:"target_port,omitempty"` Port int `toml:"port"`
Interface string `toml:"interface,omitempty"` TargetHost string `toml:"target_host"`
Address string `toml:"address,omitempty"` TargetPort int `toml:"target_port"`
Port int `toml:"port,omitempty"` TargetAddress string `toml:"target_address"`
KISSFraming bool `toml:"kiss_framing,omitempty"` Interface string `toml:"interface"`
I2PTunneled bool `toml:"i2p_tunneled,omitempty"` KISSFraming bool `toml:"kiss_framing"`
PreferIPv6 bool `toml:"prefer_ipv6,omitempty"` I2PTunneled bool `toml:"i2p_tunneled"`
PreferIPv6 bool `toml:"prefer_ipv6"`
} }
// ReticulumConfig represents the main configuration structure // ReticulumConfig represents the main configuration structure

View File

@@ -15,6 +15,8 @@ import (
"sync" "sync"
"time" "time"
"encoding/hex"
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
@@ -448,19 +450,19 @@ func (i *Identity) DecryptWithHMAC(data []byte, key []byte) ([]byte, error) {
func (i *Identity) ToFile(path string) error { func (i *Identity) ToFile(path string) error {
data := map[string]interface{}{ data := map[string]interface{}{
"private_key": i.privateKey, "private_key": i.privateKey,
"public_key": i.publicKey, "public_key": i.publicKey,
"signing_key": i.signingKey, "signing_key": i.signingKey,
"verification_key": i.verificationKey, "verification_key": i.verificationKey,
"app_data": i.appData, "app_data": i.appData,
} }
file, err := os.Create(path) file, err := os.Create(path)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
return json.NewEncoder(file).Encode(data) return json.NewEncoder(file).Encode(data)
} }
@@ -470,22 +472,30 @@ func RecallIdentity(path string) (*Identity, error) {
return nil, err return nil, err
} }
defer file.Close() defer file.Close()
var data map[string]interface{} var data map[string]interface{}
if err := json.NewDecoder(file).Decode(&data); err != nil { if err := json.NewDecoder(file).Decode(&data); err != nil {
return nil, err return nil, err
} }
// Reconstruct identity from saved data // Reconstruct identity from saved data
id := &Identity{ id := &Identity{
privateKey: data["private_key"].([]byte), privateKey: data["private_key"].([]byte),
publicKey: data["public_key"].([]byte), publicKey: data["public_key"].([]byte),
signingKey: data["signing_key"].(ed25519.PrivateKey), signingKey: data["signing_key"].(ed25519.PrivateKey),
verificationKey: data["verification_key"].(ed25519.PublicKey), verificationKey: data["verification_key"].(ed25519.PublicKey),
appData: data["app_data"].([]byte), appData: data["app_data"].([]byte),
ratchets: make(map[string][]byte), ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64), ratchetExpiry: make(map[string]int64),
} }
return id, nil return id, nil
} }
func HashFromString(hash string) ([]byte, error) {
if len(hash) != 32 {
return nil, fmt.Errorf("invalid hash length: expected 32, got %d", len(hash))
}
return hex.DecodeString(hash)
}

View File

@@ -21,7 +21,10 @@ type Interface interface {
GetMode() common.InterfaceMode GetMode() common.InterfaceMode
IsOnline() bool IsOnline() bool
IsDetached() bool IsDetached() bool
IsEnabled() bool
Detach() Detach()
Enable()
Disable()
Send(data []byte, addr string) error Send(data []byte, addr string) error
SetPacketCallback(common.PacketCallback) SetPacketCallback(common.PacketCallback)
GetPacketCallback() common.PacketCallback GetPacketCallback() common.PacketCallback
@@ -33,12 +36,25 @@ type BaseInterface struct {
mode common.InterfaceMode mode common.InterfaceMode
ifType common.InterfaceType ifType common.InterfaceType
online bool online bool
enabled bool
detached bool detached bool
mtu int mtu int
mutex sync.RWMutex mutex sync.RWMutex
packetCallback common.PacketCallback packetCallback common.PacketCallback
} }
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
return BaseInterface{
name: name,
mode: common.IF_MODE_FULL,
ifType: ifType,
online: false,
enabled: enabled,
detached: false,
mtu: common.DEFAULT_MTU,
}
}
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) { func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
i.Mutex.Lock() i.Mutex.Lock()
defer i.Mutex.Unlock() defer i.Mutex.Unlock()
@@ -136,5 +152,21 @@ func (i *BaseInterface) GetConn() net.Conn {
} }
func (i *BaseInterface) IsEnabled() bool { func (i *BaseInterface) IsEnabled() bool {
return i.Online && !i.Detached i.mutex.RLock()
defer i.mutex.RUnlock()
return i.enabled && i.online && !i.detached
}
func (i *BaseInterface) Enable() {
i.mutex.Lock()
defer i.mutex.Unlock()
i.enabled = true
i.online = true
}
func (i *BaseInterface) Disable() {
i.mutex.Lock()
defer i.mutex.Unlock()
i.enabled = false
i.online = false
} }

View File

@@ -44,85 +44,56 @@ type TCPClientInterface struct {
packetCallback common.PacketCallback packetCallback common.PacketCallback
mutex sync.RWMutex mutex sync.RWMutex
detached bool detached bool
enabled bool
} }
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) { func NewTCPClient(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
tc := &TCPClientInterface{ tc := &TCPClientInterface{
BaseInterface: BaseInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled),
name: name, targetAddr: targetHost,
mode: common.IF_MODE_FULL, targetPort: targetPort,
ifType: common.IF_TYPE_TCP, kissFraming: kissFraming,
online: false, i2pTunneled: i2pTunneled,
mtu: 1064, initiator: true,
detached: false, enabled: enabled,
},
targetAddr: targetAddr,
targetPort: targetPort,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
initiator: true,
} }
if err := tc.connect(true); err != nil { if enabled {
go tc.reconnect() addr := fmt.Sprintf("%s:%d", targetHost, targetPort)
} else { conn, err := net.Dial("tcp", addr)
go tc.readLoop() if err != nil {
return nil, err
}
tc.conn = conn
tc.online = true
} }
return tc, nil return tc, nil
} }
func (tc *TCPClientInterface) connect(initial bool) error { func (tc *TCPClientInterface) Start() error {
tc.mutex.Lock()
defer tc.mutex.Unlock()
if !tc.enabled {
return fmt.Errorf("interface not enabled")
}
if tc.conn != nil {
tc.online = true
return nil
}
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort) addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
conn, err := net.DialTimeout("tcp", addr, time.Second*INITIAL_TIMEOUT) conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
if initial {
return fmt.Errorf("initial connection failed: %v", err)
}
return err return err
} }
tc.conn = conn tc.conn = conn
tc.Online = true tc.online = true
tc.writing = false
tc.neverConnected = false
// Set TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Second * TCP_PROBE_INTERVAL)
}
return nil return nil
} }
func (tc *TCPClientInterface) reconnect() {
if tc.initiator && !tc.reconnecting {
tc.reconnecting = true
attempts := 0
for !tc.Online {
time.Sleep(time.Second * RECONNECT_WAIT)
attempts++
if tc.maxReconnectTries > 0 && attempts > tc.maxReconnectTries {
tc.teardown()
break
}
if err := tc.connect(false); err != nil {
continue
}
go tc.readLoop()
break
}
tc.reconnecting = false
}
}
func (tc *TCPClientInterface) readLoop() { func (tc *TCPClientInterface) readLoop() {
buffer := make([]byte, tc.MTU) buffer := make([]byte, tc.MTU)
inFrame := false inFrame := false
@@ -281,7 +252,9 @@ func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
} }
func (tc *TCPClientInterface) IsEnabled() bool { func (tc *TCPClientInterface) IsEnabled() bool {
return tc.Online tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.enabled && tc.online && !tc.detached
} }
func (tc *TCPClientInterface) GetName() string { func (tc *TCPClientInterface) GetName() string {
@@ -306,17 +279,68 @@ func (tc *TCPClientInterface) IsOnline() bool {
return tc.online return tc.online
} }
func (tc *TCPClientInterface) reconnect() {
tc.mutex.Lock()
if tc.reconnecting {
tc.mutex.Unlock()
return
}
tc.reconnecting = true
tc.mutex.Unlock()
retries := 0
for retries < tc.maxReconnectTries {
tc.teardown()
addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort)
conn, err := net.Dial("tcp", addr)
if err == nil {
tc.mutex.Lock()
tc.conn = conn
tc.online = true
tc.neverConnected = false
tc.reconnecting = false
tc.mutex.Unlock()
// Restart read loop
go tc.readLoop()
return
}
retries++
// Wait before retrying
select {
case <-time.After(RECONNECT_WAIT * time.Second):
continue
}
}
// Failed to reconnect after max retries
tc.mutex.Lock()
tc.reconnecting = false
tc.mutex.Unlock()
tc.teardown()
}
func (tc *TCPClientInterface) Enable() {
tc.mutex.Lock()
defer tc.mutex.Unlock()
tc.online = true
}
func (tc *TCPClientInterface) Disable() {
tc.mutex.Lock()
defer tc.mutex.Unlock()
tc.online = false
}
type TCPServerInterface struct { type TCPServerInterface struct {
BaseInterface BaseInterface
listener net.Listener
connections map[string]net.Conn connections map[string]net.Conn
mutex sync.RWMutex mutex sync.RWMutex
bindAddr string bindAddr string
bindPort int bindPort int
preferIPv6 bool preferIPv6 bool
spawned bool
port int
host string
kissFraming bool kissFraming bool
i2pTunneled bool i2pTunneled bool
packetCallback common.PacketCallback packetCallback common.PacketCallback
@@ -387,3 +411,15 @@ func (ts *TCPServerInterface) IsOnline() bool {
defer ts.mutex.RUnlock() defer ts.mutex.RUnlock()
return ts.online return ts.online
} }
func (ts *TCPServerInterface) Enable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.online = true
}
func (ts *TCPServerInterface) Disable() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.online = false
}

View File

@@ -2,6 +2,7 @@ package interfaces
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"sync" "sync"
@@ -19,9 +20,10 @@ type UDPInterface struct {
rxBytes uint64 rxBytes uint64
mtu int mtu int
bitrate int bitrate int
enabled bool
} }
func NewUDPInterface(name string, addr string, target string) (*UDPInterface, error) { func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr) udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -36,17 +38,12 @@ func NewUDPInterface(name string, addr string, target string) (*UDPInterface, er
} }
ui := &UDPInterface{ ui := &UDPInterface{
BaseInterface: BaseInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
name: name, addr: udpAddr,
mode: common.IF_MODE_FULL, targetAddr: targetAddr,
ifType: common.IF_TYPE_UDP, readBuffer: make([]byte, common.DEFAULT_MTU),
online: false, mtu: common.DEFAULT_MTU,
mtu: common.DEFAULT_MTU, enabled: enabled,
detached: false,
},
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.DEFAULT_MTU),
} }
return ui, nil return ui, nil
@@ -86,29 +83,16 @@ func (ui *UDPInterface) Detach() {
} }
func (ui *UDPInterface) Send(data []byte, addr string) error { func (ui *UDPInterface) Send(data []byte, addr string) error {
if !ui.IsOnline() { if !ui.IsEnabled() {
return fmt.Errorf("interface offline") return fmt.Errorf("interface not enabled")
} }
targetAddr := ui.targetAddr if ui.targetAddr == nil {
if addr != "" {
var err error
targetAddr, err = net.ResolveUDPAddr("udp", addr)
if err != nil {
return fmt.Errorf("invalid target address: %v", err)
}
}
if targetAddr == nil {
return fmt.Errorf("no target address configured") return fmt.Errorf("no target address configured")
} }
_, err := ui.conn.WriteToUDP(data, targetAddr) _, err := ui.conn.WriteTo(data, ui.targetAddr)
if err != nil { return err
return fmt.Errorf("UDP write failed: %v", err)
}
return nil
} }
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) { func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
@@ -173,3 +157,58 @@ func (ui *UDPInterface) GetMTU() int {
func (ui *UDPInterface) GetBitrate() int { func (ui *UDPInterface) GetBitrate() int {
return ui.bitrate return ui.bitrate
} }
func (ui *UDPInterface) Enable() {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.online = true
}
func (ui *UDPInterface) Disable() {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.online = false
}
func (ui *UDPInterface) Start() error {
conn, err := net.ListenUDP("udp", ui.addr)
if err != nil {
return err
}
ui.conn = conn
ui.online = true
return nil
}
func (ui *UDPInterface) readLoop() {
buffer := make([]byte, ui.mtu)
for {
if ui.IsDetached() {
return
}
n, addr, err := ui.conn.ReadFromUDP(buffer)
if err != nil {
if !ui.IsDetached() {
log.Printf("UDP read error: %v", err)
}
return
}
ui.mutex.Lock()
ui.rxBytes += uint64(n)
ui.mutex.Unlock()
log.Printf("Received %d bytes from %s", n, addr.String())
if callback := ui.GetPacketCallback(); callback != nil {
callback(buffer[:n], ui)
}
}
}
func (ui *UDPInterface) IsEnabled() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.enabled && ui.online && !ui.detached
}

View File

@@ -54,6 +54,7 @@ type Transport struct {
mutex sync.RWMutex mutex sync.RWMutex
handlerLock sync.RWMutex handlerLock sync.RWMutex
pathLock sync.RWMutex pathLock sync.RWMutex
links map[string]*Link
} }
func NewTransport(config *common.ReticulumConfig) (*Transport, error) { func NewTransport(config *common.ReticulumConfig) (*Transport, error) {
@@ -61,6 +62,7 @@ func NewTransport(config *common.ReticulumConfig) (*Transport, error) {
config: config, config: config,
interfaces: make(map[string]common.NetworkInterface), interfaces: make(map[string]common.NetworkInterface),
paths: make(map[string]*common.Path), paths: make(map[string]*common.Path),
links: make(map[string]*Link),
} }
transportMutex.Lock() transportMutex.Lock()
@@ -126,6 +128,8 @@ type Link struct {
packetCb func([]byte) packetCb func([]byte)
resourceCb func(interface{}) bool resourceCb func(interface{}) bool
resourceStrategy int resourceStrategy int
connectedCb func()
disconnectedCb func()
} }
type Destination struct { type Destination struct {
@@ -183,6 +187,9 @@ func (l *Link) SetResourceCallback(cb func(interface{}) bool) {
} }
func (l *Link) Teardown() { func (l *Link) Teardown() {
if l.disconnectedCb != nil {
l.disconnectedCb()
}
if l.closedCb != nil { if l.closedCb != nil {
l.closedCb() l.closedCb()
} }
@@ -206,7 +213,9 @@ func (l *Link) Send(data []byte) error {
} }
type AnnounceHandler interface { type AnnounceHandler interface {
ReceivedAnnounce(destinationHash []byte, identity interface{}, appData []byte) AspectFilter() []string
ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error
ReceivePathResponses() bool
} }
func (t *Transport) RegisterAnnounceHandler(handler AnnounceHandler) { func (t *Transport) RegisterAnnounceHandler(handler AnnounceHandler) {
@@ -630,3 +639,33 @@ func (t *Transport) SendPacket(p *packet.Packet) error {
return nil return nil
} }
func (t *Transport) GetLink(destHash []byte) (*Link, error) {
t.mutex.RLock()
defer t.mutex.RUnlock()
link, exists := t.links[string(destHash)]
if !exists {
// Create new link if it doesn't exist
link = NewLink(
destHash,
nil, // established callback
nil, // closed callback
)
t.links[string(destHash)] = link
}
return link, nil
}
func (l *Link) OnConnected(cb func()) {
l.connectedCb = cb
// If already established, trigger callback immediately
if !l.establishedAt.IsZero() && cb != nil {
cb()
}
}
func (l *Link) OnDisconnected(cb func()) {
l.disconnectedCb = cb
}

View File

@@ -1,10 +1,47 @@
#!/bin/bash #!/bin/bash
# Build the client and server # Function to show usage
echo "Building Reticulum client..." show_usage() {
go build -o bin/reticulum-client ./cmd/client echo "Usage: $0 [--type TYPE]"
echo " --type Type of client to run (default: client, options: client, ftp)"
exit 1
}
# Parse command line arguments
CLIENT_TYPE="client"
while [[ $# -gt 0 ]]; do
case $1 in
--type)
CLIENT_TYPE="$2"
shift 2
;;
*)
show_usage
;;
esac
done
# Validate client type
if [[ "$CLIENT_TYPE" != "client" && "$CLIENT_TYPE" != "ftp" ]]; then
echo "Error: Invalid client type. Must be 'client' or 'ftp'"
show_usage
fi
# Build the appropriate binaries
echo "Building Reticulum binaries..."
go build -o bin/reticulum ./cmd/reticulum go build -o bin/reticulum ./cmd/reticulum
case $CLIENT_TYPE in
"client")
go build -o bin/reticulum-client ./cmd/client
CLIENT_BIN="reticulum-client"
;;
"ftp")
go build -o bin/reticulum-client-ftp ./cmd/client-ftp
CLIENT_BIN="reticulum-client-ftp"
;;
esac
# Check if build was successful # Check if build was successful
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Build failed!" echo "Build failed!"
@@ -23,8 +60,8 @@ sleep 2 # Give server time to start
# Generate identities for both clients # Generate identities for both clients
echo "Generating identities..." echo "Generating identities..."
CLIENT1_HASH=$(./bin/reticulum-client -config configs/test-client1.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3) CLIENT1_HASH=$(./bin/"$CLIENT_BIN" -config configs/test-client1.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3)
CLIENT2_HASH=$(./bin/reticulum-client -config configs/test-client2.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3) CLIENT2_HASH=$(./bin/"$CLIENT_BIN" -config configs/test-client2.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3)
echo "Client 1 Hash: $CLIENT1_HASH" echo "Client 1 Hash: $CLIENT1_HASH"
echo "Client 2 Hash: $CLIENT2_HASH" echo "Client 2 Hash: $CLIENT2_HASH"
@@ -34,15 +71,35 @@ run_client() {
local config=$1 local config=$1
local target=$2 local target=$2
local logfile=$3 local logfile=$3
echo "Starting client with config: $config targeting: $target"
./bin/reticulum-client -config "$config" -target "$target" > "$logfile" 2>&1 & case $CLIENT_TYPE in
"client")
echo "Starting regular client with config: $config targeting: $target"
./bin/"$CLIENT_BIN" -config "$config" -target "$target" > "$logfile" 2>&1 &
;;
"ftp")
echo "Starting FTP client with config: $config serving directory: $target"
./bin/"$CLIENT_BIN" -config "$config" -serve "$target" > "$logfile" 2>&1 &
;;
esac
echo $! > "$logfile.pid" echo $! > "$logfile.pid"
echo "Client started with PID: $(cat $logfile.pid)" echo "Client started with PID: $(cat $logfile.pid)"
} }
# Run both clients targeting each other # Run both clients with appropriate parameters
run_client "configs/test-client1.toml" "$CLIENT2_HASH" "logs/client1.log" case $CLIENT_TYPE in
run_client "configs/test-client2.toml" "$CLIENT1_HASH" "logs/client2.log" "client")
run_client "configs/test-client1.toml" "$CLIENT2_HASH" "logs/client1.log"
run_client "configs/test-client2.toml" "$CLIENT1_HASH" "logs/client2.log"
;;
"ftp")
# Create shared directories for FTP clients
mkdir -p ./shared/client1 ./shared/client2
run_client "configs/test-client1.toml" "./shared/client1" "logs/client1.log" "$CLIENT2_HASH"
run_client "configs/test-client2.toml" "./shared/client2" "logs/client2.log" "$CLIENT1_HASH"
;;
esac
echo echo
echo "Both clients are running. To stop everything:" echo "Both clients are running. To stop everything:"
@@ -50,4 +107,11 @@ echo "kill \$(cat logs/*.pid)"
echo echo
echo "To view logs:" echo "To view logs:"
echo "tail -f logs/client1.log" echo "tail -f logs/client1.log"
echo "tail -f logs/client2.log" echo "tail -f logs/client2.log"
if [ "$CLIENT_TYPE" = "ftp" ]; then
echo
echo "FTP shared directories:"
echo "./shared/client1"
echo "./shared/client2"
fi