增加IP动作

This commit is contained in:
GoEdgeLab
2021-02-06 17:34:33 +08:00
parent b457277b6e
commit 644aa401a2
22 changed files with 1406 additions and 11 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
// - 添加IPfirewall-cmd --add-rich-rule="rule family='ipv4' source address='192.168.2.32' reject" --timeout=30s
// - 删除IPfirewall-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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
// - 添加Itemipset add edge_ip_list 192.168.2.32 timeout 30
// - 删除Item: ipset del edge_ip_list 192.168.2.32
// - 创建setipset 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()
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
package iplibrary
import "testing"
func TestIPv4RangeToCIDRRange(t *testing.T) {
t.Log(iPv4RangeToCIDRRange("192.168.0.0", "192.168.255.255"))
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
package iplibrary
type IPListType = string
const (
IPListTypeWhite IPListType = "white"
IPListTypeBlack IPListType = "black"
)

View File

@@ -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,
EventLevel: item.EventLevel,
})
// 事件操作
SharedActionManager.DeleteItem(item.ListType, item)
SharedActionManager.AddItem(item.ListType, item)
}
this.locker.Unlock()
this.version = items[len(items)-1].Version

View File

@@ -369,6 +369,7 @@ func (this *Node) syncConfig() error {
}
sharedWAFManager.UpdatePolicies(nodeConfig.FindAllFirewallPolicies())
iplibrary.SharedActionManager.UpdateActions(nodeConfig.FirewallActions)
sharedNodeConfig = nodeConfig
// 发送事件