github.com/TeaOSLab/EdgeNode@v1.3.8/internal/iplibrary/action_ipset.go (about)

     1  package iplibrary
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
     7  	"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
     8  	executils "github.com/TeaOSLab/EdgeNode/internal/utils/exec"
     9  	"github.com/iwind/TeaGo/types"
    10  	"runtime"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  )
    15  
    16  // IPSetAction IPSet动作
    17  // 相关命令:
    18  //   - 利用Firewalld管理set:
    19  //   - 添加:firewall-cmd --permanent --new-ipset=edge_ip_list --type=hash:ip --option="timeout=0"
    20  //   - 删除:firewall-cmd --permanent --delete-ipset=edge_ip_list
    21  //   - 重载:firewall-cmd --reload
    22  //   - firewalld+ipset: firewall-cmd --permanent --add-rich-rule="rule source ipset='edge_ip_list' reject"
    23  //   - 利用IPTables管理set:
    24  //   - 添加:iptables -A INPUT -m set --match-set edge_ip_list src -j REJECT
    25  //   - 添加Item:ipset add edge_ip_list 192.168.2.32 timeout 30
    26  //   - 删除Item: ipset del edge_ip_list 192.168.2.32
    27  //   - 创建set:ipset create edge_ip_list hash:ip timeout 0
    28  //   - 查看统计:ipset -t list edge_black_list
    29  //   - 删除set:ipset destroy edge_black_list
    30  type IPSetAction struct {
    31  	BaseAction
    32  
    33  	config *firewallconfigs.FirewallActionIPSetConfig
    34  
    35  	ipsetNotfound bool
    36  }
    37  
    38  func NewIPSetAction() *IPSetAction {
    39  	return &IPSetAction{}
    40  }
    41  
    42  func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) error {
    43  	this.config = &firewallconfigs.FirewallActionIPSetConfig{}
    44  	err := this.convertParams(config.Params, this.config)
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	if len(this.config.WhiteName) == 0 {
    50  		return NewFataError("white list name should not be empty")
    51  	}
    52  	if len(this.config.BlackName) == 0 {
    53  		return NewFataError("black list name should not be empty")
    54  	}
    55  
    56  	// 创建ipset
    57  	{
    58  		path, err := executils.LookPath("ipset")
    59  		if err != nil {
    60  			return err
    61  		}
    62  
    63  		// ipv4
    64  		for _, listName := range []string{this.config.WhiteName, this.config.BlackName} {
    65  			if len(listName) == 0 {
    66  				continue
    67  			}
    68  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "create", listName, "hash:ip", "timeout", "0", "maxelem", "1000000")
    69  			cmd.WithStderr()
    70  			err := cmd.Run()
    71  			if err != nil {
    72  				var output = cmd.Stderr()
    73  				if !strings.Contains(output, "already exists") {
    74  					return fmt.Errorf("create ipset '%s': %w, output: %s", listName, err, output)
    75  				} else {
    76  					err = nil
    77  				}
    78  			}
    79  		}
    80  
    81  		// ipv6
    82  		for _, listName := range []string{this.config.WhiteNameIPv6, this.config.BlackNameIPv6} {
    83  			if len(listName) == 0 {
    84  				continue
    85  			}
    86  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "create", listName, "hash:ip", "family", "inet6", "timeout", "0", "maxelem", "1000000")
    87  			cmd.WithStderr()
    88  			err := cmd.Run()
    89  			if err != nil {
    90  				var output = cmd.Stderr()
    91  				if !strings.Contains(output, "already exists") {
    92  					return fmt.Errorf("create ipset '%s': %w, output: %s", listName, err, output)
    93  				} else {
    94  					err = nil
    95  				}
    96  			}
    97  		}
    98  	}
    99  
   100  	// firewalld
   101  	if this.config.AutoAddToFirewalld {
   102  		path, err := executils.LookPath("firewall-cmd")
   103  		if err != nil {
   104  			return err
   105  		}
   106  
   107  		// ipv4
   108  		for _, listName := range []string{this.config.WhiteName, this.config.BlackName} {
   109  			if len(listName) == 0 {
   110  				continue
   111  			}
   112  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "--permanent", "--new-ipset="+listName, "--type=hash:ip", "--option=timeout=0", "--option=maxelem=1000000")
   113  			cmd.WithStderr()
   114  			err := cmd.Run()
   115  			if err != nil {
   116  				var output = cmd.Stderr()
   117  				if strings.Contains(output, "NAME_CONFLICT") {
   118  					err = nil
   119  				} else {
   120  					return fmt.Errorf("firewall-cmd add ipset '%s': %w, output: %s", listName, err, output)
   121  				}
   122  			}
   123  		}
   124  
   125  		// ipv6
   126  		for _, listName := range []string{this.config.WhiteNameIPv6, this.config.BlackNameIPv6} {
   127  			if len(listName) == 0 {
   128  				continue
   129  			}
   130  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "--permanent", "--new-ipset="+listName, "--type=hash:ip", "--option=family=inet6", "--option=timeout=0", "--option=maxelem=1000000")
   131  			cmd.WithStderr()
   132  			err := cmd.Run()
   133  			if err != nil {
   134  				var output = cmd.Stderr()
   135  				if strings.Contains(output, "NAME_CONFLICT") {
   136  					err = nil
   137  				} else {
   138  					return fmt.Errorf("firewall-cmd add ipset '%s': %w, output: %s", listName, err, output)
   139  				}
   140  			}
   141  		}
   142  
   143  		// accept
   144  		for _, listName := range []string{this.config.WhiteName, this.config.WhiteNameIPv6} {
   145  			if len(listName) == 0 {
   146  				continue
   147  			}
   148  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "--permanent", "--add-rich-rule=rule source ipset='"+listName+"' accept")
   149  			cmd.WithStderr()
   150  			err := cmd.Run()
   151  			if err != nil {
   152  				return fmt.Errorf("firewall-cmd add rich rule '%s': %w, output: %s", listName, err, cmd.Stderr())
   153  			}
   154  		}
   155  
   156  		// reject
   157  		for _, listName := range []string{this.config.BlackName, this.config.BlackNameIPv6} {
   158  			if len(listName) == 0 {
   159  				continue
   160  			}
   161  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "--permanent", "--add-rich-rule=rule source ipset='"+listName+"' reject")
   162  			cmd.WithStderr()
   163  			err := cmd.Run()
   164  			if err != nil {
   165  				return fmt.Errorf("firewall-cmd add rich rule '%s': %w, output: %s", listName, err, cmd.Stderr())
   166  			}
   167  		}
   168  
   169  		// reload
   170  		{
   171  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "--reload")
   172  			cmd.WithStderr()
   173  			err := cmd.Run()
   174  			if err != nil {
   175  				return fmt.Errorf("firewall-cmd reload: %w, output: %s", err, cmd.Stderr())
   176  			}
   177  		}
   178  	}
   179  
   180  	// iptables
   181  	if this.config.AutoAddToIPTables {
   182  		path, err := executils.LookPath("iptables")
   183  		if err != nil {
   184  			return err
   185  		}
   186  
   187  		// accept
   188  		for _, listName := range []string{this.config.WhiteName, this.config.WhiteNameIPv6} {
   189  			if len(listName) == 0 {
   190  				continue
   191  			}
   192  
   193  			// 检查规则是否存在
   194  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "-C", "INPUT", "-m", "set", "--match-set", listName, "src", "-j", "ACCEPT")
   195  			err := cmd.Run()
   196  			var exists = err == nil
   197  
   198  			// 添加规则
   199  			if !exists {
   200  				var cmd = executils.NewTimeoutCmd(30*time.Second, path, "-A", "INPUT", "-m", "set", "--match-set", listName, "src", "-j", "ACCEPT")
   201  				cmd.WithStderr()
   202  				err := cmd.Run()
   203  				if err != nil {
   204  					return fmt.Errorf("iptables add rule: %w, output: %s", err, cmd.Stderr())
   205  				}
   206  			}
   207  		}
   208  
   209  		// reject
   210  		for _, listName := range []string{this.config.BlackName, this.config.BlackNameIPv6} {
   211  			if len(listName) == 0 {
   212  				continue
   213  			}
   214  
   215  			// 检查规则是否存在
   216  			var cmd = executils.NewTimeoutCmd(30*time.Second, path, "-C", "INPUT", "-m", "set", "--match-set", listName, "src", "-j", "REJECT")
   217  			err := cmd.Run()
   218  			var exists = err == nil
   219  
   220  			if !exists {
   221  				var cmd = executils.NewTimeoutCmd(30*time.Second, path, "-A", "INPUT", "-m", "set", "--match-set", listName, "src", "-j", "REJECT")
   222  				cmd.WithStderr()
   223  				err := cmd.Run()
   224  				if err != nil {
   225  					return fmt.Errorf("iptables add rule: %w, output: %s", err, cmd.Stderr())
   226  				}
   227  			}
   228  		}
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  func (this *IPSetAction) AddItem(listType IPListType, item *pb.IPItem) error {
   235  	return this.runAction("addItem", listType, item)
   236  }
   237  
   238  func (this *IPSetAction) DeleteItem(listType IPListType, item *pb.IPItem) error {
   239  	return this.runAction("deleteItem", listType, item)
   240  }
   241  
   242  func (this *IPSetAction) runAction(action string, listType IPListType, item *pb.IPItem) error {
   243  	if item.Type == "all" {
   244  		return nil
   245  	}
   246  	if len(item.IpTo) == 0 {
   247  		return this.runActionSingleIP(action, listType, item)
   248  	}
   249  	cidrList, err := iPv4RangeToCIDRRange(item.IpFrom, item.IpTo)
   250  	if err != nil {
   251  		// 不合法的范围不予处理即可
   252  		return nil
   253  	}
   254  	if len(cidrList) == 0 {
   255  		return nil
   256  	}
   257  	for _, cidr := range cidrList {
   258  		var index = strings.Index(cidr, "/")
   259  		if index <= 0 {
   260  			continue
   261  		}
   262  
   263  		// 只支持/24以下的
   264  		if types.Int(cidr[index+1:]) < 24 {
   265  			continue
   266  		}
   267  
   268  		item.IpFrom = cidr
   269  		item.IpTo = ""
   270  		err := this.runActionSingleIP(action, listType, item)
   271  		if err != nil {
   272  			return err
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  func (this *IPSetAction) SetConfig(config *firewallconfigs.FirewallActionIPSetConfig) {
   279  	this.config = config
   280  }
   281  
   282  func (this *IPSetAction) runActionSingleIP(action string, listType IPListType, item *pb.IPItem) error {
   283  	if item.Type == "all" {
   284  		return nil
   285  	}
   286  
   287  	var listName string
   288  	var isIPv6 = strings.Contains(item.IpFrom, ":")
   289  
   290  	switch listType {
   291  	case IPListTypeWhite:
   292  		if isIPv6 {
   293  			listName = this.config.WhiteNameIPv6
   294  		} else {
   295  			listName = this.config.WhiteName
   296  		}
   297  	case IPListTypeBlack:
   298  		if isIPv6 {
   299  			listName = this.config.BlackNameIPv6
   300  		} else {
   301  			listName = this.config.BlackName
   302  		}
   303  	default:
   304  		// 不支持的类型
   305  		return nil
   306  	}
   307  	if len(listName) == 0 {
   308  		return nil
   309  	}
   310  
   311  	var path = this.config.Path
   312  	var err error
   313  	if len(path) == 0 {
   314  		path, err = executils.LookPath("ipset")
   315  		if err != nil {
   316  			// 找不到ipset命令错误只提示一次
   317  			if this.ipsetNotfound {
   318  				return nil
   319  			}
   320  			this.ipsetNotfound = true
   321  			return err
   322  		}
   323  	}
   324  
   325  	// ipset add edge_ip_list 192.168.2.32 timeout 30
   326  	var args = []string{}
   327  	switch action {
   328  	case "addItem":
   329  		args = append(args, "add")
   330  	case "deleteItem":
   331  		args = append(args, "del")
   332  	}
   333  
   334  	args = append(args, listName, item.IpFrom)
   335  	if action == "addItem" {
   336  		var timestamp = time.Now().Unix()
   337  		if item.ExpiredAt > timestamp {
   338  			args = append(args, "timeout", strconv.FormatInt(item.ExpiredAt-timestamp, 10))
   339  		}
   340  	}
   341  
   342  	if runtime.GOOS == "darwin" {
   343  		// MAC OS直接返回
   344  		return nil
   345  	}
   346  
   347  	var cmd = executils.NewTimeoutCmd(30*time.Second, path, args...)
   348  	cmd.WithStderr()
   349  	err = cmd.Run()
   350  	if err != nil {
   351  		var errString = cmd.Stderr()
   352  		if action == "deleteItem" && strings.Contains(errString, "not added") {
   353  			return nil
   354  		}
   355  		return errors.New(strings.TrimSpace(errString))
   356  	}
   357  	return nil
   358  }