0.3.1
This commit is contained in:
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Reticulum-Go
|
# Reticulum-Go
|
||||||
|
|
||||||
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go
|
[Reticulum Network](https://github.com/markqvist/Reticulum) implementation in Go.
|
||||||
|
|
||||||
Aiming for full spec compatibility with the Python version 0.8.8+.
|
Aiming for full spec compatibility with the Python version 0.8.8+.
|
||||||
|
|
||||||
@@ -8,9 +8,7 @@ Aiming for full spec compatibility with the Python version 0.8.8+.
|
|||||||
|
|
||||||
Packages:
|
Packages:
|
||||||
|
|
||||||
- `github.com/pelletier/go-toml`
|
|
||||||
- `golang.org/x/crypto`
|
- `golang.org/x/crypto`
|
||||||
- `gopkg.in/yaml.v3`
|
|
||||||
|
|
||||||
## To-Do List
|
## To-Do List
|
||||||
|
|
||||||
@@ -108,7 +106,9 @@ Packages:
|
|||||||
- [x] Cleanup routines
|
- [x] Cleanup routines
|
||||||
|
|
||||||
### Compatibility
|
### Compatibility
|
||||||
- [ ] RNS Utilities Compatibility
|
- [ ] RNS Utilities.
|
||||||
|
- [ ] Reticulum config.
|
||||||
|
|
||||||
|
|
||||||
### Testing & Validation
|
### Testing & Validation
|
||||||
- [ ] Unit tests for all components (Link, Resource, Destination, Identity, Packet, Transport, Interface)
|
- [ ] Unit tests for all components (Link, Resource, Destination, Identity, Packet, Transport, Interface)
|
||||||
@@ -124,7 +124,7 @@ Packages:
|
|||||||
### Cleanup
|
### Cleanup
|
||||||
- [ ] Seperate Cryptography from identity.go to their own files.
|
- [ ] Seperate Cryptography from identity.go to their own files.
|
||||||
- [ ] Move constants to their own files.
|
- [ ] Move constants to their own files.
|
||||||
- [ ] Remove TOML stuff, use reticulum config.
|
- [ ] Remove default community interfaces in default config creation after testing.
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
- [ ] Rate limiting
|
- [ ] Rate limiting
|
||||||
|
|||||||
30
SECURITY.md
Normal file
30
SECURITY.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
I use [Socket](https://socket.dev/) and [Deepsource](https://deepsource.com/) for this project.
|
||||||
|
|
||||||
|
## Cryptography Dependencies
|
||||||
|
|
||||||
|
- golang.org/x/crypto for core cryptographic primitives
|
||||||
|
- hkdf
|
||||||
|
- curve25519
|
||||||
|
|
||||||
|
- go/crypto
|
||||||
|
- ed25519
|
||||||
|
- sha256
|
||||||
|
- rand
|
||||||
|
- aes
|
||||||
|
- cipher
|
||||||
|
- hmac
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report any security vulnerabilities to [rns@quad4.io](mailto:rns@quad4.io)
|
||||||
|
|
||||||
|
**PGP Key:**
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
xjMEZ3RaxBYJKwYBBAHaRw8BAQdAcW8OFXyQ6KuqoTWKVbULYgakD
|
||||||
|
|
||||||
|
|
||||||
2
go.mod
2
go.mod
@@ -3,7 +3,5 @@ module github.com/Sudo-Ivan/reticulum-go
|
|||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/pelletier/go-toml v1.9.5
|
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||||
"github.com/pelletier/go-toml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -44,35 +47,168 @@ func EnsureConfigDir() error {
|
|||||||
return os.MkdirAll(configDir, 0755)
|
return os.MkdirAll(configDir, 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseValue parses string values into appropriate types
|
||||||
|
func parseValue(value string) interface{} {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
// Try bool
|
||||||
|
if value == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if value == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try int
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads the configuration from the specified path
|
// LoadConfig loads the configuration from the specified path
|
||||||
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
func LoadConfig(path string) (*common.ReticulumConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
cfg.ConfigPath = path
|
||||||
return nil, err
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentInterface *common.InterfaceConfig
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
name := strings.Trim(line, "[]")
|
||||||
|
currentInterface = &common.InterfaceConfig{Name: name}
|
||||||
|
cfg.Interfaces[name] = currentInterface
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if currentInterface != nil {
|
||||||
|
// Parse interface config
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
currentInterface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
currentInterface.Enabled = value == "true"
|
||||||
|
case "address":
|
||||||
|
currentInterface.Address = value
|
||||||
|
case "port":
|
||||||
|
currentInterface.Port, _ = strconv.Atoi(value)
|
||||||
|
case "target_host":
|
||||||
|
currentInterface.TargetHost = value
|
||||||
|
case "target_port":
|
||||||
|
currentInterface.TargetPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_port":
|
||||||
|
currentInterface.DiscoveryPort, _ = strconv.Atoi(value)
|
||||||
|
case "data_port":
|
||||||
|
currentInterface.DataPort, _ = strconv.Atoi(value)
|
||||||
|
case "discovery_scope":
|
||||||
|
currentInterface.DiscoveryScope = value
|
||||||
|
case "group_id":
|
||||||
|
currentInterface.GroupID = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse global config
|
||||||
|
switch key {
|
||||||
|
case "enable_transport":
|
||||||
|
cfg.EnableTransport = value == "true"
|
||||||
|
case "share_instance":
|
||||||
|
cfg.ShareInstance = value == "true"
|
||||||
|
case "shared_instance_port":
|
||||||
|
cfg.SharedInstancePort, _ = strconv.Atoi(value)
|
||||||
|
case "instance_control_port":
|
||||||
|
cfg.InstanceControlPort, _ = strconv.Atoi(value)
|
||||||
|
case "panic_on_interface_error":
|
||||||
|
cfg.PanicOnInterfaceErr = value == "true"
|
||||||
|
case "loglevel":
|
||||||
|
cfg.LogLevel, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ConfigPath = path
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveConfig saves the configuration to the specified path
|
// SaveConfig saves the configuration to the specified path
|
||||||
func SaveConfig(cfg *common.ReticulumConfig) error {
|
func SaveConfig(cfg *common.ReticulumConfig) error {
|
||||||
data, err := toml.Marshal(cfg)
|
if cfg.ConfigPath == "" {
|
||||||
if err != nil {
|
return fmt.Errorf("config path not set")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(cfg.ConfigPath, data, 0644)
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write global config
|
||||||
|
builder.WriteString("# Reticulum Configuration\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("enable_transport = %v\n", cfg.EnableTransport))
|
||||||
|
builder.WriteString(fmt.Sprintf("share_instance = %v\n", cfg.ShareInstance))
|
||||||
|
builder.WriteString(fmt.Sprintf("shared_instance_port = %d\n", cfg.SharedInstancePort))
|
||||||
|
builder.WriteString(fmt.Sprintf("instance_control_port = %d\n", cfg.InstanceControlPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("panic_on_interface_error = %v\n", cfg.PanicOnInterfaceErr))
|
||||||
|
builder.WriteString(fmt.Sprintf("loglevel = %d\n\n", cfg.LogLevel))
|
||||||
|
|
||||||
|
// Write interface configs
|
||||||
|
for name, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[%s]\n", name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
|
||||||
|
if iface.Address != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("address = %s\n", iface.Address))
|
||||||
|
}
|
||||||
|
if iface.Port != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("port = %d\n", iface.Port))
|
||||||
|
}
|
||||||
|
if iface.TargetHost != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_host = %s\n", iface.TargetHost))
|
||||||
|
}
|
||||||
|
if iface.TargetPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("target_port = %d\n", iface.TargetPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_port = %d\n", iface.DiscoveryPort))
|
||||||
|
}
|
||||||
|
if iface.DataPort != 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("data_port = %d\n", iface.DataPort))
|
||||||
|
}
|
||||||
|
if iface.DiscoveryScope != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("discovery_scope = %s\n", iface.DiscoveryScope))
|
||||||
|
}
|
||||||
|
if iface.GroupID != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("group_id = %s\n", iface.GroupID))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(cfg.ConfigPath, []byte(builder.String()), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDefaultConfig creates a default configuration file
|
// CreateDefaultConfig creates a default configuration file
|
||||||
func CreateDefaultConfig(path string) error {
|
func CreateDefaultConfig(path string) error {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
cfg.ConfigPath = path
|
||||||
|
|
||||||
// Add Auto Interface
|
// Add Auto Interface
|
||||||
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{
|
||||||
@@ -84,7 +220,7 @@ func CreateDefaultConfig(path string) error {
|
|||||||
DataPort: 42671,
|
DataPort: 42671,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add RNS Amsterdam Testnet interface
|
// Add default interfaces
|
||||||
cfg.Interfaces["RNS Testnet Amsterdam"] = &common.InterfaceConfig{
|
cfg.Interfaces["RNS Testnet Amsterdam"] = &common.InterfaceConfig{
|
||||||
Type: "TCPClientInterface",
|
Type: "TCPClientInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -92,7 +228,6 @@ func CreateDefaultConfig(path string) error {
|
|||||||
TargetPort: 4965,
|
TargetPort: 4965,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add RNS BetweenTheBorders Testnet interface
|
|
||||||
cfg.Interfaces["RNS Testnet BetweenTheBorders"] = &common.InterfaceConfig{
|
cfg.Interfaces["RNS Testnet BetweenTheBorders"] = &common.InterfaceConfig{
|
||||||
Type: "TCPClientInterface",
|
Type: "TCPClientInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -100,7 +235,6 @@ func CreateDefaultConfig(path string) error {
|
|||||||
TargetPort: 4242,
|
TargetPort: 4242,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add local UDP interface
|
|
||||||
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{
|
||||||
Type: "UDPInterface",
|
Type: "UDPInterface",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -108,16 +242,11 @@ func CreateDefaultConfig(path string) error {
|
|||||||
Port: 37696,
|
Port: 37696,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := toml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0644)
|
return SaveConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConfig initializes the configuration system
|
// InitConfig initializes the configuration system
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
DEFAULT_SHARED_INSTANCE_PORT = 37428
|
||||||
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
DEFAULT_INSTANCE_CONTROL_PORT = 37429
|
||||||
DEFAULT_LOG_LEVEL = 20
|
DEFAULT_LOG_LEVEL = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigProvider interface for accessing configuration
|
// ConfigProvider interface for accessing configuration
|
||||||
type ConfigProvider interface {
|
type ConfigProvider interface {
|
||||||
GetConfigPath() string
|
GetConfigPath() string
|
||||||
GetLogLevel() int
|
GetLogLevel() int
|
||||||
GetInterfaces() map[string]InterfaceConfig
|
GetInterfaces() map[string]InterfaceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceConfig represents interface configuration
|
// InterfaceConfig represents interface configuration
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Name string `toml:"name"`
|
Name string
|
||||||
Type string `toml:"type"`
|
Type string
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool
|
||||||
Address string `toml:"address"`
|
Address string
|
||||||
Port int `toml:"port"`
|
Port int
|
||||||
TargetHost string `toml:"target_host"`
|
TargetHost string
|
||||||
TargetPort int `toml:"target_port"`
|
TargetPort int
|
||||||
TargetAddress string `toml:"target_address"`
|
TargetAddress string
|
||||||
Interface string `toml:"interface"`
|
Interface string
|
||||||
KISSFraming bool `toml:"kiss_framing"`
|
KISSFraming bool
|
||||||
I2PTunneled bool `toml:"i2p_tunneled"`
|
I2PTunneled bool
|
||||||
PreferIPv6 bool `toml:"prefer_ipv6"`
|
PreferIPv6 bool
|
||||||
MaxReconnTries int `toml:"max_reconnect_tries"`
|
MaxReconnTries int
|
||||||
Bitrate int64 `toml:"bitrate"`
|
Bitrate int64
|
||||||
MTU int `toml:"mtu"`
|
MTU int
|
||||||
GroupID string
|
GroupID string
|
||||||
DiscoveryScope string
|
DiscoveryScope string
|
||||||
DiscoveryPort int
|
DiscoveryPort int
|
||||||
DataPort int
|
DataPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReticulumConfig represents the main configuration structure
|
// ReticulumConfig represents the main configuration structure
|
||||||
type ReticulumConfig struct {
|
type ReticulumConfig struct {
|
||||||
ConfigPath string `toml:"-"`
|
ConfigPath string
|
||||||
EnableTransport bool `toml:"enable_transport"`
|
EnableTransport bool
|
||||||
ShareInstance bool `toml:"share_instance"`
|
ShareInstance bool
|
||||||
SharedInstancePort int `toml:"shared_instance_port"`
|
SharedInstancePort int
|
||||||
InstanceControlPort int `toml:"instance_control_port"`
|
InstanceControlPort int
|
||||||
PanicOnInterfaceErr bool `toml:"panic_on_interface_error"`
|
PanicOnInterfaceErr bool
|
||||||
LogLevel int `toml:"loglevel"`
|
LogLevel int
|
||||||
Interfaces map[string]*InterfaceConfig `toml:"interfaces"`
|
Interfaces map[string]*InterfaceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReticulumConfig creates a new ReticulumConfig with default values
|
// NewReticulumConfig creates a new ReticulumConfig with default values
|
||||||
func NewReticulumConfig() *ReticulumConfig {
|
func NewReticulumConfig() *ReticulumConfig {
|
||||||
return &ReticulumConfig{
|
return &ReticulumConfig{
|
||||||
EnableTransport: true,
|
EnableTransport: true,
|
||||||
ShareInstance: false,
|
ShareInstance: false,
|
||||||
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
SharedInstancePort: DEFAULT_SHARED_INSTANCE_PORT,
|
||||||
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
InstanceControlPort: DEFAULT_INSTANCE_CONTROL_PORT,
|
||||||
PanicOnInterfaceErr: false,
|
PanicOnInterfaceErr: false,
|
||||||
LogLevel: DEFAULT_LOG_LEVEL,
|
LogLevel: DEFAULT_LOG_LEVEL,
|
||||||
Interfaces: make(map[string]*InterfaceConfig),
|
Interfaces: make(map[string]*InterfaceConfig),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the configuration is valid
|
// Validate checks if the configuration is valid
|
||||||
func (c *ReticulumConfig) Validate() error {
|
func (c *ReticulumConfig) Validate() error {
|
||||||
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
if c.SharedInstancePort < 1 || c.SharedInstancePort > 65535 {
|
||||||
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
return fmt.Errorf("invalid shared instance port: %d", c.SharedInstancePort)
|
||||||
}
|
}
|
||||||
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
if c.InstanceControlPort < 1 || c.InstanceControlPort > 65535 {
|
||||||
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
return fmt.Errorf("invalid instance control port: %d", c.InstanceControlPort)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1,49 +1,270 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"bufio"
|
||||||
"gopkg.in/yaml.v3"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Identity struct {
|
Identity struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
StoragePath string `yaml:"storage_path"`
|
StoragePath string
|
||||||
} `yaml:"identity"`
|
}
|
||||||
|
|
||||||
Interfaces []struct {
|
Interfaces []struct {
|
||||||
Name string `yaml:"name"`
|
Name string
|
||||||
Type string `yaml:"type"`
|
Type string
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool
|
||||||
ListenPort int `yaml:"listen_port"`
|
ListenPort int
|
||||||
ListenIP string `yaml:"listen_ip"`
|
ListenIP string
|
||||||
KissFraming bool `yaml:"kiss_framing"`
|
KissFraming bool
|
||||||
I2PTunneled bool `yaml:"i2p_tunneled"`
|
I2PTunneled bool
|
||||||
} `yaml:"interfaces"`
|
}
|
||||||
|
|
||||||
Transport struct {
|
Transport struct {
|
||||||
AnnounceInterval int `yaml:"announce_interval"`
|
AnnounceInterval int
|
||||||
PathRequestTimeout int `yaml:"path_request_timeout"`
|
PathRequestTimeout int
|
||||||
MaxHops int `yaml:"max_hops"`
|
MaxHops int
|
||||||
BitrateLimit int64 `yaml:"bitrate_limit"`
|
BitrateLimit int64
|
||||||
} `yaml:"transport"`
|
}
|
||||||
|
|
||||||
Logging struct {
|
Logging struct {
|
||||||
Level string `yaml:"level"`
|
Level string
|
||||||
File string `yaml:"file"`
|
File string
|
||||||
} `yaml:"logging"`
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
data, err := ioutil.ReadFile(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
var cfg Config
|
cfg := &Config{}
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
scanner := bufio.NewScanner(file)
|
||||||
return nil, err
|
var currentSection string
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sections
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
currentSection = strings.Trim(line, "[]")
|
||||||
|
|
||||||
|
// If this is an interface section, append new interface
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") {
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key-value pairs
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
switch currentSection {
|
||||||
|
case "identity":
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
cfg.Identity.Name = value
|
||||||
|
case "storage_path":
|
||||||
|
cfg.Identity.StoragePath = value
|
||||||
|
}
|
||||||
|
|
||||||
|
case "transport":
|
||||||
|
switch key {
|
||||||
|
case "announce_interval":
|
||||||
|
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
|
||||||
|
case "path_request_timeout":
|
||||||
|
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
|
||||||
|
case "max_hops":
|
||||||
|
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
|
||||||
|
case "bitrate_limit":
|
||||||
|
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "logging":
|
||||||
|
switch key {
|
||||||
|
case "level":
|
||||||
|
cfg.Logging.Level = value
|
||||||
|
case "file":
|
||||||
|
cfg.Logging.File = value
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle interface sections
|
||||||
|
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
|
||||||
|
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
iface.Name = value
|
||||||
|
case "type":
|
||||||
|
iface.Type = value
|
||||||
|
case "enabled":
|
||||||
|
iface.Enabled = value == "true"
|
||||||
|
case "listen_port":
|
||||||
|
iface.ListenPort, _ = strconv.Atoi(value)
|
||||||
|
case "listen_ip":
|
||||||
|
iface.ListenIP = value
|
||||||
|
case "kiss_framing":
|
||||||
|
iface.KissFraming = value == "true"
|
||||||
|
case "i2p_tunneled":
|
||||||
|
iface.I2PTunneled = value == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveConfig(cfg *Config, path string) error {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Write Identity section
|
||||||
|
builder.WriteString("[identity]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
|
||||||
|
|
||||||
|
// Write Transport section
|
||||||
|
builder.WriteString("[transport]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
|
||||||
|
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
|
||||||
|
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
|
||||||
|
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
|
||||||
|
|
||||||
|
// Write Logging section
|
||||||
|
builder.WriteString("[logging]\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
|
||||||
|
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
|
||||||
|
|
||||||
|
// Write Interface sections
|
||||||
|
for _, iface := range cfg.Interfaces {
|
||||||
|
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
|
||||||
|
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
|
||||||
|
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
|
||||||
|
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
|
||||||
|
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, []byte(builder.String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigDir() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to current directory if home directory cannot be determined
|
||||||
|
return ".reticulum-go"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".reticulum-go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfigPath() string {
|
||||||
|
return filepath.Join(GetConfigDir(), "config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureConfigDir() error {
|
||||||
|
configDir := GetConfigDir()
|
||||||
|
return os.MkdirAll(configDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitConfig() (*Config, error) {
|
||||||
|
// Ensure config directory exists
|
||||||
|
if err := EnsureConfigDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := GetDefaultConfigPath()
|
||||||
|
|
||||||
|
// Check if config file exists
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
// Create default config
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
cfg.Identity.Name = "reticulum-node"
|
||||||
|
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
|
||||||
|
|
||||||
|
cfg.Transport.AnnounceInterval = 300
|
||||||
|
cfg.Transport.PathRequestTimeout = 15
|
||||||
|
cfg.Transport.MaxHops = 8
|
||||||
|
cfg.Transport.BitrateLimit = 1000000
|
||||||
|
|
||||||
|
cfg.Logging.Level = "info"
|
||||||
|
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
|
||||||
|
|
||||||
|
// Add default interfaces
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Local UDP",
|
||||||
|
Type: "UDPInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 37697,
|
||||||
|
ListenIP: "0.0.0.0",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg.Interfaces = append(cfg.Interfaces, struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Enabled bool
|
||||||
|
ListenPort int
|
||||||
|
ListenIP string
|
||||||
|
KissFraming bool
|
||||||
|
I2PTunneled bool
|
||||||
|
}{
|
||||||
|
Name: "Auto Discovery",
|
||||||
|
Type: "AutoInterface",
|
||||||
|
Enabled: true,
|
||||||
|
ListenPort: 29717,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save default config
|
||||||
|
if err := SaveConfig(cfg, configPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save default config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,8 @@ func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
|||||||
if key == nil {
|
if key == nil {
|
||||||
return nil, errors.New("no ratchet key available")
|
return nil, errors.New("no ratchet key available")
|
||||||
}
|
}
|
||||||
return d.identity.EncryptSymmetric(plaintext, key)
|
// CBC encryption with HMAC for group messages
|
||||||
|
return d.identity.EncryptWithHMAC(plaintext, key)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unsupported destination type for encryption")
|
return nil, errors.New("unsupported destination type for encryption")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,51 +57,6 @@ var (
|
|||||||
ratchetPersistLock sync.Mutex
|
ratchetPersistLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func encryptAESGCM(key, plaintext []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
|
||||||
return ciphertext, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptAESGCM(key, ciphertext []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonceSize := gcm.NonceSize()
|
|
||||||
if len(ciphertext) < nonceSize {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
||||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return plaintext, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptAESCBC(key, plaintext []byte) ([]byte, error) {
|
func encryptAESCBC(key, plaintext []byte) ([]byte, error) {
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -412,20 +367,6 @@ func (i *Identity) GetCurrentRatchetKey() []byte {
|
|||||||
return latestKey
|
return latestKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Identity) EncryptSymmetric(plaintext []byte, key []byte) ([]byte, error) {
|
|
||||||
if len(key) != 32 {
|
|
||||||
return nil, errors.New("invalid key length")
|
|
||||||
}
|
|
||||||
return encryptAESGCM(key, plaintext)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Identity) DecryptSymmetric(ciphertext []byte, key []byte) ([]byte, error) {
|
|
||||||
if len(key) != 32 {
|
|
||||||
return nil, errors.New("invalid key length")
|
|
||||||
}
|
|
||||||
return decryptAESGCM(key, ciphertext)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRatchets bool, ratchetIDReceiver *common.RatchetIDReceiver) ([]byte, error) {
|
func (i *Identity) Decrypt(ciphertextToken []byte, ratchets [][]byte, enforceRatchets bool, ratchetIDReceiver *common.RatchetIDReceiver) ([]byte, error) {
|
||||||
if i.privateKey == nil {
|
if i.privateKey == nil {
|
||||||
return nil, errors.New("decryption failed because identity does not hold a private key")
|
return nil, errors.New("decryption failed because identity does not hold a private key")
|
||||||
|
|||||||
Reference in New Issue
Block a user