Skip to content

Commit

Permalink
Implement Firewall support (#301)
Browse files Browse the repository at this point in the history
  • Loading branch information
LKaemmerling authored Mar 10, 2021
1 parent 67ba0ad commit afd597a
Show file tree
Hide file tree
Showing 23 changed files with 1,191 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/dist
/bats
/cmd/hcloud/hcloud
hcloud_cli.p12
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ signs:
cmd: gon
args:
- -log-json
- gon.hcl
- gon_arm64.hcl
artifacts: all
signature: "hcloud-macos-arm64.zip"
id: hcloud-macos-sign-arm64
Expand Down
24 changes: 24 additions & 0 deletions examples/firewall_rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"direction": "in",
"source_ips": [
"28.239.13.1/32",
"28.239.14.0/24",
"ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128"
],
"destination_ips": [],
"protocol": "tcp",
"port": "80"
},
{
"direction": "out",
"source_ips": [],
"destination_ips": [
"28.239.13.1/32",
"28.239.14.0/24",
"ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128"
],
"protocol": "tcp",
"port": "80"
}
]
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/fatih/structs v1.1.0
github.com/guptarohit/asciigraph v0.5.1
github.com/hetznercloud/hcloud-go v1.23.1
github.com/hetznercloud/hcloud-go v1.24.0
github.com/pelletier/go-toml v1.8.0
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
)

Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
Expand Down Expand Up @@ -103,8 +103,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hetznercloud/hcloud-go v1.23.1 h1:SkYdCa6x458cMSDz5GI18iPz5j2hicACiDP6J/s/bTs=
github.com/hetznercloud/hcloud-go v1.23.1/go.mod h1:xng8lbDUg+xM1dgc0yGHX5EeqbwIq7UYlMWMTx3SQVg=
github.com/hetznercloud/hcloud-go v1.24.0 h1:/CeHDzhH3Fhm83pjxvE3xNNLbvACl0Lu1/auJ83gG5U=
github.com/hetznercloud/hcloud-go v1.24.0/go.mod h1:3YmyK8yaZZ48syie6xpm3dt26rtB6s65AisBHylXYFA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
Expand Down Expand Up @@ -187,8 +187,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/completion"
"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/cmd/datacenter"
"github.com/hetznercloud/cli/internal/cmd/firewall"
"github.com/hetznercloud/cli/internal/cmd/floatingip"
"github.com/hetznercloud/cli/internal/cmd/image"
"github.com/hetznercloud/cli/internal/cmd/iso"
Expand Down Expand Up @@ -50,6 +51,7 @@ func NewRootCommand(state *state.State) *cobra.Command {
loadbalancer.NewCommand(state),
loadbalancertype.NewCommand(state),
certificate.NewCommand(state),
firewall.NewCommand(state),
)
cmd.PersistentFlags().Duration("poll-interval", 500*time.Millisecond, "Interval at which to poll information, for example action progress")
return cmd
Expand Down
68 changes: 68 additions & 0 deletions internal/cmd/firewall/add_label.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package firewall

import (
"fmt"

"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/cmpl"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

func newAddLabelCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "add-label [FLAGS] FIREWALL LABEL",
Short: "Add a label to a Firewall",
Args: cobra.ExactArgs(2),
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(cli.FirewallNames)),
TraverseChildren: true,
DisableFlagsInUseLine: true,
PreRunE: util.ChainRunE(validateFirewallAddLabel, cli.EnsureToken),
RunE: cli.Wrap(runFirewallAddLabel),
}

cmd.Flags().BoolP("overwrite", "o", false, "Overwrite label if it exists already")
return cmd
}

func validateFirewallAddLabel(cmd *cobra.Command, args []string) error {
label := util.SplitLabel(args[1])
if len(label) != 2 {
return fmt.Errorf("invalid label: %s", args[1])
}

return nil
}

func runFirewallAddLabel(cli *state.State, cmd *cobra.Command, args []string) error {
overwrite, _ := cmd.Flags().GetBool("overwrite")

idOrName := args[0]
firewall, _, err := cli.Client().Firewall.Get(cli.Context, idOrName)
if err != nil {
return err
}
if firewall == nil {
return fmt.Errorf("Firewall not found: %v", idOrName)
}

label := util.SplitLabel(args[1])

if _, ok := firewall.Labels[label[0]]; ok && !overwrite {
return fmt.Errorf("label %s on Firewall %d already exists", label[0], firewall.ID)
}
labels := firewall.Labels
labels[label[0]] = label[1]
opts := hcloud.FirewallUpdateOpts{
Labels: labels,
}
_, _, err = cli.Client().Firewall.Update(cli.Context, firewall, opts)
if err != nil {
return err
}
fmt.Printf("Label %s added to Firewall %d\n", label[0], firewall.ID)

return nil
}
112 changes: 112 additions & 0 deletions internal/cmd/firewall/add_rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package firewall

import (
"fmt"
"net"

"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/cmpl"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

func newAddRuleCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "add-rule FIREWALL FLAGS",
Short: "Add a single rule to a firewall",
Args: cobra.ExactArgs(1),
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(cli.FirewallNames)),
TraverseChildren: true,
DisableFlagsInUseLine: true,
PreRunE: util.ChainRunE(cli.EnsureToken),
RunE: cli.Wrap(runAddRule),
}
cmd.Flags().String("direction", "", "Direction (in, out) (required)")
cmd.RegisterFlagCompletionFunc("direction", cmpl.SuggestCandidates("in", "out"))
cmd.MarkFlagRequired("direction")

