package daecommon import ( "bytes" "fmt" "io" "isle/bootstrap" "isle/toolkit" "isle/yamlutil" "net" _ "embed" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "github.com/sirupsen/logrus" "github.com/slackhq/nebula" "github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/config" "gopkg.in/yaml.v3" ) const ( // Network ID used when translating from the old single-network daemon // config to the multi-network config. DeprecatedNetworkID = "_" // DEPRECATED ) //go:embed daemon.yml var defaultConfigB []byte // ConfigFirewall defines all firewall configuration for a VPN. type ConfigFirewall struct { Outbound []ConfigFirewallRule `yaml:"outbound"` Inbound []ConfigFirewallRule `yaml:"inbound"` } // validate returns an error if the ConfigFirewall is invalid. func (f ConfigFirewall) validate() error { // This method is implemented by actually constructing nebula's firewall // using its own internals. l := logrus.New() l.SetOutput(io.Discard) nebulaConfig := struct { Firewall ConfigFirewall `yaml:"firewall"` }{f} nebulaConfigB, err := yaml.Marshal(nebulaConfig) if err != nil { return fmt.Errorf("yaml marshaling firewall config: %w", err) } config := config.NewC(l) if err := config.LoadString(string(nebulaConfigB)); err != nil { return fmt.Errorf("loading firewall config into nebula config: %w", err) } // The NebulaCertificate is used internally only to pull out relevant IP // ranges, which the firewall doesn't use for instantiation/validation. nc := new(cert.NebulaCertificate) _, err = nebula.NewFirewallFromConfig(l, nc, config) return err } // ConfigFirewallRule represents a firewall rule which can allow either incoming // or outgoing packets via the VPN. type ConfigFirewallRule struct { Port string `yaml:"port,omitempty"` Code string `yaml:"code,omitempty"` Proto string `yaml:"proto,omitempty"` Host string `yaml:"host,omitempty"` Group string `yaml:"group,omitempty"` Groups []string `yaml:"groups,omitempty"` CIDR string `yaml:"cidr,omitempty"` CASha string `yaml:"ca_sha,omitempty"` CAName string `yaml:"ca_name,omitempty"` } // ConfigStorageAllocation describes the structure of each storage allocation // within the daemon config file. type ConfigStorageAllocation struct { DataPath string `yaml:"data_path"` MetaPath string `yaml:"meta_path"` Capacity int `yaml:"capacity"` S3APIPort int `yaml:"s3_api_port"` RPCPort int `yaml:"rpc_port"` AdminPort int `yaml:"admin_port"` } // Annotate implements the mctx.Annotator interface. func (csa ConfigStorageAllocation) Annotate(aa mctx.Annotations) { aa["allocDataPath"] = csa.DataPath aa["allocMetaPath"] = csa.MetaPath aa["allocCapacity"] = csa.Capacity aa["allocS3APIPort"] = csa.S3APIPort aa["allocRPCPort"] = csa.RPCPort aa["allocAdminPort"] = csa.AdminPort } // NetworkConfig describes the configuration of a single network. type NetworkConfig struct { DNS struct { Resolvers []string `yaml:"resolvers"` } `yaml:"dns"` VPN struct { PublicAddr string `yaml:"public_addr"` Firewall ConfigFirewall `yaml:"firewall"` } `yaml:"vpn"` Storage struct { Allocations []ConfigStorageAllocation `yaml:"allocations"` } `yaml:"storage"` } // NewNetworkConfig returns a new NetworkConfig populated with its default // values. If a callback is given the NetworkConfig will be passed to it for // modification prior to having defaults populated. func NewNetworkConfig(fn func(*NetworkConfig)) NetworkConfig { var c NetworkConfig if fn != nil { fn(&c) } c.fillDefaults() return c } func (c *NetworkConfig) fillDefaults() { if c.DNS.Resolvers == nil { c.DNS.Resolvers = []string{ "1.1.1.1", "8.8.8.8", } } if c.VPN.Firewall.Outbound == nil { c.VPN.Firewall.Outbound = []ConfigFirewallRule{ { Port: "any", Proto: "any", Host: "any", }, } } if c.VPN.Firewall.Inbound == nil { c.VPN.Firewall.Inbound = []ConfigFirewallRule{ { Port: "any", Proto: "icmp", Host: "any", }, } } nextRPCPort := 3900 for i := range c.Storage.Allocations { if c.Storage.Allocations[i].RPCPort == 0 { c.Storage.Allocations[i].RPCPort = nextRPCPort } nextRPCPort += 10 if c.Storage.Allocations[i].S3APIPort == 0 { c.Storage.Allocations[i].S3APIPort = c.Storage.Allocations[i].RPCPort + 1 } if c.Storage.Allocations[i].AdminPort == 0 { c.Storage.Allocations[i].AdminPort = c.Storage.Allocations[i].RPCPort + 2 } } } // Validate asserts that the NetworkConfig is valid as far as can be determined // without further context. func (c NetworkConfig) Validate() error { if err := c.VPN.Firewall.validate(); err != nil { return fmt.Errorf("invalid VPN firewall config: %w", err) } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to // fill in default values where it can. func (c *NetworkConfig) UnmarshalYAML(n *yaml.Node) error { type wrap NetworkConfig if err := n.Decode((*wrap)(c)); err != nil { return fmt.Errorf("decoding into %T: %w", c, err) } c.fillDefaults() return nil } // Config describes the structure of the daemon config file. type Config struct { Networks map[string]NetworkConfig `yaml:"networks"` } // Validate asserts that the Config has no internal inconsistencies which would // render it unusable. func (c Config) Validate() error { nebulaPorts := map[string]string{} for id, network := range c.Networks { if network.VPN.PublicAddr != "" { _, port, err := net.SplitHostPort(network.VPN.PublicAddr) if err != nil { return fmt.Errorf( "invalid vpn.public_addr %q: %w", network.VPN.PublicAddr, err, ) } else if otherID, ok := nebulaPorts[port]; ok { return fmt.Errorf( "two networks with the same vpn.public_addr port: %q and %q", id, otherID, ) } nebulaPorts[port] = id } if err := network.Validate(); err != nil { return fmt.Errorf("invalid config for network %q: %w", id, err) } } return nil } // CopyDefaultConfig copies the daemon config file embedded in the AppDir into // the given io.Writer. func CopyDefaultConfig(into io.Writer) error { _, err := io.Copy(into, bytes.NewReader(defaultConfigB)) return err } // UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to // fill in default values where it can. func (c *Config) UnmarshalYAML(n *yaml.Node) error { { // DEPRECATED // We decode into a wrapped NetworkConfig, so that its UnmarshalYAML // doesn't get invoked. This prevents fillDefaults from getting // automatically called, so the IsZero check will work as intended. This // means we need to call fillDefaults manually though. type wrap NetworkConfig var networkConfig NetworkConfig _ = n.Decode((*wrap)(&networkConfig)) if !toolkit.IsZero(networkConfig) { networkConfig.fillDefaults() *c = Config{ Networks: map[string]NetworkConfig{ DeprecatedNetworkID: networkConfig, }, } return c.Validate() } } type wrap Config if err := n.Decode((*wrap)(c)); err != nil { return fmt.Errorf("yaml unmarshaling back into Config struct: %w", err) } return c.Validate() } // LoadConfig loads the daemon config from userConfigPath. // // If userConfigPath is not given then the default is loaded and returned. func LoadConfig(userConfigPath string) (Config, error) { if userConfigPath == "" { return Config{}, nil } var config Config err := yamlutil.LoadYamlFile(&config, userConfigPath) return config, err } // BootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which // corresponds with the given alloc from the daemon config. This will panic if // no associated instance can be found. func BootstrapGarageHostForAlloc( host bootstrap.Host, alloc ConfigStorageAllocation, ) bootstrap.GarageHostInstance { for _, inst := range host.Garage.Instances { if inst.RPCPort == alloc.RPCPort { return inst } } panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc)) }