package main import ( "errors" "fmt" "isle/daemon/daecommon" "strings" ) const vpnFirewallConfigChangeStagerName = "vpn-firewall-config" // vpnFirewallGetConfigWithStaged returns the network config along with any // staged firewall configuration changes, if there are any. func vpnFirewallGetConfig(ctx subCmdCtx) (daecommon.NetworkConfig, error) { config, err := ctx.getDaemonRPC().GetConfig(ctx) if err != nil { return daecommon.NetworkConfig{}, err } var firewallConfig daecommon.ConfigFirewall if ok, err := ctx.opts.changeStager.get( &firewallConfig, vpnFirewallConfigChangeStagerName, ); err != nil { return daecommon.NetworkConfig{}, fmt.Errorf( "getting staged VPN firewall config: %w", err, ) } else if ok { config.VPN.Firewall = firewallConfig } return config, nil } func vpnFirewallRuleSetToFn( str string, ) ( func(*daecommon.ConfigFirewall) *[]daecommon.ConfigFirewallRule, error, ) { switch strings.ToLower(str) { case "outbound": return func(c *daecommon.ConfigFirewall) *[]daecommon.ConfigFirewallRule { return &c.Outbound }, nil case "inbound": return func(c *daecommon.ConfigFirewall) *[]daecommon.ConfigFirewallRule { return &c.Inbound }, nil default: return nil, fmt.Errorf("must be 'inbound' or 'outbound'") } } var subCmdVPNFirewallAdd = subCmd{ name: "add", descr: "Add a new firewall rule to the staged configuration", do: func(ctx subCmdCtx) error { to := ctx.flags.String( "to", "", "Which set of rules to add to, either 'inbound' or 'outbound'", ) var rule daecommon.ConfigFirewallRule ctx.flags.StringVar( &rule.Port, "port", "any", "Port number or range to allow", ) ctx.flags.StringVar( &rule.Proto, "proto", "any", "Protocol to allow. Can be 'tcp', 'udp', 'icmp', or 'any'", ) ctx.flags.StringVar( &rule.Host, "host", "", "Name of host to allow. Defaults to 'any' if --groups is not given", ) ctx.flags.StringSliceVar( &rule.Groups, "groups", nil, "One or more comma-separated group names to allow", ) ctx, err := ctx.withParsedFlags() if err != nil { return fmt.Errorf("parsing flags: %w", err) } if *to == "" { return errors.New("--to is required") } ruleSetFn, err := vpnFirewallRuleSetToFn(*to) if err != nil { return fmt.Errorf("invalid --to value %q: %w", *to, err) } if rule.Host != "" && len(rule.Groups) > 0 { return fmt.Errorf("--host and --groups are mutually exclusive") } else if rule.Host == "" && len(rule.Groups) == 0 { rule.Host = "any" } config, err := vpnFirewallGetConfig(ctx) if err != nil { return fmt.Errorf("getting network config: %w", err) } ruleSet := ruleSetFn(&config.VPN.Firewall) *ruleSet = append(*ruleSet, rule) if err := config.Validate(); err != nil { return err } if err := ctx.opts.changeStager.set( config.VPN.Firewall, vpnFirewallConfigChangeStagerName, ); err != nil { return fmt.Errorf("staging changes: %w", err) } return nil }, } type firewallRuleView struct { Index int `yaml:"index"` daecommon.ConfigFirewallRule `yaml:",inline"` } func newFirewallRuleViews( rules []daecommon.ConfigFirewallRule, ) []firewallRuleView { views := make([]firewallRuleView, len(rules)) for i := range rules { views[i] = firewallRuleView{ Index: i, ConfigFirewallRule: rules[i], } } return views } type firewallView struct { Outbound []firewallRuleView `yaml:"outbound"` Inbound []firewallRuleView `yaml:"inbound"` } func newFirewallView(firewallConfig daecommon.ConfigFirewall) firewallView { return firewallView{ Outbound: newFirewallRuleViews(firewallConfig.Outbound), Inbound: newFirewallRuleViews(firewallConfig.Inbound), } } var subCmdVPNFirewallList = subCmd{ name: "list", descr: "List all currently configured firewall rules", do: doWithOutput(func(ctx subCmdCtx) (any, error) { staged := ctx.flags.Bool( "staged", false, "Return the firewall configuration with staged changes included", ) ctx, err := ctx.withParsedFlags() if err != nil { return nil, fmt.Errorf("parsing flags: %w", err) } var firewallConfig daecommon.ConfigFirewall if !*staged { config, err := ctx.getDaemonRPC().GetConfig(ctx) if err != nil { return nil, fmt.Errorf("getting network config: %w", err) } firewallConfig = config.VPN.Firewall } else if ok, err := ctx.opts.changeStager.get( &firewallConfig, vpnFirewallConfigChangeStagerName, ); err != nil { return nil, fmt.Errorf("checking for staged changes: %w", err) } else if !ok { return nil, errors.New("no firewall configuration changes have been staged") } return newFirewallView(firewallConfig), nil }), } var subCmdVPNFirewall = subCmd{ name: "firewall", descr: "Sub-commands related to this host's VPN firewall", do: func(ctx subCmdCtx) error { return ctx.doSubCmd( subCmdVPNFirewallAdd, subCmdVPNFirewallList, ) }, }