diff --git a/README.md b/README.md index 0579ac7..8af0449 100644 --- a/README.md +++ b/README.md @@ -1 +1,58 @@ -# iptables-api \ No newline at end of file +# iptables-api + +Very simple API for managing local iptables chain: `APIBANLOCAL` + +Simple `GET` actions of add, remove, and flush. + +## Usage + +`./iptables-api` + +Defaults: + +* port: `8082` +* log: `/var/log/iptables-api.log` +* target: `REJECT` + +### Example with flags + +`./iptables-api -log=stdout -port=8008 -target=DROP` + +## Running as a Service (example) + +If executable is in `/usr/local/iptables-api/`... + +```bash +cat > /lib/systemd/system/iptables-api.service << EOT +[Unit] +Description=iptables-api + +[Service] +Type=simple +Restart=always +RestartSec=5s +ExecStart=/usr/local/iptables-api/iptables-api + +[Install] +WantedBy=multi-user.target +EOT +``` + +Log rotate... + +```bash +cat > /etc/logrotate.d/iptables-api << EOF +/var/log/iptables-api.log { + daily + copytruncate + rotate 12 + compress +} +EOF +``` + +## License / Warranty + +iptables-api is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version + +iptables-api is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. diff --git a/go/iptables-api.go b/go/iptables-api.go new file mode 100644 index 0000000..ee73a8c --- /dev/null +++ b/go/iptables-api.go @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2021 The Palner Group, Inc. (palner.com) + * Fred Posner (@fredposner) + * + * iptables-api is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * iptables-api is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "runtime" + + "github.com/coreos/go-iptables/iptables" + "github.com/gorilla/mux" +) + +var logFile string +var targetChain string +var APIport string + +func init() { + flag.StringVar(&targetChain, "target", "REJECT", "target chain for matching entries") + flag.StringVar(&logFile, "log", "/var/log/iptables-api.log", "location of log file or - for stdout") + flag.StringVar(&APIport, "port", "8082", "port to listen on") +} + +func main() { + // get flags + flag.Parse() + + // Open our Log + if logFile != "-" && logFile != "stdout" { + lf, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + log.Panic(err) + fmt.Fprintf(os.Stderr, "error: %v\n", err) + runtime.Goexit() + } + defer lf.Close() + + log.SetOutput(lf) + } + + log.Print("** Starting iptables-API") + log.Print("** Choose to be optimistic, it feels better.") + log.Print("** Licensed under GPLv2. See LICENSE for details.") + log.Print("** API will listen on port ", APIport) + + router := mux.NewRouter() + router.HandleFunc("/addip/{ipaddress}", addIPAddress).Methods("GET") + router.HandleFunc("/removeip/{ipaddress}", removeIPAddress).Methods("GET") + router.HandleFunc("/flushchain", flushChain).Methods("GET") + http.ListenAndServe("0.0.0.0:"+APIport, router) +} + +// Function to see if string within string +func contains(list []string, value string) bool { + for _, val := range list { + if val == value { + return true + } + } + return false +} + +func checkIPAddress(ip string) bool { + if net.ParseIP(ip) == nil { + return false + } else { + return true + } +} + +func checkIPAddressv4(ip string) bool { + if net.ParseIP(ip) == nil { + return false + } + for i := 0; i < len(ip); i++ { + switch ip[i] { + case '.': + return true + case ':': + return false + } + } + + return false +} + +func initializeIPTables(ipt *iptables.IPTables) (string, error) { + // Get existing chains from IPTABLES + originaListChain, err := ipt.ListChains("filter") + if err != nil { + return "error", fmt.Errorf("failed to read iptables: %w", err) + } + + // Search for INPUT in IPTABLES + chain := "INPUT" + if !contains(originaListChain, chain) { + return "error", errors.New("iptables does not contain expected INPUT chain") + } + + // Search for FORWARD in IPTABLES + chain = "FORWARD" + if !contains(originaListChain, chain) { + return "error", errors.New("iptables does not contain expected FORWARD chain") + } + + // Search for APIBAN in IPTABLES + chain = "APIBANLOCAL" + if contains(originaListChain, chain) { + // APIBAN chain already exists + return "chain exists", nil + } + + log.Print("IPTABLES doesn't contain APIBANLOCAL. Creating now...") + + // Add APIBAN chain + err = ipt.ClearChain("filter", chain) + if err != nil { + return "error", fmt.Errorf("failed to clear APIBANLOCAL chain: %w", err) + } + + // Add APIBAN chain to INPUT + err = ipt.Insert("filter", "INPUT", 1, "-j", chain) + if err != nil { + return "error", fmt.Errorf("failed to add APIBANLOCAL chain to INPUT chain: %w", err) + } + + // Add APIBAN chain to FORWARD + err = ipt.Insert("filter", "FORWARD", 1, "-j", chain) + if err != nil { + return "error", fmt.Errorf("failed to add APIBANLOCAL chain to FORWARD chain: %w", err) + } + + return "chain created", nil +} + +func addIPAddress(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + params := mux.Vars(r) + log.Println("processing addIPAddress", params["ipaddress"]) + + if checkIPAddressv4(params["ipaddress"]) { + log.Println(params["ipaddress"], "is a valid ip address") + } else { + log.Println(params["ipaddress"], "is not a valid ipv4 address") + http.Error(w, "{\"error\":\"only valid ipv4 address supported\"}", http.StatusBadRequest) + return + } + + // Go connect for IPTABLES + ipt, err := iptables.New() + if err != nil { + log.Println(err) + http.Error(w, "{\"error\":\"error with iptables\"}", http.StatusInternalServerError) + } + + _, err = initializeIPTables(ipt) + if err != nil { + log.Fatalln("failed to initialize IPTables:", err) + http.Error(w, "{\"error\":\"error initializing iptables\"}", http.StatusInternalServerError) + } + + err = ipt.AppendUnique("filter", "APIBANLOCAL", "-s", params["ipaddress"], "-d", "0/0", "-j", targetChain) + if err != nil { + log.Println("error adding address", err) + http.Error(w, "{\"error\":\"error adding address\"}", http.StatusBadRequest) + return + } + + io.WriteString(w, "{\"success\":\"added\"}\n") +} + +func removeIPAddress(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + params := mux.Vars(r) + log.Println("processing removeIPAddress", params["ipaddress"]) + + if checkIPAddressv4(params["ipaddress"]) { + log.Println(params["ipaddress"], "is a valid ip address") + } else { + log.Println(params["ipaddress"], "is not a valid ipv4 address") + http.Error(w, "{\"error\":\"only valid ipv4 address supported\"}", http.StatusBadRequest) + return + } + + // Go connect for IPTABLES + ipt, err := iptables.New() + if err != nil { + log.Println(err) + http.Error(w, "{\"error\":\"error with iptables\"}", http.StatusInternalServerError) + } + + _, err = initializeIPTables(ipt) + if err != nil { + log.Fatalln("failed to initialize IPTables:", err) + http.Error(w, "{\"error\":\"error initializing iptables\"}", http.StatusInternalServerError) + } + + err = ipt.DeleteIfExists("filter", "APIBANLOCAL", "-s", params["ipaddress"], "-d", "0/0", "-j", targetChain) + if err != nil { + log.Println("error removing address", err) + http.Error(w, "{\"error\":\"error removing address\"}", http.StatusBadRequest) + return + } + + io.WriteString(w, "{\"success\":\"removed\"}\n") +} + +func flushChain(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + log.Println("processing flushChain") + + // Go connect for IPTABLES + ipt, err := iptables.New() + if err != nil { + log.Println(err) + http.Error(w, "{\"error\":\"error with iptables\"}", http.StatusInternalServerError) + } + + _, err = initializeIPTables(ipt) + if err != nil { + log.Fatalln("failed to initialize IPTables:", err) + http.Error(w, "{\"error\":\"error initializing iptables\"}", http.StatusInternalServerError) + } + + err = ipt.ClearChain("filter", "APIBANLOCAL") + if err != nil { + log.Print("Flushing APIBANLOCAL chain failed. ", err.Error()) + http.Error(w, "{\"error\":\"error flushing chain\"}", http.StatusBadRequest) + } else { + log.Print("APIBANLOCAL chain flushed.") + io.WriteString(w, "{\"success\":\"flushed\"}\n") + } +} diff --git a/iptables-api b/iptables-api new file mode 100755 index 0000000..dd4d600 Binary files /dev/null and b/iptables-api differ