Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce firewall egress and ingress rules for firewall allocation. #491

Merged
merged 28 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e693aa1
Introduce firewall egress and ingress rules for firewall allocation.
Gerrit91 Jan 22, 2024
e96760e
Fix tests.
Gerrit91 Jan 22, 2024
c54e748
Review comments.
Gerrit91 Jan 22, 2024
0cf1a12
Review comments.
Gerrit91 Jan 22, 2024
b28bca8
Merge master
majst01 Feb 5, 2024
48e169c
Merge master
majst01 Feb 5, 2024
a974798
Fill response
majst01 Feb 5, 2024
1d75046
Better naming
majst01 Feb 5, 2024
641a2a6
API refinement
majst01 Feb 6, 2024
11533de
Consistent api
majst01 Feb 6, 2024
e3d6526
Simplify integration tests
majst01 Feb 6, 2024
dd35380
Fix.
Gerrit91 Feb 6, 2024
46bbc65
Ensure rule comment is not dangerous
majst01 Feb 8, 2024
695f053
Merge branch 'firewall-rules' of https://github.com/metal-stack/metal…
majst01 Feb 8, 2024
62ebcbc
Merge branch 'master' into firewall-rules
majst01 Feb 8, 2024
d2fde04
Update deps
majst01 Feb 8, 2024
2c2afd1
Add validation test
majst01 Feb 8, 2024
105e52f
Merge branch 'master' into firewall-rules
majst01 Feb 12, 2024
5459307
Fix
majst01 Feb 12, 2024
53e8462
Fix protocol
majst01 Feb 12, 2024
8c2bdce
Use range over int
majst01 Feb 13, 2024
84a6e41
update masterdata
majst01 Feb 13, 2024
ff634e9
Merge branch 'master' of https://github.com/metal-stack/metal-api int…
majst01 Feb 13, 2024
f477814
Ingress Rule with toCidrs as well
majst01 Feb 13, 2024
ede5378
More validation
majst01 Feb 14, 2024
01c4d02
Renaming in the api
majst01 Feb 14, 2024
b7cda32
Merge branch 'master' into firewall-rules
majst01 Feb 14, 2024
90b3adc
Better tests
majst01 Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions cmd/metal-api/internal/metal/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package metal

import (
"fmt"
"net"
majst01 marked this conversation as resolved.
Show resolved Hide resolved
"os"
"strings"
"time"
Expand Down Expand Up @@ -147,6 +148,99 @@ type MachineAllocation struct {
MachineSetup *MachineSetup `rethinkdb:"setup" json:"setup"`
Role Role `rethinkdb:"role" json:"role"`
VPN *MachineVPN `rethinkdb:"vpn" json:"vpn"`
Egress []EgressRule `rethinkdb:"egress" json:"egress"`
Ingress []IngressRule `rethinkdb:"ingress" json:"ingress"`
}

type EgressRule struct {
Protocol Protocol `rethinkdb:"protocol" json:"protocol"`
Ports []int `rethinkdb:"ports" json:"ports"`
FromCIDRs []string `rethinkdb:"from_cidrs" json:"from_cidrs"`
Gerrit91 marked this conversation as resolved.
Show resolved Hide resolved
Comment string `rethinkdb:"comment" json:"comment"`
}

type IngressRule struct {
Protocol Protocol `rethinkdb:"protocol" json:"protocol"`
Ports []int `rethinkdb:"ports" json:"ports"`
ToCIDRs []string `rethinkdb:"to_cidrs" json:"from_cidrs"`
majst01 marked this conversation as resolved.
Show resolved Hide resolved
Comment string `rethinkdb:"comment" json:"comment"`
}

type Protocol string

const (
ProtocolTCP Protocol = "TCP"
ProtocolUDP Protocol = "UDP"
)

func ProtocolFromString(s string) (Protocol, error) {
switch strings.ToLower(s) {
case "tcp":
return ProtocolTCP, nil
case "udp":
return ProtocolTCP, nil
default:
return Protocol(""), fmt.Errorf("no such protocol: %s", s)
}
}

func (r EgressRule) Validate() error {
switch r.Protocol {
case ProtocolTCP, ProtocolUDP:
// ok
default:
return fmt.Errorf("invalid procotol: %s", r.Protocol)
}

if err := validatePorts(r.Ports); err != nil {
return err
}

if err := validateCIDRs(r.FromCIDRs); err != nil {
return err
}

return nil
}

func (r IngressRule) Validate() error {
switch r.Protocol {
case ProtocolTCP, ProtocolUDP:
// ok
default:
return fmt.Errorf("invalid procotol: %s", r.Protocol)
}

if err := validatePorts(r.Ports); err != nil {
return err
}

if err := validateCIDRs(r.ToCIDRs); err != nil {
return err
}

return nil
}

func validatePorts(ports []int) error {
for _, port := range ports {
if port < 0 || port > 65535 {
return fmt.Errorf("port is out of range")
}
}

return nil
}

func validateCIDRs(cidrs []string) error {
for _, cidr := range cidrs {
_, _, err := net.ParseCIDR(cidr)
majst01 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("invalid cidr: %w", err)
}
}

return nil
}

