diff --git a/apis/networking/v1alpha1/firewall/common_types.go b/apis/networking/v1alpha1/firewall/common_types.go new file mode 100644 index 0000000000..9b5f10f35f --- /dev/null +++ b/apis/networking/v1alpha1/firewall/common_types.go @@ -0,0 +1,51 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import ( + "fmt" + "net" +) + +// IPValueType is the type of the match value. +type IPValueType string + +const ( + // IPValueTypeIP is a string representing an ip. + IPValueTypeIP IPValueType = "ip" + // IPValueTypeSubnet is a string representing a subnet (eg. 10.0.0.0/24). + IPValueTypeSubnet IPValueType = "subnet" + // IPValueTypeInvalid is an invalid match value. + IPValueTypeInvalid IPValueType = "invalid" +) + +// GetIPValueType parses the match value and returns the type of the value. +func GetIPValueType(value string) (IPValueType, error) { + if value == "" { + return IPValueTypeInvalid, fmt.Errorf("match value cannot be empty") + } + + // Check if the value is a pool subnet. + if _, _, err := net.ParseCIDR(value); err == nil { + return IPValueTypeSubnet, nil + } + + // Check if the value is an IP. + if net.ParseIP(value) != nil { + return IPValueTypeIP, nil + } + + return IPValueTypeInvalid, fmt.Errorf("invalid match value %s", value) +} diff --git a/apis/networking/v1alpha1/firewall/filterrule_types.go b/apis/networking/v1alpha1/firewall/filterrule_types.go new file mode 100644 index 0000000000..409206c619 --- /dev/null +++ b/apis/networking/v1alpha1/firewall/filterrule_types.go @@ -0,0 +1,24 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +var _ Rule = &FilterRule{} + +// FilterRule is a rule to be applied to a filter chain. +// +kubebuilder:object:generate=true +type FilterRule struct { + // Name is the name of the rule. + Name *string `json:"name,omitempty"` +} diff --git a/apis/networking/v1alpha1/firewall/filterule.go b/apis/networking/v1alpha1/firewall/filterule.go new file mode 100644 index 0000000000..21f4f54559 --- /dev/null +++ b/apis/networking/v1alpha1/firewall/filterule.go @@ -0,0 +1,39 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import "github.com/google/nftables" + +// GetName returns the name of the rule. +func (fr *FilterRule) GetName() *string { + return fr.Name +} + +// SetName sets the name of the rule. +func (fr *FilterRule) SetName(name string) { + fr.Name = &name +} + +// Add adds the rule to the chain. +func (fr *FilterRule) Add(_ *nftables.Conn, _ *nftables.Chain) error { + // TODO: implement + return nil +} + +// Equal checks if the rule is equal to the given one. +func (fr *FilterRule) Equal(_ *nftables.Rule) bool { + // TODO: implement + return true +} diff --git a/apis/networking/v1alpha1/firewall/match.go b/apis/networking/v1alpha1/firewall/match.go new file mode 100644 index 0000000000..8ad02b265c --- /dev/null +++ b/apis/networking/v1alpha1/firewall/match.go @@ -0,0 +1,167 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import ( + "fmt" + "net" + + "github.com/google/nftables" + "github.com/google/nftables/expr" +) + +func applyMatch(m *Match, rule *nftables.Rule) error { + op, err := getMatchCmpOp(m) + if err != nil { + return err + } + + switch { + case m.IP != nil: + return applyMatchIP(m, rule, op) + case m.Dev != nil: + return applyMatchDev(m, rule, op) + } + return nil +} + +func applyMatchIP(m *Match, rule *nftables.Rule, op expr.CmpOp) error { + matchIPValueType, err := GetIPValueType(m.IP.Value) + if err != nil { + return err + } + + switch matchIPValueType { + case IPValueTypeIP: + return applyMatchIPSingleIP(m, rule, op) + case IPValueTypeSubnet: + return applyMatchIPPoolSubnet(m, rule, op) + default: + return fmt.Errorf("invalid match value type %s", matchIPValueType) + } +} + +func applyMatchIPSingleIP(m *Match, rule *nftables.Rule, op expr.CmpOp) error { + posOffset, err := getMatchIPPositionOffset(m) + if err != nil { + return err + } + + rule.Exprs = append(rule.Exprs, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: posOffset, + Len: 4, + }, + &expr.Cmp{ + Op: op, + Register: 1, + Data: net.ParseIP(m.IP.Value).To4(), + }, + ) + return nil +} + +func applyMatchIPPoolSubnet(m *Match, rule *nftables.Rule, op expr.CmpOp) error { + posOffset, err := getMatchIPPositionOffset(m) + if err != nil { + return err + } + + ip, subnet, err := net.ParseCIDR(m.IP.Value) + if err != nil { + return err + } + + rule.Exprs = append(rule.Exprs, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: posOffset, + Len: 4, + }, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + Xor: []byte{0x0, 0x0, 0x0, 0x0}, + Mask: subnet.Mask, + }, + &expr.Cmp{ + Op: op, + Register: 1, + Data: ip.To4(), + }, + ) + return nil +} + +func applyMatchDev(m *Match, rule *nftables.Rule, op expr.CmpOp) error { + metakey, err := getMatchDevMetaKey(m) + if err != nil { + return err + } + + rule.Exprs = append(rule.Exprs, + &expr.Meta{ + Register: 1, + Key: metakey, + }, + &expr.Cmp{ + Op: op, + Register: 1, + Data: ifname(m.Dev.Value), + }, + ) + + return nil +} + +func getMatchCmpOp(m *Match) (expr.CmpOp, error) { + switch m.Op { + case MatchOperationEq: + return expr.CmpOpEq, nil + case MatchOperationNeq: + return expr.CmpOpNeq, nil + } + return expr.CmpOp(0), fmt.Errorf("invalid match operation %s", m.Op) +} + +func getMatchIPPositionOffset(m *Match) (uint32, error) { + switch m.IP.Position { + case MatchIPPositionSrc: + return 12, nil + case MatchIPPositionDst: + return 16, nil + } + return 0, fmt.Errorf("invalid match IP position %s", m.Dev.Position) +} + +func getMatchDevMetaKey(m *Match) (expr.MetaKey, error) { + switch m.Dev.Position { + case MatchDevPositionIn: + return expr.MetaKeyIIFNAME, nil + case MatchDevPositionOut: + return expr.MetaKeyOIFNAME, nil + } + return 0, fmt.Errorf("invalid match IP position %s", m.Dev.Position) +} + +func ifname(n string) []byte { + b := make([]byte, 16) + copy(b, n+"\x00") + return b +} diff --git a/apis/networking/v1alpha1/firewall/match_types.go b/apis/networking/v1alpha1/firewall/match_types.go new file mode 100644 index 0000000000..116f9d685b --- /dev/null +++ b/apis/networking/v1alpha1/firewall/match_types.go @@ -0,0 +1,79 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +// MatchOperation is the operation of the match. +type MatchOperation string + +const ( + // MatchOperationEq is the operation of the match. + MatchOperationEq MatchOperation = "eq" + // MatchOperationNeq is the operation of the match. + MatchOperationNeq MatchOperation = "neq" +) + +// MatchIPPosition is the position of the IP in the packet. +type MatchIPPosition string + +const ( + // MatchIPPositionSrc is the position of the IP in the packet. + MatchIPPositionSrc MatchIPPosition = "src" + // MatchIPPositionDst is the position of the IP in the packet. + MatchIPPositionDst MatchIPPosition = "dst" +) + +// MatchDevPosition is the position of the device in the packet. +type MatchDevPosition string + +const ( + // MatchDevPositionIn is the position of the device in the packet. + MatchDevPositionIn MatchDevPosition = "in" + // MatchDevPositionOut is the position of the device in the packet. + MatchDevPositionOut MatchDevPosition = "out" +) + +// MatchIP is an IP to be matched. +// +kubebuilder:object:generate=true +type MatchIP struct { + // Value is the IP or a SUbnet to be matched. + Value string `json:"value"` + // Position is the position of the IP in the packet. + // +kubebuilder:validation:Enum=src;dst + Position MatchIPPosition `json:"position"` +} + +// MatchDev is a device to be matched. +// +kubebuilder:object:generate=true +type MatchDev struct { + // Value is the name of the device to be matched. + Value string `json:"value"` + // Position is the source device of the packet. + // +kubebuilder:validation:Enum=in;out + Position MatchDevPosition `json:"position"` +} + +// Match is a match to be applied to a rule. +// +kubebuilder:object:generate=true +// +kubebuilder:validation:MaxProperties=2 +// +kubebuilder:validation:MinProperties=2 +type Match struct { + // Op is the operation of the match. + // +kubebuilder:validation:Enum=eq;neq + Op MatchOperation `json:"op"` + // IP contains the options to match an IP or a Subnet. + IP *MatchIP `json:"ip,omitempty"` + // Dev contains the options to match a device. + Dev *MatchDev `json:"dev,omitempty"` +} diff --git a/apis/networking/v1alpha1/firewall/natrule.go b/apis/networking/v1alpha1/firewall/natrule.go new file mode 100644 index 0000000000..4abfbda48c --- /dev/null +++ b/apis/networking/v1alpha1/firewall/natrule.go @@ -0,0 +1,177 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/google/nftables/userdata" +) + +// GetName returns the name of the rule. +func (nr *NatRule) GetName() *string { + return nr.Name +} + +// SetName sets the name of the rule. +func (nr *NatRule) SetName(name string) { + nr.Name = &name +} + +// Add adds the rule to the chain. +func (nr *NatRule) Add(nftconn *nftables.Conn, chain *nftables.Chain) error { + rule, err := forgeNatRule(nr, chain) + if err != nil { + return err + } + + nftconn.AddRule(rule) + return nil +} + +// Equal checks if the rule is equal to the given one. +func (nr *NatRule) Equal(currentrule *nftables.Rule) bool { + currentrule.Chain.Table = currentrule.Table + newrule, err := forgeNatRule(nr, currentrule.Chain) + if err != nil { + return false + } + if len(currentrule.Exprs) != len(newrule.Exprs) { + return false + } + for i := range currentrule.Exprs { + currentbytes, err := expr.Marshal(byte(currentrule.Table.Family), currentrule.Exprs[i]) + if err != nil { + return false + } + newbytes, err := expr.Marshal(byte(newrule.Table.Family), newrule.Exprs[i]) + if err != nil { + return false + } + if !bytes.Equal(currentbytes, newbytes) { + return false + } + } + return true +} + +func forgeNatRule(nr *NatRule, chain *nftables.Chain) (*nftables.Rule, error) { + rule := &nftables.Rule{ + Table: chain.Table, + Chain: chain, + UserData: userdata.AppendString([]byte{}, userdata.TypeComment, *nr.Name), + } + + for i := range nr.Match { + if err := applyMatch(&nr.Match[i], rule); err != nil { + return nil, err + } + } + + if err := applyNatRule(nr, rule); err != nil { + return nil, err + } + + return rule, nil +} + +func applyNatRule(nr *NatRule, rule *nftables.Rule) error { + ipType, err := GetIPValueType(nr.To) + if err != nil { + return err + } + + natType, err := getNatRuleType(nr) + if err != nil { + return err + } + + switch ipType { + case IPValueTypeIP: + return applyNatIP(nr.To, natType, rule) + case IPValueTypeSubnet: + return applyNatSubnet(nr.To, natType, rule) + default: + return fmt.Errorf("invalid ip value %s", nr.To) + } +} + +func applyNatIP(ip string, natType expr.NATType, rule *nftables.Rule) error { + ipNet := net.ParseIP(ip) + if ipNet == nil { + return fmt.Errorf("invalid ip %s", ip) + } + + rule.Exprs = append(rule.Exprs, + &expr.Immediate{ + Register: 1, + Data: ipNet.To4(), + }, + &expr.NAT{ + Type: natType, + RegAddrMin: 1, + Family: uint32(rule.Table.Family), + }) + return nil +} + +func applyNatSubnet(ip string, natType expr.NATType, rule *nftables.Rule) error { + _, subnet, err := net.ParseCIDR(ip) + if err != nil { + return err + } + + mask := binary.BigEndian.Uint32(subnet.Mask) + start := binary.BigEndian.Uint32(subnet.IP) + + // find the final address + lastIP := make(net.IP, 4) + binary.BigEndian.PutUint32(lastIP, (start&mask)|(mask^0xffffffff)) + + rule.Exprs = append(rule.Exprs, + &expr.Immediate{ + Register: 1, + Data: subnet.IP, + }, + &expr.Immediate{ + Register: 2, + Data: lastIP, + }, + &expr.NAT{ + Type: natType, + RegAddrMin: 1, + RegAddrMax: 2, + Prefix: true, + Family: uint32(rule.Table.Family), + }, + ) + return nil +} + +func getNatRuleType(natrule *NatRule) (expr.NATType, error) { + switch natrule.NatType { + case NatTypeDestination: + return expr.NATTypeDestNAT, nil + case NatTypeSource: + return expr.NATTypeSourceNAT, nil + default: + return expr.NATType(0), fmt.Errorf("invalid nat type %s", natrule.NatType) + } +} diff --git a/apis/networking/v1alpha1/firewall/natrule_types.go b/apis/networking/v1alpha1/firewall/natrule_types.go new file mode 100644 index 0000000000..55479c2fca --- /dev/null +++ b/apis/networking/v1alpha1/firewall/natrule_types.go @@ -0,0 +1,43 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +// NatType is the type of the NAT rule. +type NatType string + +const ( + // NatTypeDestination is the type of the NAT rule. + NatTypeDestination NatType = "dnat" + // NatTypeSource is the type of the NAT rule. + NatTypeSource NatType = "snat" +) + +var _ Rule = &NatRule{} + +// NatRule is a rule to be applied to a NAT chain. +// +kubebuilder:object:generate=true +type NatRule struct { + // Name is the name of the rule. + Name *string `json:"name,omitempty"` + // Match is the match to be applied to the rule. + // They can be multiple and they are applied with an AND operator. + // Using multiple ip matches with same position or + Match []Match `json:"match"` + // NatType is the type of the NAT rule. + // +kubebuilder:validation:Enum=dnat;snat + NatType NatType `json:"natType"` + // To is the IP to be used for the NAT translation. + To string `json:"to"` +} diff --git a/apis/networking/v1alpha1/firewall/routerule.go b/apis/networking/v1alpha1/firewall/routerule.go new file mode 100644 index 0000000000..ea8dee06ab --- /dev/null +++ b/apis/networking/v1alpha1/firewall/routerule.go @@ -0,0 +1,39 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import "github.com/google/nftables" + +// GetName returns the name of the rule. +func (rr *RouteRule) GetName() *string { + return rr.Name +} + +// SetName sets the name of the rule. +func (rr *RouteRule) SetName(name string) { + rr.Name = &name +} + +// Add adds the rule to the chain. +func (rr *RouteRule) Add(_ *nftables.Conn, _ *nftables.Chain) error { + // TODO: implement + return nil +} + +// Equal checks if the rule is equal to the given one. +func (rr *RouteRule) Equal(_ *nftables.Rule) bool { + // TODO: implement + return true +} diff --git a/apis/networking/v1alpha1/firewall/routerule_types.go b/apis/networking/v1alpha1/firewall/routerule_types.go new file mode 100644 index 0000000000..8ce59bbbae --- /dev/null +++ b/apis/networking/v1alpha1/firewall/routerule_types.go @@ -0,0 +1,24 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +var _ Rule = &RouteRule{} + +// RouteRule is a rule to be applied to a route chain. +// +kubebuilder:object:generate=true +type RouteRule struct { + // Name is the name of the rule. + Name *string `json:"name,omitempty"` +} diff --git a/apis/networking/v1alpha1/firewall/rule_types.go b/apis/networking/v1alpha1/firewall/rule_types.go new file mode 100644 index 0000000000..b419f356ae --- /dev/null +++ b/apis/networking/v1alpha1/firewall/rule_types.go @@ -0,0 +1,40 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewall + +import ( + "github.com/google/nftables" +) + +// Rule is a rule to be applied to a chain. +type Rule interface { + GetName() *string + SetName(string) + Add(nftconn *nftables.Conn, chain *nftables.Chain) error + Equal(rule *nftables.Rule) bool +} + +// RulesSet is a set of rules to be applied to a chain. +// +kubebuilder:object:generate=true +// +kubebuilder:validation:MaxProperties=1 +// +kubebuilder:validation:MinProperties=1 +type RulesSet struct { + // NatRules is a list of rules to be applied to the chain. + NatRules []NatRule `json:"natRules,omitempty"` + // FilterRules is a list of rules to be applied to the chain. + FilterRules []FilterRule `json:"filterRules,omitempty"` + // RouteRules is a list of rules to be applied to the chain. + RouteRules []RouteRule `json:"routeRules,omitempty"` +} diff --git a/apis/networking/v1alpha1/firewall/rules_types.go b/apis/networking/v1alpha1/firewall/rules_types.go deleted file mode 100644 index 06a22f97bc..0000000000 --- a/apis/networking/v1alpha1/firewall/rules_types.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2019-2023 The Liqo Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package firewall - -import ( - "github.com/google/nftables" - "github.com/google/nftables/expr" - "github.com/google/nftables/userdata" -) - -// Rule is a rule to be applied to a chain. -type Rule interface { - GetName() *string - SetName(string) - Add(nftconn *nftables.Conn, chain *nftables.Chain) - Replace(nftconn *nftables.Conn, chain *nftables.Chain, handle uint64) - Equal(rule *nftables.Rule) bool -} - -var _ Rule = &NatRule{} - -// NatRule is a rule to be applied to a NAT chain. -// +kubebuilder:object:generate=true -type NatRule struct { - Name *string `json:"name,omitempty"` -} - -// GetName returns the name of the rule. -func (nr *NatRule) GetName() *string { - return nr.Name -} - -// SetName sets the name of the rule. -func (nr *NatRule) SetName(name string) { - nr.Name = &name -} - -// Add adds the rule to the chain. -func (nr *NatRule) Add(nftconn *nftables.Conn, chain *nftables.Chain) { - AddPlaceHolderRule(nftconn, chain, *nr.Name, 0) -} - -// Replace replaces the rule in the chain. -func (nr *NatRule) Replace(nftconn *nftables.Conn, chain *nftables.Chain, handle uint64) { - AddPlaceHolderRule(nftconn, chain, *nr.Name, handle) -} - -// Equal checks if the rule is equal to the given one. -func (nr *NatRule) Equal(_ *nftables.Rule) bool { - return true -} - -var _ Rule = &FilterRule{} - -// FilterRule is a rule to be applied to a filter chain. -// +kubebuilder:object:generate=true -type FilterRule struct { - Name *string `json:"name,omitempty"` -} - -// GetName returns the name of the rule. -func (fr *FilterRule) GetName() *string { - return fr.Name -} - -// SetName sets the name of the rule. -func (fr *FilterRule) SetName(name string) { - fr.Name = &name -} - -// Add adds the rule to the chain. -func (fr *FilterRule) Add(nftconn *nftables.Conn, chain *nftables.Chain) { - AddPlaceHolderRule(nftconn, chain, *fr.Name, 0) -} - -// Replace replaces the rule in the chain. -func (fr *FilterRule) Replace(nftconn *nftables.Conn, chain *nftables.Chain, handle uint64) { - AddPlaceHolderRule(nftconn, chain, *fr.Name, handle) -} - -// Equal checks if the rule is equal to the given one. -func (fr *FilterRule) Equal(_ *nftables.Rule) bool { - return true -} - -var _ Rule = &RouteRule{} - -// RouteRule is a rule to be applied to a route chain. -// +kubebuilder:object:generate=true -type RouteRule struct { - Name *string `json:"name,omitempty"` -} - -// GetName returns the name of the rule. -func (rr *RouteRule) GetName() *string { - return rr.Name -} - -// SetName sets the name of the rule. -func (rr *RouteRule) SetName(name string) { - rr.Name = &name -} - -// Add adds the rule to the chain. -func (rr *RouteRule) Add(nftconn *nftables.Conn, chain *nftables.Chain) { - AddPlaceHolderRule(nftconn, chain, *rr.Name, 0) -} - -// Replace replaces the rule in the chain. -func (rr *RouteRule) Replace(nftconn *nftables.Conn, chain *nftables.Chain, handle uint64) { - AddPlaceHolderRule(nftconn, chain, *rr.Name, handle) -} - -// Equal checks if the rule is equal to the given one. -func (rr *RouteRule) Equal(_ *nftables.Rule) bool { - return true -} - -// RulesSet is a set of rules to be applied to a chain. -// +kubebuilder:object:generate=true -type RulesSet struct { - // NatRules is a list of rules to be applied to the chain. - NatRules []NatRule `json:"natRules,omitempty"` - // FilterRules is a list of rules to be applied to the chain. - FilterRules []FilterRule `json:"filterRules,omitempty"` - // RouteRules is a list of rules to be applied to the chain. - RouteRules []RouteRule `json:"routeRules,omitempty"` -} - -// AddPlaceHolderRule adds a placeholder rule to the chain. -func AddPlaceHolderRule(nftconn *nftables.Conn, chain *nftables.Chain, name string, handle uint64) { - nftconn.AddRule(&nftables.Rule{ - Table: chain.Table, - Chain: chain, - UserData: userdata.AppendString([]byte{}, userdata.TypeComment, name), - Handle: handle, - Exprs: []expr.Any{ - &expr.Verdict{ - // [ immediate reg 0 drop ] - Kind: expr.VerdictAccept, - }, - }, - }) -} diff --git a/apis/networking/v1alpha1/firewall/zz_generated.deepcopy.go b/apis/networking/v1alpha1/firewall/zz_generated.deepcopy.go index 8d664222d8..0cd8940ace 100644 --- a/apis/networking/v1alpha1/firewall/zz_generated.deepcopy.go +++ b/apis/networking/v1alpha1/firewall/zz_generated.deepcopy.go @@ -81,6 +81,61 @@ func (in *FilterRule) DeepCopy() *FilterRule { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Match) DeepCopyInto(out *Match) { + *out = *in + if in.IP != nil { + in, out := &in.IP, &out.IP + *out = new(MatchIP) + **out = **in + } + if in.Dev != nil { + in, out := &in.Dev, &out.Dev + *out = new(MatchDev) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Match. +func (in *Match) DeepCopy() *Match { + if in == nil { + return nil + } + out := new(Match) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchDev) DeepCopyInto(out *MatchDev) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchDev. +func (in *MatchDev) DeepCopy() *MatchDev { + if in == nil { + return nil + } + out := new(MatchDev) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchIP) DeepCopyInto(out *MatchIP) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchIP. +func (in *MatchIP) DeepCopy() *MatchIP { + if in == nil { + return nil + } + out := new(MatchIP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NatRule) DeepCopyInto(out *NatRule) { *out = *in @@ -89,6 +144,13 @@ func (in *NatRule) DeepCopyInto(out *NatRule) { *out = new(string) **out = **in } + if in.Match != nil { + in, out := &in.Match, &out.Match + *out = make([]Match, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NatRule. diff --git a/deployments/liqo/charts/liqo-crds/crds/networking.liqo.io_firewallconfigurations.yaml b/deployments/liqo/charts/liqo-crds/crds/networking.liqo.io_firewallconfigurations.yaml index 4729f8e3a2..f3bf65f114 100644 --- a/deployments/liqo/charts/liqo-crds/crds/networking.liqo.io_firewallconfigurations.yaml +++ b/deployments/liqo/charts/liqo-crds/crds/networking.liqo.io_firewallconfigurations.yaml @@ -82,6 +82,8 @@ spec: rules: description: Rules is a list of rules to be applied to the chain. + maxProperties: 1 + minProperties: 1 properties: filterRules: description: FilterRules is a list of rules to be applied @@ -91,6 +93,7 @@ spec: a filter chain. properties: name: + description: Name is the name of the rule. type: string type: object type: array @@ -101,8 +104,83 @@ spec: description: NatRule is a rule to be applied to a NAT chain. properties: + match: + description: Match is the match to be applied + to the rule. They can be multiple and they are + applied with an AND operator. Using multiple + ip matches with same position or + items: + description: Match is a match to be applied + to a rule. + maxProperties: 2 + minProperties: 2 + properties: + dev: + description: Dev contains the options to + match a device. + properties: + position: + description: Position is the source + device of the packet. + enum: + - in + - out + type: string + value: + description: Value is the name of the + device to be matched. + type: string + required: + - position + - value + type: object + ip: + description: IP contains the options to + match an IP or a Subnet. + properties: + position: + description: Position is the position + of the IP in the packet. + enum: + - src + - dst + type: string + value: + description: Value is the IP or a SUbnet + to be matched. + type: string + required: + - position + - value + type: object + op: + description: Op is the operation of the + match. + enum: + - eq + - neq + type: string + required: + - op + type: object + type: array name: + description: Name is the name of the rule. type: string + natType: + description: NatType is the type of the NAT rule. + enum: + - dnat + - snat + type: string + to: + description: To is the IP to be used for the NAT + translation. + type: string + required: + - match + - natType + - to type: object type: array routeRules: @@ -113,6 +191,7 @@ spec: a route chain. properties: name: + description: Name is the name of the rule. type: string type: object type: array diff --git a/go.mod b/go.mod index 18bed7f85a..6e8fafa417 100644 --- a/go.mod +++ b/go.mod @@ -272,4 +272,5 @@ replace github.com/grandcat/zeroconf => github.com/liqotech/zeroconf v1.0.1-0.20 // Waitin for PR https://github.com/coreos/go-iptables/pull/110 to be merged replace github.com/coreos/go-iptables => github.com/cheina97/go-iptables v0.0.0-20230824102241-61fc692e7548 -replace github.com/google/nftables => github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717 +// Waiting for an answer in issue https://github.com/google/nftables/issues/248 +replace github.com/google/nftables => github.com/cheina97/nftables v0.0.0-20231130113829-f658cd146651 diff --git a/go.sum b/go.sum index 093621ae54..6e8c67e013 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,8 @@ github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNS github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cheina97/go-iptables v0.0.0-20230824102241-61fc692e7548 h1:tsUrWiCxLp1cI8TB7qDmHNezZavWgxghH+m+pW+04wA= github.com/cheina97/go-iptables v0.0.0-20230824102241-61fc692e7548/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/cheina97/nftables v0.0.0-20231130113829-f658cd146651 h1:+fyNVmQQXROZWBvqhWREC96CWhUiQMHBlb/pzZf/36E= +github.com/cheina97/nftables v0.0.0-20231130113829-f658cd146651/go.mod h1:FODgEv85GcCEyoUYZ27mPWQBSU1f67bzgNu2IITA9k8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -421,8 +423,6 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717 h1:U0bCHvg4y7uTBQWiyE5iOC/R3Dw9Dve1TjIok0BfkbQ= -github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717/go.mod h1:FODgEv85GcCEyoUYZ27mPWQBSU1f67bzgNu2IITA9k8= 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= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= diff --git a/pkg/firewall/rule.go b/pkg/firewall/rule.go index 79f33d0ca9..20681ef78f 100644 --- a/pkg/firewall/rule.go +++ b/pkg/firewall/rule.go @@ -29,7 +29,9 @@ func addRules(nftconn *nftables.Conn, chain *firewallapi.Chain, nftchain *nftabl } for i := range apirules { if exist := existRule(nftrules, apirules[i]); !exist { - apirules[i].Add(nftconn, nftchain) + if err := apirules[i].Add(nftconn, nftchain); err != nil { + return err + } } } return nil diff --git a/pkg/liqo-controller-manager/webhooks/firewallconfiguration/firewallconfiguration.go b/pkg/liqo-controller-manager/webhooks/firewallconfiguration/firewallconfiguration.go index 64dcf08312..21ed37674c 100644 --- a/pkg/liqo-controller-manager/webhooks/firewallconfiguration/firewallconfiguration.go +++ b/pkg/liqo-controller-manager/webhooks/firewallconfiguration/firewallconfiguration.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" networkingv1alpha1 "github.com/liqotech/liqo/apis/networking/v1alpha1" + firewallapi "github.com/liqotech/liqo/apis/networking/v1alpha1/firewall" ) // cluster-role @@ -141,7 +142,14 @@ func (w *webhookValidate) Handle(ctx context.Context, req admission.Request) adm if err := checkRulesInChain(&chain); err != nil { return admission.Denied(err.Error()) } - } + switch *chain.Type { + case firewallapi.ChainTypeNAT: + if err := checkNatRulesInChain(&chain); err != nil { + return admission.Denied(err.Error()) + } + default: + } + } return admission.Allowed("") } diff --git a/pkg/liqo-controller-manager/webhooks/firewallconfiguration/natrule.go b/pkg/liqo-controller-manager/webhooks/firewallconfiguration/natrule.go new file mode 100644 index 0000000000..42b631f668 --- /dev/null +++ b/pkg/liqo-controller-manager/webhooks/firewallconfiguration/natrule.go @@ -0,0 +1,70 @@ +// Copyright 2019-2023 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firewallconfiguration + +import ( + "fmt" + + firewallapi "github.com/liqotech/liqo/apis/networking/v1alpha1/firewall" +) + +func checkNatRulesInChain(chain *firewallapi.Chain) error { + natrules := chain.Rules.NatRules + if natrules == nil { + return fmt.Errorf("natrules is nil") + } + for i := range natrules { + if err := checkNatRuleChainHook(*chain.Hook, &natrules[i]); err != nil { + return err + } + } + return nil +} + +func checkNatRuleChainHook(hook firewallapi.ChainHook, rule *firewallapi.NatRule) error { + switch hook { + case firewallapi.ChainHookPostrouting: + switch rule.NatType { + case firewallapi.NatTypeDestination: + return fmt.Errorf("natrule %s is DNAT that is incompatible with postrouting", *rule.GetName()) + case firewallapi.NatTypeSource: + return nil + } + case firewallapi.ChainHookPrerouting: + switch rule.NatType { + case firewallapi.NatTypeDestination: + return nil + case firewallapi.NatTypeSource: + return fmt.Errorf("natrule %s is SNAT that is incompatible with prerouting", *rule.GetName()) + } + case firewallapi.ChainHookInput: + switch rule.NatType { + case firewallapi.NatTypeDestination: + return fmt.Errorf("natrule %s is DNAT that is incompatible with input", *rule.GetName()) + case firewallapi.NatTypeSource: + return nil + } + case firewallapi.ChainHookOutput: + switch rule.NatType { + case firewallapi.NatTypeDestination: + return nil + case firewallapi.NatTypeSource: + return fmt.Errorf("natrule %s is SNAT that is incompatible with output", *rule.GetName()) + } + default: + return fmt.Errorf("hook %s is not a valid hook", hook) + } + return nil +}