295 lines
7.8 KiB
Go
295 lines
7.8 KiB
Go
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))
|
|
}
|