From 3f469e92848e89844626abbf56adbe63d734b97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=A5=A5=E8=B6=85?= Date: Sat, 6 Feb 2021 17:34:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0IP=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/events/utils.go | 2 +- internal/iplibrary/action_base.go | 25 ++ internal/iplibrary/action_errors.go | 22 ++ internal/iplibrary/action_firewalld.go | 147 +++++++++++ internal/iplibrary/action_firewalld_test.go | 79 ++++++ internal/iplibrary/action_http_api.go | 81 ++++++ internal/iplibrary/action_http_api_test.go | 41 +++ internal/iplibrary/action_interface.go | 20 ++ internal/iplibrary/action_ipset.go | 276 ++++++++++++++++++++ internal/iplibrary/action_ipset_test.go | 81 ++++++ internal/iplibrary/action_iptables.go | 119 +++++++++ internal/iplibrary/action_iptables_test.go | 57 ++++ internal/iplibrary/action_manager.go | 164 ++++++++++++ internal/iplibrary/action_manager_test.go | 56 ++++ internal/iplibrary/action_script.go | 69 +++++ internal/iplibrary/action_script_test.go | 46 ++++ internal/iplibrary/action_utils.go | 85 ++++++ internal/iplibrary/action_utils_test.go | 7 + internal/iplibrary/ip_item.go | 11 +- internal/iplibrary/list_type.go | 8 + internal/iplibrary/manager_ip_list.go | 20 +- internal/nodes/node.go | 1 + 22 files changed, 1406 insertions(+), 11 deletions(-) create mode 100644 internal/iplibrary/action_base.go create mode 100644 internal/iplibrary/action_errors.go create mode 100644 internal/iplibrary/action_firewalld.go create mode 100644 internal/iplibrary/action_firewalld_test.go create mode 100644 internal/iplibrary/action_http_api.go create mode 100644 internal/iplibrary/action_http_api_test.go create mode 100644 internal/iplibrary/action_interface.go create mode 100644 internal/iplibrary/action_ipset.go create mode 100644 internal/iplibrary/action_ipset_test.go create mode 100644 internal/iplibrary/action_iptables.go create mode 100644 internal/iplibrary/action_iptables_test.go create mode 100644 internal/iplibrary/action_manager.go create mode 100644 internal/iplibrary/action_manager_test.go create mode 100644 internal/iplibrary/action_script.go create mode 100644 internal/iplibrary/action_script_test.go create mode 100644 internal/iplibrary/action_utils.go create mode 100644 internal/iplibrary/action_utils_test.go create mode 100644 internal/iplibrary/list_type.go diff --git a/internal/events/utils.go b/internal/events/utils.go index 8c45e95..209f5ba 100644 --- a/internal/events/utils.go +++ b/internal/events/utils.go @@ -20,7 +20,7 @@ func Notify(event string) { locker.Lock() callbacks, _ := eventsMap[event] locker.Unlock() - + for _, callback := range callbacks { callback() } diff --git a/internal/iplibrary/action_base.go b/internal/iplibrary/action_base.go new file mode 100644 index 0000000..5375e3d --- /dev/null +++ b/internal/iplibrary/action_base.go @@ -0,0 +1,25 @@ +package iplibrary + +import ( + "encoding/json" + "github.com/iwind/TeaGo/maps" +) + +type BaseAction struct { +} + +func (this *BaseAction) Close() error { + return nil +} + +func (this *BaseAction) convertParams(params maps.Map, ptr interface{}) error { + data, err := json.Marshal(params) + if err != nil { + return err + } + err = json.Unmarshal(data, ptr) + if err != nil { + return err + } + return nil +} diff --git a/internal/iplibrary/action_errors.go b/internal/iplibrary/action_errors.go new file mode 100644 index 0000000..bb9c430 --- /dev/null +++ b/internal/iplibrary/action_errors.go @@ -0,0 +1,22 @@ +package iplibrary + +// 是否是致命错误 +type FataError struct { + err string +} + +func (this *FataError) Error() string { + return this.err +} + +func NewFataError(err string) error { + return &FataError{err: err} +} + +func IsFatalError(err error) bool { + if err == nil { + return false + } + _, ok := err.(*FataError) + return ok +} diff --git a/internal/iplibrary/action_firewalld.go b/internal/iplibrary/action_firewalld.go new file mode 100644 index 0000000..866f898 --- /dev/null +++ b/internal/iplibrary/action_firewalld.go @@ -0,0 +1,147 @@ +package iplibrary + +import ( + "bytes" + "errors" + "fmt" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "os/exec" + "runtime" + "time" +) + +// Firewalld动作管理 +// 常用命令: +// - 查询列表: firewall-cmd --list-all +// - 添加IP:firewall-cmd --add-rich-rule="rule family='ipv4' source address='192.168.2.32' reject" --timeout=30s +// - 删除IP:firewall-cmd --remove-rich-rule="rule family='ipv4' source address='192.168.2.32' reject" --timeout=30s +type FirewalldAction struct { + BaseAction + + config *firewallconfigs.FirewallActionFirewalldConfig +} + +func NewFirewalldAction() *FirewalldAction { + return &FirewalldAction{} +} + +func (this *FirewalldAction) Init(config *firewallconfigs.FirewallActionConfig) error { + this.config = &firewallconfigs.FirewallActionFirewalldConfig{} + err := this.convertParams(config.Params, this.config) + if err != nil { + return err + } + return nil +} + +func (this *FirewalldAction) AddItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("addItem", listType, item) +} + +func (this *FirewalldAction) DeleteItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("deleteItem", listType, item) +} + +func (this *FirewalldAction) runAction(action string, listType IPListType, item *pb.IPItem) error { + if item.Type == "all" { + return nil + } + + if len(item.IpTo) == 0 { + return this.runActionSingleIP(action, listType, item) + } + cidrList, err := iPv4RangeToCIDRRange(item.IpFrom, item.IpTo) + if err != nil { + // 不合法的范围不予处理即可 + return nil + } + if len(cidrList) == 0 { + return nil + } + for _, cidr := range cidrList { + item.IpFrom = cidr + item.IpTo = "" + err := this.runActionSingleIP(action, listType, item) + if err != nil { + return err + } + } + return nil +} + +func (this *FirewalldAction) runActionSingleIP(action string, listType IPListType, item *pb.IPItem) error { + timestamp := time.Now().Unix() + + if item.ExpiredAt > 0 && timestamp > item.ExpiredAt { + return nil + } + + path := this.config.Path + var err error + if len(path) == 0 { + path, err = exec.LookPath("firewall-cmd") + if err != nil { + return err + } + } + if len(path) == 0 { + return errors.New("can not find 'firewall-cmd'") + } + + opt := "" + switch action { + case "addItem": + opt = "--add-rich-rule" + case "deleteItem": + opt = "--remove-rich-rule" + default: + return errors.New("invalid action '" + action + "'") + } + opt += "=rule family='" + switch item.Type { + case "ipv4": + opt += "ipv4" + case "ipv6": + opt += "ipv6" + default: + // 我们忽略不能识别的Family + return nil + } + + opt += "' source address='" + if len(item.IpFrom) == 0 { + return errors.New("invalid ip from") + } + opt += item.IpFrom + "' " + + switch listType { + case IPListTypeWhite: + opt += " accept" + case IPListTypeBlack: + opt += " reject" + default: + // 我们忽略不能识别的列表类型 + return nil + } + + args := []string{opt} + if item.ExpiredAt > timestamp { + args = append(args, "--timeout="+fmt.Sprintf("%d", item.ExpiredAt-timestamp)+"s") + } else { + // TODO 思考是否需要permanent,不然--reload之后会丢失 + } + + if runtime.GOOS == "darwin" { + // MAC OS直接返回 + return nil + } + cmd := exec.Command(path, args...) + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err = cmd.Run() + if err != nil { + return errors.New(err.Error() + ", output: " + string(stderr.Bytes())) + } + return nil +} diff --git a/internal/iplibrary/action_firewalld_test.go b/internal/iplibrary/action_firewalld_test.go new file mode 100644 index 0000000..7920f98 --- /dev/null +++ b/internal/iplibrary/action_firewalld_test.go @@ -0,0 +1,79 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "testing" + "time" +) + +func TestFirewalldAction_AddItem(t *testing.T) { + { + action := NewFirewalldAction() + action.config = &firewallconfigs.FirewallActionFirewalldConfig{ + Path: "/usr/bin/firewalld", + } + err := action.AddItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } + + { + action := NewFirewalldAction() + action.config = &firewallconfigs.FirewallActionFirewalldConfig{ + Path: "/usr/bin/firewalld", + } + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.101", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } +} + +func TestFirewalldAction_DeleteItem(t *testing.T) { + action := NewFirewalldAction() + action.config = &firewallconfigs.FirewallActionFirewalldConfig{ + Path: "/usr/bin/firewalld", + } + err := action.DeleteItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} + +func TestFirewalldAction_MultipleItem(t *testing.T) { + action := NewFirewalldAction() + action.config = &firewallconfigs.FirewallActionFirewalldConfig{ + Path: "/usr/bin/firewalld", + } + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.30", + IpTo: "192.168.1.200", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/iplibrary/action_http_api.go b/internal/iplibrary/action_http_api.go new file mode 100644 index 0000000..c6bbc15 --- /dev/null +++ b/internal/iplibrary/action_http_api.go @@ -0,0 +1,81 @@ +package iplibrary + +import ( + "bytes" + "encoding/json" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + teaconst "github.com/TeaOSLab/EdgeNode/internal/const" + "github.com/iwind/TeaGo/maps" + "net/http" + "time" +) + +var httpAPIClient = &http.Client{ + Timeout: 5 * time.Second, +} + +type HTTPAPIAction struct { + BaseAction + + config *firewallconfigs.FirewallActionHTTPAPIConfig +} + +func NewHTTPAPIAction() *HTTPAPIAction { + return &HTTPAPIAction{} +} + +func (this *HTTPAPIAction) Init(config *firewallconfigs.FirewallActionConfig) error { + this.config = &firewallconfigs.FirewallActionHTTPAPIConfig{} + err := this.convertParams(config.Params, this.config) + if err != nil { + return err + } + + if len(this.config.URL) == 0 { + return NewFataError("'url' should not be empty") + } + + return nil +} + +func (this *HTTPAPIAction) AddItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("addItem", listType, item) +} + +func (this *HTTPAPIAction) DeleteItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("deleteItem", listType, item) +} + +func (this *HTTPAPIAction) runAction(action string, listType IPListType, item *pb.IPItem) error { + if item == nil { + return nil + } + + // TODO 增加节点ID等信息 + m := maps.Map{ + "action": action, + "listType": listType, + "item": maps.Map{ + "type": item.Type, + "ipFrom": item.IpFrom, + "ipTo": item.IpTo, + "expiredAt": item.ExpiredAt, + }, + } + mJSON, err := json.Marshal(m) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, this.config.URL, bytes.NewReader(mJSON)) + if err != nil { + return err + } + req.Header.Set("User-Agent", "GoEdge-Node/"+teaconst.Version) + resp, err := httpAPIClient.Do(req) + if err != nil { + return err + } + _ = resp.Body.Close() + return nil +} diff --git a/internal/iplibrary/action_http_api_test.go b/internal/iplibrary/action_http_api_test.go new file mode 100644 index 0000000..3d7194b --- /dev/null +++ b/internal/iplibrary/action_http_api_test.go @@ -0,0 +1,41 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "testing" +) + +func TestHTTPAPIAction_AddItem(t *testing.T) { + action := NewHTTPAPIAction() + action.config = &firewallconfigs.FirewallActionHTTPAPIConfig{ + URL: "http://127.0.0.1:2345/post", + TimeoutSeconds: 0, + } + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} + +func TestHTTPAPIAction_DeleteItem(t *testing.T) { + action := NewHTTPAPIAction() + action.config = &firewallconfigs.FirewallActionHTTPAPIConfig{ + URL: "http://127.0.0.1:2345/post", + TimeoutSeconds: 0, + } + err := action.DeleteItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/iplibrary/action_interface.go b/internal/iplibrary/action_interface.go new file mode 100644 index 0000000..0279a82 --- /dev/null +++ b/internal/iplibrary/action_interface.go @@ -0,0 +1,20 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" +) + +type ActionInterface interface { + // 初始化 + Init(config *firewallconfigs.FirewallActionConfig) error + + // 添加 + AddItem(listType IPListType, item *pb.IPItem) error + + // 删除 + DeleteItem(listType IPListType, item *pb.IPItem) error + + // 关闭 + Close() error +} diff --git a/internal/iplibrary/action_ipset.go b/internal/iplibrary/action_ipset.go new file mode 100644 index 0000000..e6b76b3 --- /dev/null +++ b/internal/iplibrary/action_ipset.go @@ -0,0 +1,276 @@ +package iplibrary + +import ( + "bytes" + "errors" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "os/exec" + "runtime" + "strconv" + "time" +) + +// IPSet动作 +// 相关命令: +// - 利用Firewalld管理set: +// - 添加:firewall-cmd --permanent --new-ipset=edge_ip_list --type=hash:ip --option="timeout=0" +// - 删除:firewall-cmd --permanent --delete-ipset=edge_ip_list +// - 重载:firewall-cmd --reload +// - firewalld+ipset: firewall-cmd --permanent --add-rich-rule="rule source ipset='edge_ip_list' reject" +// - 利用IPTables管理set: +// - 添加:iptables -A INPUT -m set --match-set edge_ip_list src -j REJECT +// - 添加Item:ipset add edge_ip_list 192.168.2.32 timeout 30 +// - 删除Item: ipset del edge_ip_list 192.168.2.32 +// - 创建set:ipset create edge_ip_list hash:ip timeout 0 +type IPSetAction struct { + BaseAction + + config *firewallconfigs.FirewallActionIPSetConfig +} + +func NewIPSetAction() *IPSetAction { + return &IPSetAction{} +} + +func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) error { + this.config = &firewallconfigs.FirewallActionIPSetConfig{} + err := this.convertParams(config.Params, this.config) + if err != nil { + return err + } + + if len(this.config.WhiteName) == 0 { + return NewFataError("white list name should not be empty") + } + if len(this.config.BlackName) == 0 { + return NewFataError("black list name should not be empty") + } + + // 创建ipset + { + path, err := exec.LookPath("ipset") + if err != nil { + return err + } + { + cmd := exec.Command(path, "create", this.config.WhiteName, "hash:ip", "timeout", "0") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + if !bytes.Contains(output, []byte("already exists")) { + return errors.New("create ipset '" + this.config.WhiteName + "': " + err.Error() + ", output: " + string(output)) + } else { + err = nil + } + } + } + { + cmd := exec.Command(path, "create", this.config.BlackName, "hash:ip", "timeout", "0") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + if !bytes.Contains(output, []byte("already exists")) { + return errors.New("create ipset '" + this.config.BlackName + "': " + err.Error() + ", output: " + string(output)) + } else { + err = nil + } + } + } + } + + // firewalld + if this.config.AutoAddToFirewalld { + path, err := exec.LookPath("firewall-cmd") + if err != nil { + return err + } + + { + cmd := exec.Command(path, "--permanent", "--new-ipset="+this.config.WhiteName, "--type=hash:ip", "--option=timeout=0", "--option=maxelem=1000000") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + if bytes.Contains(output, []byte("NAME_CONFLICT")) { + err = nil + } else { + return errors.New("firewall-cmd add ipset '" + this.config.WhiteName + "': " + err.Error() + ", output: " + string(output)) + } + } + } + + { + cmd := exec.Command(path, "--permanent", "--add-rich-rule=rule source ipset='"+this.config.WhiteName+"' accept") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + return errors.New("firewall-cmd add rich rule '" + this.config.WhiteName + "': " + err.Error() + ", output: " + string(output)) + } + } + + { + cmd := exec.Command(path, "--permanent", "--new-ipset="+this.config.BlackName, "--type=hash:ip", "--option=timeout=0", "--option=maxelem=1000000") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + if bytes.Contains(output, []byte("NAME_CONFLICT")) { + err = nil + } else { + return errors.New("firewall-cmd add ipset '" + this.config.BlackName + "': " + err.Error() + ", output: " + string(output)) + } + } + } + + { + cmd := exec.Command(path, "--permanent", "--add-rich-rule=rule source ipset='"+this.config.BlackName+"' reject") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + return errors.New("firewall-cmd add rich rule '" + this.config.WhiteName + "': " + err.Error() + ", output: " + string(output)) + } + } + + // reload + { + cmd := exec.Command(path, "--reload") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + return errors.New("firewall-cmd reload: " + err.Error() + ", output: " + string(output)) + } + } + } + + // iptables + if this.config.AutoAddToIPTables { + path, err := exec.LookPath("iptables") + if err != nil { + return err + } + + { + cmd := exec.Command(path, "-A", "INPUT", "-m", "set", "--match-set", this.config.WhiteName, "src", "-j", "ACCEPT") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + return errors.New("iptables add rule: " + err.Error() + ", output: " + string(output)) + } + } + + { + cmd := exec.Command(path, "-A", "INPUT", "-m", "set", "--match-set", this.config.BlackName, "src", "-j", "REJECT") + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + output := stderr.Bytes() + return errors.New("iptables add rule: " + err.Error() + ", output: " + string(output)) + } + } + } + + return nil +} + +func (this *IPSetAction) AddItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("addItem", listType, item) +} + +func (this *IPSetAction) DeleteItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("deleteItem", listType, item) +} + +func (this *IPSetAction) runAction(action string, listType IPListType, item *pb.IPItem) error { + if item.Type == "all" { + return nil + } + if len(item.IpTo) == 0 { + return this.runActionSingleIP(action, listType, item) + } + cidrList, err := iPv4RangeToCIDRRange(item.IpFrom, item.IpTo) + if err != nil { + // 不合法的范围不予处理即可 + return nil + } + if len(cidrList) == 0 { + return nil + } + for _, cidr := range cidrList { + item.IpFrom = cidr + item.IpTo = "" + err := this.runActionSingleIP(action, listType, item) + if err != nil { + return err + } + } + return nil +} + +func (this *IPSetAction) runActionSingleIP(action string, listType IPListType, item *pb.IPItem) error { + if item.Type == "all" { + return nil + } + + listName := "" + switch listType { + case IPListTypeWhite: + listName = this.config.WhiteName + case IPListTypeBlack: + listName = this.config.BlackName + default: + // 不支持的类型 + return nil + } + if len(listName) == 0 { + return errors.New("empty list name for '" + listType + "'") + } + + path := this.config.Path + var err error + if len(path) == 0 { + path, err = exec.LookPath("ipset") + if err != nil { + return err + } + } + + // ipset add edge_ip_list 192.168.2.32 timeout 30 + args := []string{} + switch action { + case "addItem": + args = append(args, "add") + case "deleteItem": + args = append(args, "del") + } + args = append(args, listName, item.IpFrom) + timestamp := time.Now().Unix() + if item.ExpiredAt > timestamp { + args = append(args, "timeout", strconv.FormatInt(item.ExpiredAt-timestamp, 10)) + } + + //logs.Println(args) + + if runtime.GOOS == "darwin" { + // MAC OS直接返回 + return nil + } + + cmd := exec.Command(path, args...) + return cmd.Run() +} diff --git a/internal/iplibrary/action_ipset_test.go b/internal/iplibrary/action_ipset_test.go new file mode 100644 index 0000000..bda89dd --- /dev/null +++ b/internal/iplibrary/action_ipset_test.go @@ -0,0 +1,81 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "github.com/iwind/TeaGo/maps" + "testing" + "time" +) + +func TestIPSetAction_Init(t *testing.T) { + action := NewIPSetAction() + err := action.Init(&firewallconfigs.FirewallActionConfig{ + Params: maps.Map{ + "path": "/usr/bin/iptables", + "whiteName": "white-list", + "blackName": "black-list", + }, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} + +func TestIPSetAction_AddItem(t *testing.T) { + action := NewIPSetAction() + action.config = &firewallconfigs.FirewallActionIPSetConfig{ + Path: "/usr/bin/iptables", + WhiteName: "white-list", + BlackName: "black-list", + } + { + err := action.AddItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } + + { + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } +} + +func TestIPSetAction_DeleteItem(t *testing.T) { + action := NewIPSetAction() + err := action.Init(&firewallconfigs.FirewallActionConfig{ + Params: maps.Map{ + "path": "/usr/bin/firewalld", + "whiteName": "white-list", + }, + }) + if err != nil { + t.Fatal(err) + } + err = action.DeleteItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/iplibrary/action_iptables.go b/internal/iplibrary/action_iptables.go new file mode 100644 index 0000000..1a7d4f0 --- /dev/null +++ b/internal/iplibrary/action_iptables.go @@ -0,0 +1,119 @@ +package iplibrary + +import ( + "bytes" + "errors" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "os/exec" + "runtime" +) + +// IPTables动作 +// 相关命令: +// iptables -A INPUT -s "192.168.2.32" -j ACCEPT +// iptables -A INPUT -s "192.168.2.32" -j REJECT +// iptables -D ... +type IPTablesAction struct { + BaseAction + + config *firewallconfigs.FirewallActionIPTablesConfig +} + +func NewIPTablesAction() *IPTablesAction { + return &IPTablesAction{} +} + +func (this *IPTablesAction) Init(config *firewallconfigs.FirewallActionConfig) error { + this.config = &firewallconfigs.FirewallActionIPTablesConfig{} + err := this.convertParams(config.Params, this.config) + if err != nil { + return err + } + return nil +} + +func (this *IPTablesAction) AddItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("addItem", listType, item) +} + +func (this *IPTablesAction) DeleteItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("deleteItem", listType, item) +} + +func (this *IPTablesAction) runAction(action string, listType IPListType, item *pb.IPItem) error { + if item.Type == "all" { + return nil + } + if len(item.IpTo) == 0 { + return this.runActionSingleIP(action, listType, item) + } + cidrList, err := iPv4RangeToCIDRRange(item.IpFrom, item.IpTo) + if err != nil { + // 不合法的范围不予处理即可 + return nil + } + if len(cidrList) == 0 { + return nil + } + for _, cidr := range cidrList { + item.IpFrom = cidr + item.IpTo = "" + err := this.runActionSingleIP(action, listType, item) + if err != nil { + return err + } + } + return nil +} + +func (this *IPTablesAction) runActionSingleIP(action string, listType IPListType, item *pb.IPItem) error { + if item.Type == "all" { + return nil + } + path := this.config.Path + var err error + if len(path) == 0 { + path, err = exec.LookPath("iptables") + if err != nil { + return err + } + } + iptablesAction := "" + switch action { + case "addItem": + iptablesAction = "-A" + case "deleteItem": + iptablesAction = "-D" + default: + return nil + } + args := []string{iptablesAction, "INPUT", "-s", item.IpFrom, "-j"} + switch listType { + case IPListTypeWhite: + args = append(args, "ACCEPT") + case IPListTypeBlack: + args = append(args, "REJECT") + default: + return nil + } + + if runtime.GOOS == "darwin" { + // MAC OS直接返回 + return nil + } + + cmd := exec.Command(path, args...) + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err = cmd.Run() + if err != nil { + output := stderr.Bytes() + if bytes.Contains(output, []byte("No chain/target/match")) { + err = nil + } else { + return errors.New(err.Error() + ", output: " + string(output)) + } + } + return nil +} diff --git a/internal/iplibrary/action_iptables_test.go b/internal/iplibrary/action_iptables_test.go new file mode 100644 index 0000000..37a2e1f --- /dev/null +++ b/internal/iplibrary/action_iptables_test.go @@ -0,0 +1,57 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "testing" + "time" +) + +func TestIPTablesAction_AddItem(t *testing.T) { + action := NewIPTablesAction() + action.config = &firewallconfigs.FirewallActionIPTablesConfig{ + Path: "/usr/bin/iptables", + } + { + err := action.AddItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } + + { + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") + } +} + +func TestIPTablesAction_DeleteItem(t *testing.T) { + action := NewIPTablesAction() + action.config = &firewallconfigs.FirewallActionIPTablesConfig{ + Path: "/usr/bin/firewalld", + } + err := action.DeleteItem(IPListTypeWhite, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix() + 30, + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/iplibrary/action_manager.go b/internal/iplibrary/action_manager.go new file mode 100644 index 0000000..93ed105 --- /dev/null +++ b/internal/iplibrary/action_manager.go @@ -0,0 +1,164 @@ +package iplibrary + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "github.com/TeaOSLab/EdgeNode/internal/remotelogs" + "strconv" + "sync" +) + +var SharedActionManager = NewActionManager() + +// 动作管理器定义 +type ActionManager struct { + locker sync.Mutex + + eventMap map[string][]ActionInterface // eventLevel => []instance + configMap map[int64]*firewallconfigs.FirewallActionConfig // id => config + instanceMap map[int64]ActionInterface // id => instance +} + +// 获取动作管理对象 +func NewActionManager() *ActionManager { + return &ActionManager{ + configMap: map[int64]*firewallconfigs.FirewallActionConfig{}, + instanceMap: map[int64]ActionInterface{}, + } +} + +// 更新配置 +func (this *ActionManager) UpdateActions(actions []*firewallconfigs.FirewallActionConfig) { + this.locker.Lock() + defer this.locker.Unlock() + + // 关闭不存在的 + newActionsMap := map[int64]*firewallconfigs.FirewallActionConfig{} + for _, action := range actions { + newActionsMap[action.Id] = action + } + for _, oldAction := range this.configMap { + _, ok := newActionsMap[oldAction.Id] + if !ok { + instance, ok := this.instanceMap[oldAction.Id] + if ok { + _ = instance.Close() + delete(this.instanceMap, oldAction.Id) + remotelogs.Println("IPLIBRARY/ACTION_MANAGER", "close action "+strconv.FormatInt(oldAction.Id, 10)) + } + } + } + + // 添加新的或者更新老的 + for _, newAction := range newActionsMap { + oldInstance, ok := this.instanceMap[newAction.Id] + if ok { + // 检查配置是否一致 + oldConfigJSON, err := json.Marshal(this.configMap[newAction.Id]) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "action "+strconv.FormatInt(newAction.Id, 10) + ", type:" + newAction.Type+": "+err.Error()) + continue + } + newConfigJSON, err := json.Marshal(newAction) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "action "+strconv.FormatInt(newAction.Id, 10) + ", type:" + newAction.Type+": "+err.Error()) + continue + } + if bytes.Compare(newConfigJSON, oldConfigJSON) != 0 { + _ = oldInstance.Close() + + // 重新创建 + // 之所以要重新创建,是因为前后的动作类型可能有变化,完全重建可以避免不必要的麻烦 + newInstance, err := this.createInstance(newAction) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "reload action "+strconv.FormatInt(newAction.Id, 10) + ", type:" + newAction.Type+": "+err.Error()) + continue + } + remotelogs.Println("IPLIBRARY/ACTION_MANAGER", "reloaded "+strconv.FormatInt(newAction.Id, 10)+", type:"+newAction.Type) + this.instanceMap[newAction.Id] = newInstance + } + } else { + // 创建 + instance, err := this.createInstance(newAction) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "load new action "+strconv.FormatInt(newAction.Id, 10) + ", type:" + newAction.Type+": "+err.Error()) + continue + } + remotelogs.Println("IPLIBRARY/ACTION_MANAGER", "loaded action "+strconv.FormatInt(newAction.Id, 10)+", type:"+newAction.Type) + this.instanceMap[newAction.Id] = instance + } + } + + // 更新配置 + this.configMap = newActionsMap + this.eventMap = map[string][]ActionInterface{} + for _, action := range this.configMap { + instance, ok := this.instanceMap[action.Id] + if !ok { + continue + } + + instances, _ := this.eventMap[action.EventLevel] + instances = append(instances, instance) + this.eventMap[action.EventLevel] = instances + } +} + +// 执行添加IP动作 +func (this *ActionManager) AddItem(listType IPListType, item *pb.IPItem) { + instances, ok := this.eventMap[item.EventLevel] + if ok { + for _, instance := range instances { + err := instance.AddItem(listType, item) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "add item '"+fmt.Sprintf("%d", item.Id)+"': "+err.Error()) + } + } + } +} + +// 执行删除IP动作 +func (this *ActionManager) DeleteItem(listType IPListType, item *pb.IPItem) { + instances, ok := this.eventMap[item.EventLevel] + if ok { + for _, instance := range instances { + err := instance.DeleteItem(listType, item) + if err != nil { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER", "delete item '"+fmt.Sprintf("%d", item.Id)+"': "+err.Error()) + } + } + } +} + +func (this *ActionManager) createInstance(config *firewallconfigs.FirewallActionConfig) (ActionInterface, error) { + var instance ActionInterface + switch config.Type { + case firewallconfigs.FirewallActionTypeIPSet: + instance = NewIPSetAction() + case firewallconfigs.FirewallActionTypeFirewalld: + instance = NewFirewalldAction() + case firewallconfigs.FirewallActionTypeIPTables: + instance = NewIPTablesAction() + case firewallconfigs.FirewallActionTypeScript: + instance = NewScriptAction() + case firewallconfigs.FirewallActionTypeHTTPAPI: + instance = NewHTTPAPIAction() + } + if instance == nil { + return nil, errors.New("can not create instance for type '" + config.Type + "'") + } + err := instance.Init(config) + if err != nil { + // 如果是警告错误,我们只是提示 + if !IsFatalError(err) { + remotelogs.Error("IPLIBRARY/ACTION_MANAGER/CREATE_INSTANCE", "init '"+config.Type+"' failed: "+err.Error()) + } else { + return nil, err + } + } + return instance, nil +} diff --git a/internal/iplibrary/action_manager_test.go b/internal/iplibrary/action_manager_test.go new file mode 100644 index 0000000..e0541c5 --- /dev/null +++ b/internal/iplibrary/action_manager_test.go @@ -0,0 +1,56 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "github.com/iwind/TeaGo/maps" + "testing" +) + +func TestActionManager_UpdateActions(t *testing.T) { + manager := NewActionManager() + manager.UpdateActions([]*firewallconfigs.FirewallActionConfig{ + { + Id: 1, + Type: "ipset", + Params: maps.Map{ + "whiteName": "edge-white-list", + "blackName": "edge-black-list", + }, + }, + }) + t.Log("===config===") + for _, c := range manager.configMap { + t.Log(c.Id, c.Type) + } + t.Log("===instance===") + for id, c := range manager.instanceMap { + t.Log(id, c) + } + + manager.UpdateActions([]*firewallconfigs.FirewallActionConfig{ + { + Id: 1, + Type: "ipset", + Params: maps.Map{ + "whiteName": "edge-white-list", + "blackName": "edge-black-list", + }, + }, + { + Id: 2, + Type: "iptables", + Params: maps.Map{ + }, + }, + }) + + t.Log("===config===") + for _, c := range manager.configMap { + t.Log(c.Id, c.Type) + } + t.Log("===instance===") + for id, c := range manager.instanceMap { + t.Logf("%d: %#v", id, c) + } + +} diff --git a/internal/iplibrary/action_script.go b/internal/iplibrary/action_script.go new file mode 100644 index 0000000..227d8e1 --- /dev/null +++ b/internal/iplibrary/action_script.go @@ -0,0 +1,69 @@ +package iplibrary + +import ( + "bytes" + "errors" + "fmt" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "os/exec" + "path/filepath" +) + +// 脚本命令动作 +type ScriptAction struct { + BaseAction + + config *firewallconfigs.FirewallActionScriptConfig +} + +func NewScriptAction() *ScriptAction { + return &ScriptAction{} +} + +func (this *ScriptAction) Init(config *firewallconfigs.FirewallActionConfig) error { + this.config = &firewallconfigs.FirewallActionScriptConfig{} + err := this.convertParams(config.Params, this.config) + if err != nil { + return err + } + + if len(this.config.Path) == 0 { + return NewFataError("'path' should not be empty") + } + + return nil +} + +func (this *ScriptAction) AddItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("addItem", listType, item) +} + +func (this *ScriptAction) DeleteItem(listType IPListType, item *pb.IPItem) error { + return this.runAction("deleteItem", listType, item) +} + +func (this *ScriptAction) runAction(action string, listType IPListType, item *pb.IPItem) error { + // TODO 智能支持 .sh 脚本文件 + cmd := exec.Command(this.config.Path) + cmd.Env = []string{ + "ACTION=" + action, + "TYPE=" + item.Type, + "IP_FROM=" + item.IpFrom, + "IP_TO=" + item.IpTo, + "EXPIRED_AT=" + fmt.Sprintf("%d", item.ExpiredAt), + "LIST_TYPE=" + listType, + } + if len(this.config.Cwd) > 0 { + cmd.Dir = this.config.Cwd + } else { + cmd.Dir = filepath.Dir(this.config.Path) + } + stderr := bytes.NewBuffer([]byte{}) + cmd.Stderr = stderr + err := cmd.Run() + if err != nil { + return errors.New(err.Error() + ", output: " + string(stderr.Bytes())) + } + return nil +} diff --git a/internal/iplibrary/action_script_test.go b/internal/iplibrary/action_script_test.go new file mode 100644 index 0000000..310abc2 --- /dev/null +++ b/internal/iplibrary/action_script_test.go @@ -0,0 +1,46 @@ +package iplibrary + +import ( + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs" + "testing" + "time" +) + +func TestScriptAction_AddItem(t *testing.T) { + action := NewScriptAction() + action.config = &firewallconfigs.FirewallActionScriptConfig{ + Path: "/tmp/ip-item.sh", + Cwd: "", + Args: nil, + } + err := action.AddItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix(), + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} + +func TestScriptAction_DeleteItem(t *testing.T) { + action := NewScriptAction() + action.config = &firewallconfigs.FirewallActionScriptConfig{ + Path: "/tmp/ip-item.sh", + Cwd: "", + Args: nil, + } + err := action.DeleteItem(IPListTypeBlack, &pb.IPItem{ + Type: "ipv4", + Id: 1, + IpFrom: "192.168.1.100", + ExpiredAt: time.Now().Unix(), + }) + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/iplibrary/action_utils.go b/internal/iplibrary/action_utils.go new file mode 100644 index 0000000..ccba676 --- /dev/null +++ b/internal/iplibrary/action_utils.go @@ -0,0 +1,85 @@ +package iplibrary + +import ( + "fmt" + "log" + "math" + "strconv" + "strings" +) + +// Convert IPv4 range into CIDR +// 来自:https://gist.github.com/P-A-R-U-S/a090dd90c5104ce85a29c32669dac107 +func iPv4RangeToCIDRRange(ipStart string, ipEnd string) (cidrs []string, err error) { + + cidr2mask := []uint32{ + 0x00000000, 0x80000000, 0xC0000000, + 0xE0000000, 0xF0000000, 0xF8000000, + 0xFC000000, 0xFE000000, 0xFF000000, + 0xFF800000, 0xFFC00000, 0xFFE00000, + 0xFFF00000, 0xFFF80000, 0xFFFC0000, + 0xFFFE0000, 0xFFFF0000, 0xFFFF8000, + 0xFFFFC000, 0xFFFFE000, 0xFFFFF000, + 0xFFFFF800, 0xFFFFFC00, 0xFFFFFE00, + 0xFFFFFF00, 0xFFFFFF80, 0xFFFFFFC0, + 0xFFFFFFE0, 0xFFFFFFF0, 0xFFFFFFF8, + 0xFFFFFFFC, 0xFFFFFFFE, 0xFFFFFFFF, + } + + ipStartUint32 := iPv4ToUint32(ipStart) + ipEndUint32 := iPv4ToUint32(ipEnd) + + if ipStartUint32 > ipEndUint32 { + log.Fatalf("start IP:%s must be less than end IP:%s", ipStart, ipEnd) + } + + for ipEndUint32 >= ipStartUint32 { + maxSize := 32 + for maxSize > 0 { + + maskedBase := ipStartUint32 & cidr2mask[maxSize-1] + + if maskedBase != ipStartUint32 { + break + } + maxSize-- + + } + + x := math.Log(float64(ipEndUint32-ipStartUint32+1)) / math.Log(2) + maxDiff := 32 - int(math.Floor(x)) + if maxSize < maxDiff { + maxSize = maxDiff + } + + cidrs = append(cidrs, uInt32ToIPv4(ipStartUint32)+"/"+strconv.Itoa(maxSize)) + + ipStartUint32 += uint32(math.Exp2(float64(32 - maxSize))) + } + + return cidrs, err +} + +//Convert IPv4 to uint32 +func iPv4ToUint32(iPv4 string) uint32 { + + ipOctets := [4]uint64{} + + for i, v := range strings.SplitN(iPv4, ".", 4) { + ipOctets[i], _ = strconv.ParseUint(v, 10, 32) + } + + result := (ipOctets[0] << 24) | (ipOctets[1] << 16) | (ipOctets[2] << 8) | ipOctets[3] + + return uint32(result) +} + +//Convert uint32 to IP +func uInt32ToIPv4(iPuInt32 uint32) (iP string) { + iP = fmt.Sprintf("%d.%d.%d.%d", + iPuInt32>>24, + (iPuInt32&0x00FFFFFF)>>16, + (iPuInt32&0x0000FFFF)>>8, + iPuInt32&0x000000FF) + return iP +} diff --git a/internal/iplibrary/action_utils_test.go b/internal/iplibrary/action_utils_test.go new file mode 100644 index 0000000..ef1eb03 --- /dev/null +++ b/internal/iplibrary/action_utils_test.go @@ -0,0 +1,7 @@ +package iplibrary + +import "testing" + +func TestIPv4RangeToCIDRRange(t *testing.T) { + t.Log(iPv4RangeToCIDRRange("192.168.0.0", "192.168.255.255")) +} diff --git a/internal/iplibrary/ip_item.go b/internal/iplibrary/ip_item.go index 8bb0c74..81e0fd3 100644 --- a/internal/iplibrary/ip_item.go +++ b/internal/iplibrary/ip_item.go @@ -12,11 +12,12 @@ const ( // IP条目 type IPItem struct { - Type string - Id int64 - IPFrom uint64 - IPTo uint64 - ExpiredAt int64 + Type string `json:"type"` + Id int64 `json:"id"` + IPFrom uint64 `json:"ipFrom"` + IPTo uint64 `json:"ipTo"` + ExpiredAt int64 `json:"expiredAt"` + EventLevel string `json:"eventLevel"` } // 检查是否包含某个IP diff --git a/internal/iplibrary/list_type.go b/internal/iplibrary/list_type.go new file mode 100644 index 0000000..a8dabbf --- /dev/null +++ b/internal/iplibrary/list_type.go @@ -0,0 +1,8 @@ +package iplibrary + +type IPListType = string + +const ( + IPListTypeWhite IPListType = "white" + IPListTypeBlack IPListType = "black" +) diff --git a/internal/iplibrary/manager_ip_list.go b/internal/iplibrary/manager_ip_list.go index a89dc3d..6700f7f 100644 --- a/internal/iplibrary/manager_ip_list.go +++ b/internal/iplibrary/manager_ip_list.go @@ -120,15 +120,25 @@ func (this *IPListManager) fetch() (hasNext bool, err error) { } if item.IsDeleted { list.Delete(item.Id) + + // 操作事件 + SharedActionManager.DeleteItem(item.ListType, item) + continue } + list.Add(&IPItem{ - Id: item.Id, - Type: item.Type, - IPFrom: utils.IP2Long(item.IpFrom), - IPTo: utils.IP2Long(item.IpTo), - ExpiredAt: item.ExpiredAt, + Id: item.Id, + Type: item.Type, + IPFrom: utils.IP2Long(item.IpFrom), + IPTo: utils.IP2Long(item.IpTo), + ExpiredAt: item.ExpiredAt, + EventLevel: item.EventLevel, }) + + // 事件操作 + SharedActionManager.DeleteItem(item.ListType, item) + SharedActionManager.AddItem(item.ListType, item) } this.locker.Unlock() this.version = items[len(items)-1].Version diff --git a/internal/nodes/node.go b/internal/nodes/node.go index 783a717..05cb107 100644 --- a/internal/nodes/node.go +++ b/internal/nodes/node.go @@ -369,6 +369,7 @@ func (this *Node) syncConfig() error { } sharedWAFManager.UpdatePolicies(nodeConfig.FindAllFirewallPolicies()) + iplibrary.SharedActionManager.UpdateActions(nodeConfig.FirewallActions) sharedNodeConfig = nodeConfig // 发送事件