package nodefirewall import ( "errors" "fmt" "path/filepath" "strings" "slices" "github.com/spf13/afero" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" ) const ( // Example rule: `-i eno1 -p udp -m udp --dport 67:68 -s 172.23.1.1 -d 172.23.1.5 -j ACCEPT` ruleTemplate = "%s-p %[2]s -m %[2]s --dport %s %s%s-j %s" fwDirectory = "/etc/ien-fw/ipv4/dynamic" inputFilename = "filter-INPUT-%s.rules" outputFilename = "filter-OUTPUT-%s.rules" fileHeaderComment = "# ClusterFirewall: %s, NodeFirewall: %s, Rule: %s" ) var ( actions = map[v1ien.Action]string{ v1ien.Allow: "ACCEPT", v1ien.Deny: "LOGDROP", } directionalInterfaceFlags = map[v1ien.Direction]string{ v1ien.Input: "-i %s ", v1ien.Output: "-o %s ", } ) func (fw Plugin) writeFiles(rules []v1ien.NodeRule) ([]string, error) { fs := fw.config.Fs() files := []string{} for i := range rules { filename, err := fw.writeFile(fs, &rules[i]) if err != nil { return files, err } files = append(files, filename) } return files, nil } func (fw Plugin) writeFile(fs afero.Fs, rule *v1ien.NodeRule) (string, error) { filename, bytes := fw.generateRuleFile(rule) if err := afero.WriteFile(fs, filename, bytes, 0644); err != nil { return "", err } return filename, nil } func (fw Plugin) generateRuleFile(rule *v1ien.NodeRule) (string, []byte) { // add this back in for edge-os v1.11.0 // lines := []string{fmt.Sprintf(fileHeaderComment, fw.owner, fw.name, rule.Name)} lines := []string{} for i := range rule.Filters { lines = append(lines, fw.generateRuleFromFilter(rule, &rule.Filters[i])) } lines = append(lines, "") filename := filepath.Join(fwDirectory, fw.generateFilename(rule)) content := []byte(strings.Join(lines, "\n")) return filename, content } func (fw Plugin) generateRuleFromFilter(rule *v1ien.NodeRule, filter *v1ien.Filter) string { // rule v1ien.NodeRule, filter v1ien.Filter, action string sourceRanges := strings.Join(rule.SourceRanges, ",") destinationRanges := strings.Join(rule.DestinationRanges, ",") if sourceRanges != "" { sourceRanges = "-s " + sourceRanges + " " } if destinationRanges != "" { destinationRanges = "-d " + destinationRanges + " " } ifaceFlag, err := fw.getInterfaceFlag(rule) if err != nil { return "" // don't add rule if no interface matches specified MAC } return fmt.Sprintf(ruleTemplate, ifaceFlag, filter.IPProtocol, filter.PortRange, sourceRanges, destinationRanges, actions[filter.Action]) } func (fw Plugin) getInterfaceFlag(rule *v1ien.NodeRule) (string, error) { var iface string var err error if rule.InterfaceMAC == "" { iface = fw.defaultInterface } else { iface, err = fw.findInterfaceByMAC(rule.InterfaceMAC) if err != nil { return "", err } } return fmt.Sprintf(directionalInterfaceFlags[rule.Direction], iface), nil } func (fw Plugin) deleteFiles(files []string) error { fs := fw.config.Fs() for _, file := range files { if err := fs.Remove(file); err != nil && !errors.Is(err, afero.ErrFileNotFound) { return err } } return nil } func (fw Plugin) deleteStaleFiles(old, new []string) error { fs := fw.config.Fs() for _, file := range old { if !slices.Contains(new, file) { if err := fs.Remove(file); err != nil && !errors.Is(err, afero.ErrFileNotFound) { return err } } } return nil } func (fw Plugin) generateFilename(rule *v1ien.NodeRule) string { var filename string if rule.Direction == v1ien.Input { filename = inputFilename } else { filename = outputFilename } featureName := firstN(rule.ID, 8) return fmt.Sprintf(filename, featureName) } // take the first n chars of a string func firstN(str string, n int) string { if len(str) < n { return str } return str[:n] } func (fw Plugin) findInterfaceByMAC(macAddress string) (string, error) { netlink, err := fw.config.GetInterfaceFromHardwareAddress(macAddress) if err != nil { return "", err } return netlink.Attrs().Name, nil }