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

Improve on routemap testability and provide dmz support #43

Merged
merged 5 commits into from
Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 12 additions & 5 deletions internal/netconf/chrony.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,20 @@ func (c ChronyServiceEnabler) Enable() error {
func getDefaultRouteVRFName(kb KnowledgeBase) (string, error) {
networks := kb.GetNetworks(mn.External)
for _, network := range networks {
for _, prefix := range network.Destinationprefixes {
if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR {
vrf := fmt.Sprintf("vrf%d", *network.Vrf)
return vrf, nil
}
if containsDefaultRoute(network.Destinationprefixes) {
vrf := fmt.Sprintf("vrf%d", *network.Vrf)
return vrf, nil
}
}

return "", fmt.Errorf("there is no network providing a default (0.0.0.0/0) route")
}

func containsDefaultRoute(prefixes []string) bool {
for _, prefix := range prefixes {
if prefix == IPv4ZeroCIDR || prefix == IPv6ZeroCIDR {
return true
}
}
return false
}
201 changes: 4 additions & 197 deletions internal/netconf/frr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package netconf

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/metal-stack/metal-go/api/models"
mn "github.com/metal-stack/metal-lib/pkg/net"
Expand Down Expand Up @@ -124,217 +121,27 @@ func (v FRRValidator) Validate() error {
return exec.NewVerboseCmd("bash", "-c", vtysh, v.path).Run()
}

func getDestinationPrefixes(networks []models.V1MachineNetwork) []string {
var result []string
for _, network := range networks {
result = append(result, network.Destinationprefixes...)
}

return result
}

func getPrefixes(networks ...models.V1MachineNetwork) []string {
var result []string
for _, network := range networks {
result = append(result, network.Prefixes...)
}

return result
}

func assembleVRFs(kb KnowledgeBase) []VRF {
var result []VRF

privatePrimary := kb.getPrivatePrimaryNetwork()
networks := kb.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared, mn.External)

for _, network := range networks {
var targets []models.V1MachineNetwork
var prefixes []string

if network.Networktype == nil {
continue
}
nt := *network.Networktype
switch nt {
case mn.PrivatePrimaryUnshared:
fallthrough
case mn.PrivatePrimaryShared:
// reach out from private primary network into public networks
publicTargets := kb.GetNetworks(mn.External)
prefixes = getDestinationPrefixes(publicTargets)
targets = append(targets, publicTargets...)

// reach out from private primary network into shared private networks
privateSharedTargets := kb.GetNetworks(mn.PrivateSecondaryShared)
prefixes = append(prefixes, getPrefixes(privateSharedTargets...)...)
targets = append(targets, privateSharedTargets...)
case mn.PrivateSecondaryShared:
// reach out from private shared networks into private primary network
targets = []models.V1MachineNetwork{privatePrimary}
prefixes = getPrefixes(append(targets, network)...)
case mn.External:
// reach out from public into private and other public networks
targets = []models.V1MachineNetwork{privatePrimary}
prefixes = getPrefixes(append(targets, network)...)
}
shared := (nt == mn.PrivatePrimaryShared || nt == mn.PrivateSecondaryShared)
vrfID := network.Vrf
vrfName := "vrf" + strconv.Itoa(int(*vrfID))

prefixLists := assembleIPPrefixListsFor(vrfName, prefixes, IPPrefixListSeqSeed, kb, shared, AddressFamilyIPv4)
prefixLists = append(prefixLists, assembleIPPrefixListsFor(vrfName, prefixes, IPPrefixListSeqSeed, kb, shared, AddressFamilyIPv6)...)
i := importRulesForNetwork(kb, network)
vrf := VRF{
Identity: Identity{
ID: int(*network.Vrf),
},
VNI: int(*network.Vrf),
ImportVRFNames: vrfNamesOf(targets...),
IPPrefixLists: prefixLists,
RouteMaps: assembleRouteMapsFor(vrfName, prefixLists),
}
result = append(result, vrf)
}

return result
}

func byName(prefixLists []IPPrefixList) map[string]IPPrefixList {
byName := map[string]IPPrefixList{}
for _, prefixList := range prefixLists {
if _, isPresent := byName[prefixList.Name]; isPresent {
continue
ImportVRFNames: i.importVRFs,
IPPrefixLists: i.prefixLists(),
RouteMaps: i.routeMaps(),
}

byName[prefixList.Name] = prefixList
}

return byName
}

func assembleRouteMapsFor(vrfName string, prefixLists []IPPrefixList) []RouteMap {
var result []RouteMap

order := RouteMapOrderSeed
byName := byName(prefixLists)

names := []string{}
for n := range byName {
names = append(names, n)
}
sort.Sort(sort.Reverse(sort.StringSlice(names)))

for _, n := range names {
prefixList := byName[n]
match := fmt.Sprintf("match %s address prefix-list %s", prefixList.AddressFamily, n)
entries := []string{match}
if strings.HasSuffix(n, IPPrefixListNoExportSuffix) {
entries = append(entries, "set community additive no-export")
}

routeMap := RouteMap{
Name: routeMapName(vrfName),
Policy: Permit.String(),
Order: order,
Entries: entries,
}
order += RouteMapOrderSeed

result = append(result, routeMap)
}

routeMap := RouteMap{
Name: routeMapName(vrfName),
Policy: Deny.String(),
Order: order,
}

result = append(result, routeMap)

return result
}

func routeMapName(vrfName string) string {
return vrfName + "-import-map"
}

func vrfNamesOf(networks ...models.V1MachineNetwork) []string {
var result []string

for _, n := range networks {
vrf := fmt.Sprintf("vrf%d", *n.Vrf)
result = append(result, vrf)
}

return result
}

func buildIPPrefixListSpecs(seq int, prefix netaddr.IPPrefix) []string {
var result []string

spec := fmt.Sprintf("seq %d %s %s", seq, Permit, prefix)
if prefix.Bits != 0 {
spec += fmt.Sprintf(" le %d", prefix.IP.BitLen())
}

result = append(result, spec)

return result
}

func assembleIPPrefixListsFor(vrfName string, prefixes []string, seed int, kb KnowledgeBase, shared bool, af AddressFamily) []IPPrefixList {
var result []IPPrefixList

private := kb.getPrivatePrimaryNetwork()

for _, prefix := range prefixes {
if len(prefix) == 0 {
continue
}

p, err := netaddr.ParseIPPrefix(prefix)
if err != nil {
continue
}

if af == AddressFamilyIPv4 && !p.IP.Is4() {
continue
}

if af == AddressFamilyIPv6 && !p.IP.Is6() {
continue
}

specs := buildIPPrefixListSpecs(seed, p)

for _, spec := range specs {
name := namePrefixList(vrfName, private, p, shared, af)
prefixList := IPPrefixList{
Name: name,
Spec: spec,
AddressFamily: af,
}
result = append(result, prefixList)
}

seed += len(specs)
}

return result
}

func namePrefixList(vrfName string, private models.V1MachineNetwork, prefix netaddr.IPPrefix, shared bool, af AddressFamily) string {
name := fmt.Sprintf("%s-import-prefixes", vrfName)
if af != AddressFamilyIPv4 {
name += fmt.Sprintf("-%s", af)
}

for _, privatePrefix := range private.Prefixes {
if privatePrefix == prefix.String() && !shared {
// tenant private network ip addresses must not be visible in the public VRFs to avoid blown up routing tables
name += IPPrefixListNoExportSuffix
}
}

return name
}
14 changes: 14 additions & 0 deletions internal/netconf/frr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ func TestFrrConfigApplier(t *testing.T) {
configuratorType: Firewall,
tpl: TplFirewallFRR,
},
{
name: "standard firewall with private primary unshared network, private secondary shared dmz network, internet and mpls",
input: "testdata/firewall_dmz.yaml",
expectedOutput: "testdata/frr.conf.firewall_dmz",
configuratorType: Firewall,
tpl: TplFirewallFRR,
},
{
name: "standard firewall with private primary unshared network, private secondary shared dmz network",
input: "testdata/firewall_dmz_app.yaml",
expectedOutput: "testdata/frr.conf.firewall_dmz_app",
configuratorType: Firewall,
tpl: TplFirewallFRR,
},
{
name: "firewall with private primary unshared ipv6 network, private secondary shared ipv4 network, ipv6 internet and ipv4 mpls",
input: "testdata/firewall_ipv6.yaml",
Expand Down
10 changes: 9 additions & 1 deletion internal/netconf/knowledgebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,15 @@ func (kb KnowledgeBase) Validate(kind BareMetalType) error {
}

func (kb KnowledgeBase) containsAnyPublicNetwork() bool {
return len(kb.GetNetworks(mn.External)) > 0
if len(kb.GetNetworks(mn.External)) > 0 {
return true
}
for _, n := range kb.Networks {
if isDMZNetwork(n) {
return true
}
}
return false
}

func (kb KnowledgeBase) containsSinglePrivatePrimary() bool {
Expand Down
14 changes: 13 additions & 1 deletion internal/netconf/nftables.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package netconf
import (
"fmt"

"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-networker/pkg/exec"
"inet.af/netaddr"

Expand Down Expand Up @@ -53,12 +54,23 @@ func NewNftablesConfigApplier(kb KnowledgeBase, validator net.Validator) net.App
return net.NewNetworkApplier(data, validator, nil)
}

func isDMZNetwork(n models.V1MachineNetwork) bool {
return *n.Networktype == mn.PrivateSecondaryShared && containsDefaultRoute(n.Destinationprefixes)
}

func getSNAT(kb KnowledgeBase) []SNAT {
var result []SNAT

private := kb.getPrivatePrimaryNetwork()
networks := kb.GetNetworks(mn.PrivatePrimaryUnshared, mn.PrivatePrimaryShared, mn.PrivateSecondaryShared, mn.External)

privatePfx := private.Prefixes
for _, n := range kb.Networks {
if isDMZNetwork(n) {
privatePfx = append(privatePfx, n.Prefixes...)
}
}

for _, n := range networks {
if n.Nat != nil && !*n.Nat {
continue
Expand All @@ -68,7 +80,7 @@ func getSNAT(kb KnowledgeBase) []SNAT {
cmt := fmt.Sprintf("snat (networkid: %s)", *n.Networkid)
svi := fmt.Sprintf("vlan%d", *n.Vrf)

for _, p := range private.Prefixes {
for _, p := range privatePfx {
ipprefix, err := netaddr.ParseIPPrefix(p)
if err != nil {
continue
Expand Down
54 changes: 37 additions & 17 deletions internal/netconf/nftables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,45 @@ func TestCompileNftRules(t *testing.T) {
assert := assert.New(t)

tests := []struct {
input string
expected string
template string
}{
{expected: "testdata/nftrules", template: TplNftables},
{
input: "testdata/firewall.yaml",
expected: "testdata/nftrules",
},
{
input: "testdata/firewall_dmz.yaml",
expected: "testdata/nftrules_dmz",
},
{
input: "testdata/firewall_dmz_app.yaml",
expected: "testdata/nftrules_dmz_app",
},
{
input: "testdata/firewall_ipv6.yaml",
expected: "testdata/nftrules_ipv6",
},
{
input: "testdata/firewall_shared.yaml",
expected: "testdata/nftrules_shared",
},
}

for _, test := range tests {
expected, err := ioutil.ReadFile(test.expected)
assert.NoError(err)

kb := NewKnowledgeBase("testdata/firewall.yaml")
assert.NoError(err)

a := NewNftablesConfigApplier(kb, nil)
b := bytes.Buffer{}

tpl := mustParseTpl(test.template)
err = a.Render(&b, *tpl)
assert.NoError(err)
assert.Equal(string(expected), b.String())
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
expected, err := ioutil.ReadFile(tt.expected)
assert.NoError(err)

kb := NewKnowledgeBase(tt.input)
assert.NoError(err)

a := NewNftablesConfigApplier(kb, nil)
b := bytes.Buffer{}

tpl := mustParseTpl(TplNftables)
err = a.Render(&b, *tpl)
assert.NoError(err)
assert.Equal(string(expected), b.String())
})
}
}
Loading