// This file copyright 2024-2025, PBX.com LLC package main import ( "errors" "os/exec" "strconv" "strings" "time" // "kazoo_firewall_agent/cache" ) func firewall(action, ipaddr string, inputports interface{}) (err error) { return firewallWithTimeout(action, ipaddr, inputports, appconf.CacheTimeout) } func firewallWithTimeout(action, ipaddr string, inputports interface{}, timeout int64) (err error) { switch p := inputports.(type) { case []interface{}: for _, porti := range p { switch port := porti.(type) { case string: firewallAction(action, ipaddr, port, timeout) case float64: firewallAction(action, ipaddr, strconv.Itoa(int(port)), timeout) case int64: firewallAction(action, ipaddr, strconv.Itoa(int(port)), timeout) case int: firewallAction(action, ipaddr, strconv.Itoa(port), timeout) } } case []string: for _, port := range p { firewallAction(action, ipaddr, port, timeout) } case float64: firewallAction(action, ipaddr, strconv.Itoa(int(p)), timeout) case string: splitPorts := strings.Split(p, ",") for _, port := range splitPorts { firewallAction(action, ipaddr, port, timeout) } case nil: firewallAction(action, ipaddr, "", timeout) default: } return nil } func parsePortProto(p string) PortProto { var pp PortProto pparsed := strings.Split(p, "/") //"8443/tcp" --> [8443, tcp] if len(pparsed) < 1 { return pp } pp.Port = pparsed[0] pp.Proto = "tcp" if len(pparsed) > 1 { switch pparsed[1] { case "tcp": pp.Proto = "tcp" case "TCP": pp.Proto = "tcp" case "udp": pp.Proto = "udp" case "UDP": pp.Proto = "udp" default: } } return pp } func firewallAction(action, ipaddr, portstring string, timeout int64) (err error) { switch action { case "firewall_add": err = firewallAdd(ipaddr, portstring, timeout) case "firewall_remove": err = firewallDelete(ipaddr, portstring) } return err } func firewallAdd(ipaddr, portstring string, timeout int64) error { var pp PortProto pp = parsePortProto(portstring) zone := determineZone(ipaddr, pp) if len(pp.Port) > 0 { logit(5, "Firewall adding to zone '"+zone+"': "+ipaddr+":"+pp.Port+"/"+pp.Proto) } else { logit(5, "Firewall adding to zone '"+zone+"': "+ipaddr) } //ipcache.Set(ipaddr+":"+pp.Port+"/"+pp.Proto, zone, time.Duration(appconf.CacheTimeout) * time.Second) if len(pp.Port) > 0 { err := maybeAddPort(zone, pp, false) if err != nil { logit(3, err.Error()) return err } } //if it's already cached, then don't add it, just refresh the expiry time and return _, alreadyExists := ipcache.Get(ipaddr) if alreadyExists { ipcache.Set(ipaddr, zone, time.Duration(timeout)*time.Second) return nil } fwOutput, err := exec.Command("firewall-cmd", "--zone="+zone, "--add-source="+ipaddr).CombinedOutput() if err != nil { logit(3, "Error executing firewall-cmd: "+err.Error()+" OUTPUT: "+string(fwOutput)) return errors.New("Error executing current firewall-cmd: " + err.Error()) } ipcache.Set(ipaddr, zone, time.Duration(timeout)*time.Second) // maybe add permanently also if shouldAddPermanently() { fwOutput, err = exec.Command("firewall-cmd", "--permanent", "--zone="+zone, "--add-source="+ipaddr).CombinedOutput() if err != nil { logit(3, "Error executing firewall-cmd --permanent: "+err.Error()+" OUTPUT: "+string(fwOutput)) return errors.New("Error executing permanent firewall-cmd: " + err.Error()) } if len(pp.Port) > 0 { err := maybeAddPort(zone, pp, true) if err != nil { logit(3, err.Error()) return err } } } return nil } func shouldAddPermanently() bool { if appconf.ServerType == "ephemeral" || appconf.ServerType == "freeswitch" { return false } return true } func firewallDelete(ipaddr, portstring string) error { var pp PortProto pp = parsePortProto(portstring) zone := determineZone(ipaddr, pp) logit(5, "Firewall removing from zone '"+zone+"': "+ipaddr) ipcache.Remove(ipaddr) //ipcache.Remove(ipaddr+":"+pp.Port+"/"+pp.Proto) fwOutput, err := exec.Command("firewall-cmd", "--zone="+zone, "--remove-source="+ipaddr).CombinedOutput() if err != nil { logit(3, "Error executing firewall-cmd: "+err.Error()+" OUTPUT: "+string(fwOutput)) return errors.New("Error executing firewall-cmd: " + err.Error()) } // maybe remove permanently also if shouldAddPermanently() { fwOutput, err = exec.Command("firewall-cmd", "--permanent", "--zone="+zone, "--remove-source="+ipaddr).CombinedOutput() if err != nil { logit(3, "Error executing firewall-cmd --permanent: "+err.Error()+" OUTPUT: "+string(fwOutput)) return errors.New("Error executing firewall-cmd --permanent: " + err.Error()) } } return nil } func firewallDeleteCacheOnly(ipaddr, portstring string) error { var pp PortProto pp = parsePortProto(portstring) zone := determineZone(ipaddr, pp) logit(5, "removing from cache: '"+zone+"' "+ipaddr) ipcache.Remove(ipaddr) return nil } func maybeAddPort(zone string, pp PortProto, permanent bool) error { var found bool ports, err := getZonePorts(zone) if err != nil { return err } for _, port := range ports { if port == pp.Port+"/"+pp.Proto { found = true return nil } } if !found { return addZonePort(zone, pp.Port+"/"+pp.Proto, permanent) } return nil } func addZonePort(zone, port string, permanent bool) error { cmd := exec.Command("firewall-cmd", "--zone="+zone, "--add-port="+port) if permanent { cmd = exec.Command("firewall-cmd", "--zone="+zone, "--add-port="+port, "--permanent") } output, err := cmd.CombinedOutput() if err != nil { return errors.New(string(output) + " | " + err.Error()) } ipcache.Remove("zone_" + zone) return nil } func getZonePorts(zone string) ([]string, error) { logit(7, "Get ports in zone: '"+zone+"'...") ports, found := ipcache.Get("zone_" + zone) //check cache first if found { logit(7, "Ports for zone: '"+zone+"' are in cache; returning.") return strings.Split(ports, " "), nil } logit(7, "Ports for zone: '"+zone+"' are not cached; getting from firewalld.") cmd := exec.Command("firewall-cmd", "--zone="+zone, "--list-ports") output, err := cmd.CombinedOutput() outputArr := strings.Split(string(output), " ") ipcache.Set("zone_"+zone, string(output), time.Duration(appconf.CacheTimeout)*time.Second) return outputArr, err } func maybeSetupZone(zone string) error { cmd := exec.Command("firewall-cmd", "--zone="+zone, "--list-ports") output, err := cmd.CombinedOutput() if err != nil { logit(3, "Error querying zone '"+zone+"': "+err.Error()+" : "+string(output)+" : Zone may not exist. Trying to create...") cmd = exec.Command("firewall-cmd", "--new-zone="+zone, "--permanent") output, err = cmd.CombinedOutput() if err != nil { logit(3, "Error creating zone '"+zone+"': "+err.Error()+" : "+string(output)) return err } } err = maybeAddDefaultPorts(zone) if err != nil { return err } return reload() } func maybeAddDefaultPorts(zone string) error { var ports []string if len(appconf.Ports) > 0 { ports = strings.Split(appconf.Ports, ",") for _, port := range ports { pp := parsePortProto(port) err := maybeAddPort(zone, pp, true) if err != nil { logit(3, err.Error()) return err } } } return nil } func reload() error { cmd := exec.Command("firewall-cmd", "--reload") output, err := cmd.CombinedOutput() if err != nil { logit(3, "Unable to reload firewalld! Output: "+string(output)+"Error: "+err.Error()) } logit(5, "Reload firewalld: "+string(output)) return err } func determineZone(ipaddr string, pp PortProto) string { var zone string //logit(7, "Determine zone for ipaddr '"+ipaddr+"'...") //do stuff zone = appconf.FirewallZone return zone }