cmd.Flags().String("protocol", "", "Protocol (icmp, udp or tcp) (required)")
cmd.RegisterFlagCompletionFunc("protocol", cmpl.SuggestCandidates("icmp", "udp", "tcp"))
cmd.MarkFlagRequired("protocol")

cmd.Flags().StringArray("source-ips", []string{}, "Source IPs (CIDR Notation) (required when direction is in)")

cmd.Flags().StringArray("destination-ips", []string{}, "Destination IPs (CIDR Notation) (required when direction is out)")

cmd.Flags().String("port", "", "Port to which traffic will be allowed, only applicable for protocols TCP and UDP, you can specify port ranges, sample: 80-85")
return cmd
}

func runAddRule(cli *state.State, cmd *cobra.Command, args []string) error {
direction, _ := cmd.Flags().GetString("direction")
protocol, _ := cmd.Flags().GetString("protocol")
sourceIPs, _ := cmd.Flags().GetStringArray("source-ips")
destinationIPs, _ := cmd.Flags().GetStringArray("destination-ips")
port, _ := cmd.Flags().GetString("port")

idOrName := args[0]
firewall, _, err := cli.Client().Firewall.Get(cli.Context, idOrName)
if err != nil {
return err
}
if firewall == nil {
return fmt.Errorf("Firewall not found: %v", idOrName)
}

var sourceNets []net.IPNet
for i, sourceIP := range sourceIPs {
_, sourceNet, err := net.ParseCIDR(sourceIP)
if err != nil {
return fmt.Errorf("invalid CIDR on index %d : %s", i, err)
}
sourceNets = append(sourceNets, *sourceNet)
}
d := hcloud.FirewallRuleDirection(direction)
rule := hcloud.FirewallRule{
Direction: d,
Protocol: hcloud.FirewallRuleProtocol(protocol),
}

if port != "" {
rule.Port = hcloud.String(port)
}

switch d {
case hcloud.FirewallRuleDirectionOut:
rule.DestinationIPs = make([]net.IPNet, 0, len(destinationIPs))
for i, ip := range destinationIPs {
_, n, err := net.ParseCIDR(ip)
if err != nil {
return fmt.Errorf("invalid CIDR on index %d : %s", i, err)
}
rule.DestinationIPs[i] = *n
}
case hcloud.FirewallRuleDirectionIn:
rule.SourceIPs = make([]net.IPNet, 0, len(sourceIPs))
for i, ip := range sourceIPs {
_, n, err := net.ParseCIDR(ip)
if err != nil {
return fmt.Errorf("invalid CIDR on index %d : %s", i, err)
}
rule.SourceIPs[i] = *n
}
}

rules := append(firewall.Rules, rule)

actions, _, err := cli.Client().Firewall.SetRules(cli.Context, firewall,
hcloud.FirewallSetRulesOpts{Rules: rules},
)
if err != nil {
return err
}
if err := cli.ActionsProgresses(cli.Context, actions); err != nil {
return err
}

fmt.Printf("Firewall Rules for Firewall %d updated\n", firewall.ID)

return nil
}
88 changes: 88 additions & 0 deletions internal/cmd/firewall/apply_to_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package firewall

import (
"fmt"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/cmpl"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

func newApplyToResourceCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "apply-to-resource FIREWALL FLAGS",
Short: "Applies a Firewall to a single resource",
Args: cobra.ExactArgs(1),
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidatesF(cli.FirewallNames)),
TraverseChildren: true,
DisableFlagsInUseLine: true,
PreRunE: util.ChainRunE(validateApplyToResource, cli.EnsureToken),
RunE: cli.Wrap(runApplyToResource),
}
cmd.Flags().String("type", "", "Resource Type (server) (required)")
cmd.RegisterFlagCompletionFunc("type", cmpl.SuggestCandidates("server"))
cmd.MarkFlagRequired("type")

cmd.Flags().String("server", "", "Server name of ID (required when type is server)")
cmd.RegisterFlagCompletionFunc("server", cmpl.SuggestCandidatesF(cli.ServerNames))

return cmd
}
func validateApplyToResource(cmd *cobra.Command, args []string) error {
resourceType, _ := cmd.Flags().GetString("type")

switch resourceType {
case "server":
server, _ := cmd.Flags().GetString("server")
if server == "" {
return fmt.Errorf("type %s need a --server specific", resourceType)
}
default:
return fmt.Errorf("unknown type %s", resourceType)
}

return nil
}
func runApplyToResource(cli *state.State, cmd *cobra.Command, args []string) error {
resourceType, _ := cmd.Flags().GetString("type")
serverIdOrName, _ := cmd.Flags().GetString("server")

idOrName := args[0]
firewall, _, err := cli.Client().Firewall.Get(cli.Context, idOrName)
if err != nil {
return err
}
if firewall == nil {
return fmt.Errorf("Firewall not found: %v", idOrName)
}

server, _, err := cli.Client().Server.Get(cli.Context, serverIdOrName)
if err != nil {
return err
}
if server == nil {
return fmt.Errorf("Server not found: %v", serverIdOrName)
}

opts := hcloud.FirewallResource{Type: hcloud.FirewallResourceType(resourceType)}

switch opts.Type {
case hcloud.FirewallResourceTypeServer:
opts.Server = &hcloud.FirewallResourceServer{ID: server.ID}
default:
return fmt.Errorf("unkown type %s", resourceType)
}

actions, _, err := cli.Client().Firewall.ApplyResources(cli.Context, firewall, []hcloud.FirewallResource{opts})
if err != nil {
return err
}
if err := cli.ActionsProgresses(cli.Context, actions); err != nil {
return err
}
fmt.Printf("Firewall %d applied\n", firewall.ID)

return nil
}
Loading

0 comments on commit afd597a

Please sign in to comment.