internal/firewall/ufw.go (99 lines of code) (raw):

package firewall import ( "bufio" "fmt" "os/exec" "regexp" "strings" ) const ( ufwBinary = "ufw" actionAllow = "ALLOW" ) var ( ufwActiveRegex = regexp.MustCompile(`.*Status: active*`) ufwStatusRuleRegex = regexp.MustCompile(`(\d+)\s*/(\w+)\s+(ALLOW|DENY)\s+Anywhere`) ) type UncomplicatedFireWall struct { binPath string rules []rule } type rule struct { port string protocol string action string } func NewUncomplicatedFirewall() Manager { path, _ := exec.LookPath(ufwBinary) return &UncomplicatedFireWall{ binPath: path, } } // IsEnabled returns true if ufw is enabled and running on the node func (ufw *UncomplicatedFireWall) IsEnabled() (bool, error) { // Check if ufw is installed if ufw.binPath != "" { statusCmd := exec.Command(ufw.binPath, "status") out, err := statusCmd.CombinedOutput() if err != nil { return false, fmt.Errorf("failed to get status of uncomplicated firewall: %s, error: %v", out, err) } // Check for active status output if match := ufwActiveRegex.MatchString(string(out)); match { return true, nil } } return false, nil } // AllowTcpPort adds a rule to the firewall to open input port func (ufw *UncomplicatedFireWall) AllowTcpPort(port string) error { portAddCmd := exec.Command(ufw.binPath, "allow", fmt.Sprintf("%s/tcp", port)) out, err := portAddCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to allow port %s in firewall: %s, error: %v", port, out, err) } return nil } // AllowTcpPortRange adds a rule to the firewall to open the range of input port func (ufw *UncomplicatedFireWall) AllowTcpPortRange(startPort, endPort string) error { portAddCmd := exec.Command(ufw.binPath, "allow", fmt.Sprintf("%s:%s/tcp", startPort, endPort)) out, err := portAddCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to allow ports in firewall: %s, error: %v", out, err) } return nil } // FlushRules flushes the rules and reloads the firewall to enforce the rules func (ufw *UncomplicatedFireWall) FlushRules() error { // UFW activates the rules the moment its added, there is no need to flush them out to disk explicitly return nil } // IsPortOpen returns is port/protocol is open on the firewall // UFW doesn't have a way to query a port, so this function refreshes the active rules // maintained by firewall and checks if port/protocol is allowed. func (ufw *UncomplicatedFireWall) IsPortOpen(port, protocol string) (bool, error) { if len(ufw.rules) == 0 { if err := ufw.refreshActiveRules(); err != nil { return false, err } } for _, rule := range ufw.rules { if rule.port == port && rule.protocol == protocol && rule.action == actionAllow { return true, nil } } return false, nil } func (ufw *UncomplicatedFireWall) refreshActiveRules() error { statusCmd := exec.Command(ufw.binPath, "status") out, err := statusCmd.CombinedOutput() if err != nil { return err } scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { ruleLine := scanner.Text() matches := ufwStatusRuleRegex.FindStringSubmatch(ruleLine) if len(matches) > 0 { ufw.rules = append(ufw.rules, rule{ port: matches[1], protocol: matches[2], action: matches[3], }) } } if err := scanner.Err(); err != nil { return err } return nil }