Without an upper bound, a client or malicious peer could set `ValidFor` to an absurdly large duration (e.g. 290 years), which is functionally equivalent to no expiry. Add `config.MaxValidFor` (default 30 days) and enforce it at all three ingress points: HTTP handler, CLI, and gossip import filter.
246 lines
8.0 KiB
Go
246 lines
8.0 KiB
Go
// Package config provides structures and functions for loading app config.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.clan.lol/clan/data-mesher/pkg/model"
|
|
"git.clan.lol/clan/data-mesher/pkg/systemd"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/spf13/pflag"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const (
|
|
// DefaultMaxFileSize is the default maximum file size (10 MiB).
|
|
DefaultMaxFileSize = 10 * 1024 * 1024
|
|
|
|
// DefaultSweepInterval is how often the background sweeper walks the signature
|
|
// store removing files whose ValidFor has elapsed.
|
|
DefaultSweepInterval = 60 * time.Second
|
|
|
|
// DefaultClockSkewTolerance is the slack the gossip ingress filter applies
|
|
// when rejecting already-expired remote signatures. A remote sig is rejected
|
|
// only if its effective expiry is more than this far in the past relative to
|
|
// the local clock.
|
|
DefaultClockSkewTolerance = 2 * time.Minute
|
|
|
|
// DefaultMaxValidFor is the upper bound on per-file TTLs. Any ValidFor duration
|
|
// exceeding this value is rejected at the HTTP, CLI, and gossip boundaries.
|
|
DefaultMaxValidFor = 30 * 24 * time.Hour // 30 days
|
|
)
|
|
|
|
// ErrInvalidConfig represents an error indicating that the provided configuration is invalid.
|
|
var ErrInvalidConfig = errors.New("invalid config")
|
|
|
|
// Config represents the application's configuration, containing settings for state directory, host, network, HTTP,
|
|
// cluster, and DNS.
|
|
type Config struct {
|
|
HTTP HTTP `mapstructure:"http"` // HTTP-related config such as ports and addresses
|
|
Cluster Cluster `mapstructure:"cluster"` // clustering related configuration
|
|
|
|
MaxFileSize int64 `mapstructure:"max_file_size"` // maximum allowed file size in bytes
|
|
MaxValidFor time.Duration `mapstructure:"max_valid_for"` // upper bound on per-file TTLs
|
|
|
|
StateDirectory string `mapstructure:"state_directory"` // directory in which to store any state
|
|
PluginDirectories []string `mapstructure:"plugin_directories"` // plugin subdirs per network
|
|
|
|
// SweepInterval controls how often the background sweeper walks the
|
|
// signature store removing files whose ValidFor has elapsed. Zero falls
|
|
// back to DefaultSweepInterval.
|
|
SweepInterval time.Duration `mapstructure:"sweep_interval"`
|
|
|
|
// ClockSkewTolerance is the slack applied by the gossip ingress filter
|
|
// when rejecting already-expired remote signatures. Zero falls back to
|
|
// DefaultClockSkewTolerance.
|
|
ClockSkewTolerance time.Duration `mapstructure:"clock_skew_tolerance"`
|
|
|
|
Network Network `mapstructure:"network"`
|
|
ExtraNetworks ExtraNetworks `mapstructure:"extra_networks"`
|
|
|
|
Now model.Clock `mapstructure:"-"`
|
|
|
|
SignatureAuth *SignatureAuth `mapstructure:"-"`
|
|
|
|
NetworksByID map[string]*Network `mapstructure:"-"`
|
|
}
|
|
|
|
// Validate checks the configuration fields for required values and validates associated components.
|
|
// Returns an error if invalid.
|
|
func (c *Config) Validate() error {
|
|
var err error
|
|
|
|
if c.StateDirectory == "" {
|
|
return fmt.Errorf("%w: state directory is required", ErrInvalidConfig)
|
|
}
|
|
|
|
// apply default max file size if not set
|
|
if c.MaxFileSize == 0 {
|
|
c.MaxFileSize = DefaultMaxFileSize
|
|
}
|
|
|
|
if c.MaxFileSize <= 0 {
|
|
return fmt.Errorf("%w: max_file_size must be positive", ErrInvalidConfig)
|
|
}
|
|
|
|
// apply default max valid for if not set
|
|
if c.MaxValidFor == 0 {
|
|
c.MaxValidFor = DefaultMaxValidFor
|
|
}
|
|
|
|
if c.MaxValidFor <= 0 {
|
|
return fmt.Errorf("%w: max_valid_for must be positive", ErrInvalidConfig)
|
|
}
|
|
|
|
// apply defaults for TTL-related settings
|
|
if c.SweepInterval == 0 {
|
|
c.SweepInterval = DefaultSweepInterval
|
|
}
|
|
|
|
if c.SweepInterval < 0 {
|
|
return fmt.Errorf("%w: sweep_interval must be non-negative", ErrInvalidConfig)
|
|
}
|
|
|
|
if c.ClockSkewTolerance == 0 {
|
|
c.ClockSkewTolerance = DefaultClockSkewTolerance
|
|
}
|
|
|
|
if c.ClockSkewTolerance < 0 {
|
|
return fmt.Errorf("%w: clock_skew_tolerance must be non-negative", ErrInvalidConfig)
|
|
}
|
|
|
|
if err = c.HTTP.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = c.Network.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// set the default network name
|
|
c.Network.Name = DefaultNetworkName
|
|
|
|
if err = c.Cluster.Validate(c.Network.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = c.ExtraNetworks.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// index networks by ID
|
|
c.NetworksByID = map[string]*Network{
|
|
c.Network.ID.String(): &c.Network,
|
|
}
|
|
|
|
for _, network := range c.ExtraNetworks {
|
|
c.NetworksByID[network.ID.String()] = &network
|
|
}
|
|
|
|
// construct a signature filter to help with accepting/rejecting signatures via the API or from other peers
|
|
c.SignatureAuth = NewSignatureAuth(c)
|
|
|
|
// set the clock to be used throughout the system
|
|
// this can be overridden for testing
|
|
c.Now = model.Now
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetFlags initializes and attaches a set of configuration flags to the provided FlagSet.
|
|
func SetFlags(fs *pflag.FlagSet) {
|
|
fs.String("log_level", "info", "Log level (warn, info, debug)")
|
|
|
|
fs.Int64("max_file_size", DefaultMaxFileSize, "Maximum allowed file size in bytes")
|
|
fs.String("state_dir", "/var/lib/data-mesher", "Directory to store state")
|
|
|
|
fs.Duration("sweep_interval", DefaultSweepInterval,
|
|
"How often to walk the signature store removing files whose TTL has elapsed")
|
|
fs.Duration("clock_skew_tolerance", DefaultClockSkewTolerance,
|
|
"Tolerance applied when rejecting already-expired remote signatures during gossip")
|
|
|
|
fs.Uint16("cluster.port", 7946, "Port for cluster connections")
|
|
fs.StringSlice("cluster.interfaces", nil, "Network interfaces for cluster connections (empty = all)")
|
|
fs.StringSlice("cluster.bootstrap_peers", []string{}, "Bootstrap peer multiaddrs")
|
|
|
|
fs.String("cluster.push_pull_interval", "30s", "Push/pull interval")
|
|
|
|
fs.String("cluster.identity_key", "", "Path to libp2p identity key file")
|
|
fs.String(
|
|
"cluster.identity_cert",
|
|
"", "Path to identity certificate file (identity key signed by network key)",
|
|
)
|
|
|
|
fs.String("http.port", "7331", "Port to listen on for HTTP requests")
|
|
fs.StringSlice("http.interfaces", []string{"lo"}, "Interfaces to listen on for HTTP requests")
|
|
}
|
|
|
|
// NewViper initializes and returns a new viper instance configured for the application with environment and
|
|
// file settings.
|
|
func NewViper() (*viper.Viper, error) {
|
|
v := viper.New()
|
|
|
|
// set config type to TOML
|
|
v.SetConfigType("toml")
|
|
|
|
// bind env vars using Config as a template, allowing nested mappings
|
|
BindEnvVars(v, "DATA_MESHER", &Config{})
|
|
|
|
// bind systemd ENV variables to top-level config keys
|
|
if err := bindSystemdVars(v); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set config file name to dm.toml
|
|
v.SetConfigName("dm")
|
|
|
|
// look in the current working directory first
|
|
v.AddConfigPath(".")
|
|
|
|
// look in the systemd config directory next, falling back to /etc/data-mesher if not
|
|
v.AddConfigPath(systemd.ConfigDirectory("/etc/data-mesher"))
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func bindSystemdVars(v *viper.Viper) error {
|
|
if err := v.BindEnv("state_directory", "STATE_DIRECTORY"); err != nil {
|
|
return fmt.Errorf("failed to bind systemd env var: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FromViper extracts configuration from a Viper instance, unmarshals it into a Config struct, and validates it.
|
|
func FromViper(v *viper.Viper) (*Config, error) {
|
|
cfg := &Config{}
|
|
|
|
// add some custom decoders
|
|
decoderOpts := viper.DecodeHook(
|
|
mapstructure.ComposeDecodeHookFunc(
|
|
mapstructure.TextUnmarshallerHookFunc(), // standard decoder
|
|
stringToPublicKeyHookFunc(), // string to crypto.PublicKey
|
|
stringToPublicKeyListFunc(), // string list to []*crypto.PublicKey
|
|
stringToPrivateKeyHookFunc(), // string to *crypto.PrivateKey
|
|
stringToCertificateHookFunc(), // string to *crypto.Certificate
|
|
stringToAddrInfoSliceHookFunc(), // string list to []peer.AddrInfo
|
|
mapstructure.StringToTimeDurationHookFunc(), // string to time
|
|
mapstructure.StringToSliceHookFunc(","), // handle lists
|
|
),
|
|
)
|
|
|
|
// unmarshal into config instance
|
|
if err := v.Unmarshal(cfg, decoderOpts); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
}
|
|
|
|
// validate the config
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, fmt.Errorf("failed to validate config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|