// A MachineSetup stores the data used for machine reinstallations.
Expand Down
2 changes: 1 addition & 1 deletion cmd/metal-api/internal/service/firewall-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (r *firewallResource) allocateFirewall(request *restful.Request, response *
return
}

spec, err := createMachineAllocationSpec(r.ds, requestPayload.MachineAllocateRequest, metal.RoleFirewall, user)
spec, err := createMachineAllocationSpec(r.ds, requestPayload.MachineAllocateRequest, &requestPayload.FirewallAllocateRequest, user)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(err))
return
Expand Down
121 changes: 97 additions & 24 deletions cmd/metal-api/internal/service/machine-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type machineAllocationSpec struct {
Role metal.Role
VPN *metal.MachineVPN
PlacementTags []string
EgressRules []metal.EgressRule
IngressRules []metal.IngressRule
}

// allocationNetwork is intermediate struct to create machine networks from regular networks during machine allocation
Expand Down Expand Up @@ -976,7 +978,7 @@ func (r *machineResource) allocateMachine(request *restful.Request, response *re
return
}

spec, err := createMachineAllocationSpec(r.ds, requestPayload, metal.RoleMachine, user)
spec, err := createMachineAllocationSpec(r.ds, requestPayload, nil, user)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(err))
return
Expand All @@ -997,39 +999,92 @@ func (r *machineResource) allocateMachine(request *restful.Request, response *re
r.send(request, response, http.StatusOK, resp)
}

func createMachineAllocationSpec(ds *datastore.RethinkStore, requestPayload v1.MachineAllocateRequest, role metal.Role, user *security.User) (*machineAllocationSpec, error) {
func createMachineAllocationSpec(ds *datastore.RethinkStore, machineRequest v1.MachineAllocateRequest, firewallRequest *v1.FirewallAllocateRequest, user *security.User) (*machineAllocationSpec, error) {
var uuid string
if requestPayload.UUID != nil {
uuid = *requestPayload.UUID
if machineRequest.UUID != nil {
uuid = *machineRequest.UUID
}
var name string
if requestPayload.Name != nil {
name = *requestPayload.Name
if machineRequest.Name != nil {
name = *machineRequest.Name
}
var description string
if requestPayload.Description != nil {
description = *requestPayload.Description
if machineRequest.Description != nil {
description = *machineRequest.Description
}
hostname := "metal"
if requestPayload.Hostname != nil {
hostname = *requestPayload.Hostname
if machineRequest.Hostname != nil {
hostname = *machineRequest.Hostname
}
var userdata string
if requestPayload.UserData != nil {
userdata = *requestPayload.UserData
if machineRequest.UserData != nil {
userdata = *machineRequest.UserData
}
if requestPayload.Networks == nil {
if machineRequest.Networks == nil {
return nil, errors.New("network ids cannot be nil")
}
if len(requestPayload.Networks) <= 0 {
if len(machineRequest.Networks) <= 0 {
return nil, errors.New("network ids cannot be empty")
}

image, err := ds.FindImage(requestPayload.ImageID)
image, err := ds.FindImage(machineRequest.ImageID)
if err != nil {
return nil, err
}

var (
egress []metal.EgressRule
ingress []metal.IngressRule
role = metal.RoleMachine
)

if firewallRequest != nil {
role = metal.RoleFirewall

for _, ruleSpec := range firewallRequest.Egress {
ruleSpec := ruleSpec

protocol, err := metal.ProtocolFromString(ruleSpec.Protocol)
if err != nil {
return nil, err
}

rule := metal.EgressRule{
Protocol: protocol,
Ports: ruleSpec.Ports,
FromCIDRs: ruleSpec.FromCIDRs,
Comment: ruleSpec.Comment,
}

if err := rule.Validate(); err != nil {
return nil, err
}

egress = append(egress, rule)
}

for _, ruleSpec := range firewallRequest.Ingress {
ruleSpec := ruleSpec

protocol, err := metal.ProtocolFromString(ruleSpec.Protocol)
if err != nil {
return nil, err
}

rule := metal.IngressRule{
Protocol: protocol,
Ports: ruleSpec.Ports,
Comment: ruleSpec.Comment,
}

if err := rule.Validate(); err != nil {
return nil, err
}

ingress = append(ingress, rule)
}
}

imageFeatureType := metal.ImageFeatureMachine
if role == metal.RoleFirewall {
imageFeatureType = metal.ImageFeatureFirewall
Expand All @@ -1039,8 +1094,8 @@ func createMachineAllocationSpec(ds *datastore.RethinkStore, requestPayload v1.M
return nil, fmt.Errorf("given image is not usable for a %s, features: %s", imageFeatureType, image.ImageFeatureString())
}

partitionID := requestPayload.PartitionID
sizeID := requestPayload.SizeID
partitionID := machineRequest.PartitionID
sizeID := machineRequest.SizeID

if uuid == "" && partitionID == "" {
return nil, errors.New("when no machine id is given, a partition id must be specified")
Expand Down Expand Up @@ -1072,19 +1127,21 @@ func createMachineAllocationSpec(ds *datastore.RethinkStore, requestPayload v1.M
Name: name,
Description: description,
Hostname: hostname,
ProjectID: requestPayload.ProjectID,
ProjectID: machineRequest.ProjectID,
PartitionID: partitionID,
Machine: m,
Size: size,
Image: image,
SSHPubKeys: requestPayload.SSHPubKeys,
SSHPubKeys: machineRequest.SSHPubKeys,
UserData: userdata,
Tags: requestPayload.Tags,
Networks: requestPayload.Networks,
IPs: requestPayload.IPs,
Tags: machineRequest.Tags,
Networks: machineRequest.Networks,
IPs: machineRequest.IPs,
Role: role,
FilesystemLayoutID: requestPayload.FilesystemLayoutID,
PlacementTags: requestPayload.PlacementTags,
FilesystemLayoutID: machineRequest.FilesystemLayoutID,
PlacementTags: machineRequest.PlacementTags,
EgressRules: egress,
IngressRules: ingress,
}, nil
}

Expand Down Expand Up @@ -1170,6 +1227,8 @@ func allocateMachine(logger *zap.SugaredLogger, ds *datastore.RethinkStore, ipam
MachineNetworks: []*metal.MachineNetwork{},
Role: allocationSpec.Role,
VPN: allocationSpec.VPN,
Egress: allocationSpec.EgressRules,
Ingress: allocationSpec.IngressRules,
}
rollbackOnError := func(err error) error {
if err != nil {
Expand Down Expand Up @@ -1211,6 +1270,20 @@ func allocateMachine(logger *zap.SugaredLogger, ds *datastore.RethinkStore, ipam
return nil, rollbackOnError(fmt.Errorf("unable to make networks:%w", err))
}

for _, n := range networks {
n := n

if n.networkType != metal.PrivatePrimaryUnshared {
continue
}

for _, rule := range allocationSpec.IngressRules {
rule := rule

rule.ToCIDRs = n.network.Prefixes.String()
}
}

// refetch the machine to catch possible updates after dealing with the network...
machine, err := ds.FindMachineByID(machineCandidate.ID)
if err != nil {
Expand Down
24 changes: 20 additions & 4 deletions cmd/metal-api/internal/service/v1/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@ package v1

type FirewallCreateRequest struct {
MachineAllocateRequest
// HA if set to true firewall is created in ha configuration
//
// Deprecated: will be removed in the next release
HA *bool `json:"ha" description:"if set to true, this firewall is set up in a High Available manner" optional:"true"`
FirewallAllocateRequest
}

type FirewallAllocateRequest struct {
Egress []FirewallEgressRule `json:"egress,omitempty" description:"list of egress rules to be deployed during firewall allocation" optional:"true"`
Ingress []FirewallIngressRule `json:"ingress,omitempty" description:"list of ingress rules to be deployed during firewall allocation" optional:"true"`
}

type FirewallEgressRule struct {
Protocol string `json:"protocol,omitempty" description:"the protocol for the rule, defaults to tcp" enum:"tcp|udp" optional:"true"`
Ports []int `json:"ports" description:"the ports affected by this rule"`
FromCIDRs []string `json:"from_cidrs" description:"the cidrs affected by this rule"`
Gerrit91 marked this conversation as resolved.
Show resolved Hide resolved
Comment string `json:"comment,omitempty" description:"an optional comment describing what this rule is used for" optional:"true"`
}

type FirewallIngressRule struct {
Protocol string `json:"protocol,omitempty" description:"the protocol for the rule, defaults to tcp" enum:"tcp|udp" optional:"true"`
Ports []int `json:"ports" description:"the ports affected by this rule"`
// no ToCIDRs, destination is always the node network
majst01 marked this conversation as resolved.
Show resolved Hide resolved
Comment string `json:"comment,omitempty" description:"an optional comment describing what this rule is used for" optional:"true"`
}

type FirewallResponse struct {
Expand Down
Loading
Loading