From c3b77652e69f399779b48991cc0a015e02ea882f Mon Sep 17 00:00:00 2001 From: mlkt <365690226@qq.com> Date: Thu, 25 Jul 2024 04:35:44 +0800 Subject: [PATCH 1/2] feat: add `SRC-MAC` rule --- constant/rule.go | 3 + rules/common/mac.go | 181 ++++++++++++++++++++++++++++++++++++++++++++ rules/parser.go | 2 + 3 files changed, 186 insertions(+) create mode 100644 rules/common/mac.go diff --git a/constant/rule.go b/constant/rule.go index a91ee6cb..c04379f6 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -10,6 +10,7 @@ const ( GEOIP SrcGEOIP IPASN + SrcMAC SrcIPASN IPCIDR SrcIPCIDR @@ -56,6 +57,8 @@ func (rt RuleType) String() string { return "SrcGeoIP" case IPASN: return "IPASN" + case SrcMAC: + return "SrcMAC" case SrcIPASN: return "SrcIPASN" case IPCIDR: diff --git a/rules/common/mac.go b/rules/common/mac.go new file mode 100644 index 00000000..008c975b --- /dev/null +++ b/rules/common/mac.go @@ -0,0 +1,181 @@ +package common + +import ( + "errors" + "github.com/metacubex/mihomo/common/cmd" + "os" + "regexp" + "runtime" + "strings" + "sync" + "time" + + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +var arpTable = make(map[string]string) + +const reloadInterval = 5 * time.Minute + +var startOnce sync.Once +func init() { +} + +type SrcMAC struct { + *Base + mac string + adapter string +} + +func (d *SrcMAC) RuleType() C.RuleType { + return C.SrcMAC +} + +func getLoadArpTableFunc() func() (string, error) { + const ipv6Error = "can't load ipv6 arp table, SRC-MAC rule can't match src ipv6 address" + + getIpv4Only := func() (string, error) { + return cmd.ExecCmd("arp -a") + } + + switch runtime.GOOS { + case "linux": + result, err := cmd.ExecCmd("ip --help") + if err != nil { + result += err.Error() + } + if strings.Contains(result, "neigh") && strings.Contains(result, "inet6") { + return func() (string, error) { + return cmd.ExecCmd("ip -s neigh show") + } + } else { + log.Warnln(ipv6Error) + const arpPath = "/proc/net/arp" + if file, err := os.Open(arpPath); err == nil { + defer file.Close() + return func() (string, error) { + data, err := os.ReadFile(arpPath) + if err != nil { + return "", err + } + return string(data), nil + } + } else { + return func() (string, error) { + return cmd.ExecCmd("arp -a -n") + } + } + } + + case "windows": + getIpv6ArpWindows := func() (string, error) { + return cmd.ExecCmd("netsh interface ipv6 show neighbors") + } + result, err := getIpv6ArpWindows() + if err != nil || !strings.Contains(result, "----") { + log.Warnln(ipv6Error) + return getIpv4Only + } + return func() (string, error) { + result, err := cmd.ExecCmd("netsh interface ipv4 show neighbors") + if err != nil { + return "", err + } + ipv6Result, err := getIpv6ArpWindows() + if err == nil { + result += ipv6Result + } + return result, nil + } + + default: + log.Warnln(ipv6Error) + return getIpv4Only + } +} + +func (d *SrcMAC) Match(metadata *C.Metadata) (bool, string) { + table := getArpTable() + srcIP := metadata.SrcIP.String() + mac, exists := table[srcIP] + if exists { + if mac == d.mac { + return true, d.adapter + } + } else { + log.Warnln("can't find the IP address in arp table: %s", srcIP) + } + return false, d.adapter +} + +func (d *SrcMAC) Adapter() string { + return d.adapter +} + +func (d *SrcMAC) Payload() string { + return d.mac +} + +var macRegex = regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`) + +func NewMAC(mac string, adapter string) (*SrcMAC, error) { + macAddr := strings.ReplaceAll(strings.ToLower(mac), "-", ":") + if !macRegex.MatchString(macAddr) { + return nil, errors.New("mac address format error: " + mac) + } + return &SrcMAC{ + Base: &Base{}, + mac: macAddr, + adapter: adapter, + }, nil +} + +var arpMapRegex = regexp.MustCompile(`((([0-9]{1,3}\.){3}[0-9]{1,3})|(\b[0-9a-fA-F:].*?:.*?))\s.*?\b(([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2})\b`) + +func getArpTable() map[string]string { + startOnce.Do(func() { + loadArpTable := getLoadArpTableFunc() + table, err := reloadArpTable(loadArpTable) + if err == nil { + arpTable = table + } else { + log.Errorln("init arp table failed: %s", err) + } + timer := time.NewTimer(reloadInterval) + go func() { + for { + <-timer.C + table, err := reloadArpTable(loadArpTable) + if err == nil { + arpTable = table + } else { + log.Errorln("reload arp table failed: %s", err) + } + timer.Reset(reloadInterval) + } + }() + }) + return arpTable +} + +func reloadArpTable(loadArpFunc func() (string, error)) (map[string]string, error) { + result, err := loadArpFunc() + if err != nil { + return nil, err + } + newArpTable := make(map[string]string) + for _, line := range strings.Split(result, "\n") { + matches := arpMapRegex.FindStringSubmatch(line) + if matches == nil || len(matches) <= 0 { + continue + } + ip := matches[1] + mac := strings.ToLower(matches[5]) + if strings.Contains(mac, "-") { + mac = strings.ReplaceAll(mac, "-", ":") + } + newArpTable[ip] = mac + } + return newArpTable, nil +} diff --git a/rules/parser.go b/rules/parser.go index 9b1f5520..12bc2710 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -29,6 +29,8 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "IP-ASN": noResolve := RC.HasNoResolve(params) parsed, parseErr = RC.NewIPASN(payload, target, false, noResolve) + case "SRC-MAC": + parsed, parseErr = RC.NewMAC(payload, target) case "SRC-IP-ASN": parsed, parseErr = RC.NewIPASN(payload, target, true, true) case "IP-CIDR", "IP-CIDR6": From d1a78581aaa57e3fba5a187760efc354a4c0e0b6 Mon Sep 17 00:00:00 2001 From: mlkt <365690226@qq.com> Date: Wed, 7 Aug 2024 21:46:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=94=AF=E6=8C=81DST-MAC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- constant/rule.go | 9 ++++++--- rules/common/mac.go | 46 ++++++++++++++++++++++++++++----------------- rules/parser.go | 6 ++++-- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/constant/rule.go b/constant/rule.go index c04379f6..6550f5c4 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,8 +9,9 @@ const ( GEOSITE GEOIP SrcGEOIP - IPASN SrcMAC + DstMAC + IPASN SrcIPASN IPCIDR SrcIPCIDR @@ -55,10 +56,12 @@ func (rt RuleType) String() string { return "GeoIP" case SrcGEOIP: return "SrcGeoIP" - case IPASN: - return "IPASN" case SrcMAC: return "SrcMAC" + case DstMAC: + return "DstMAC" + case IPASN: + return "IPASN" case SrcIPASN: return "SrcIPASN" case IPCIDR: diff --git a/rules/common/mac.go b/rules/common/mac.go index 008c975b..902a018c 100644 --- a/rules/common/mac.go +++ b/rules/common/mac.go @@ -19,21 +19,27 @@ var arpTable = make(map[string]string) const reloadInterval = 5 * time.Minute var startOnce sync.Once + func init() { } -type SrcMAC struct { +type MacAddr struct { *Base - mac string - adapter string + mac string + adapter string + isSourceIP bool } -func (d *SrcMAC) RuleType() C.RuleType { - return C.SrcMAC +func (d *MacAddr) RuleType() C.RuleType { + if d.isSourceIP { + return C.SrcMAC + } else { + return C.DstMAC + } } func getLoadArpTableFunc() func() (string, error) { - const ipv6Error = "can't load ipv6 arp table, SRC-MAC rule can't match src ipv6 address" + const ipv6Error = "can't load ipv6 arp table, SRC-MAC/DST-MAC rule can't match src ipv6 address" getIpv4Only := func() (string, error) { return cmd.ExecCmd("arp -a") @@ -95,39 +101,45 @@ func getLoadArpTableFunc() func() (string, error) { } } -func (d *SrcMAC) Match(metadata *C.Metadata) (bool, string) { +func (d *MacAddr) Match(metadata *C.Metadata) (bool, string) { table := getArpTable() - srcIP := metadata.SrcIP.String() - mac, exists := table[srcIP] + var ip string + if d.isSourceIP { + ip = metadata.SrcIP.String() + } else { + ip = metadata.DstIP.String() + } + mac, exists := table[ip] if exists { if mac == d.mac { return true, d.adapter } } else { - log.Warnln("can't find the IP address in arp table: %s", srcIP) + log.Infoln("can't find the IP address in arp table: %s", ip) } return false, d.adapter } -func (d *SrcMAC) Adapter() string { +func (d *MacAddr) Adapter() string { return d.adapter } -func (d *SrcMAC) Payload() string { +func (d *MacAddr) Payload() string { return d.mac } var macRegex = regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`) -func NewMAC(mac string, adapter string) (*SrcMAC, error) { +func NewMAC(mac string, adapter string, isSrc bool) (*MacAddr, error) { macAddr := strings.ReplaceAll(strings.ToLower(mac), "-", ":") if !macRegex.MatchString(macAddr) { return nil, errors.New("mac address format error: " + mac) } - return &SrcMAC{ - Base: &Base{}, - mac: macAddr, - adapter: adapter, + return &MacAddr{ + Base: &Base{}, + mac: macAddr, + adapter: adapter, + isSourceIP: isSrc, }, nil } diff --git a/rules/parser.go b/rules/parser.go index 12bc2710..77a99ba5 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -26,11 +26,13 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] parsed, parseErr = RC.NewGEOIP(payload, target, false, noResolve) case "SRC-GEOIP": parsed, parseErr = RC.NewGEOIP(payload, target, true, true) + case "SRC-MAC": + parsed, parseErr = RC.NewMAC(payload, target, true) + case "DST-MAC": + parsed, parseErr = RC.NewMAC(payload, target, false) case "IP-ASN": noResolve := RC.HasNoResolve(params) parsed, parseErr = RC.NewIPASN(payload, target, false, noResolve) - case "SRC-MAC": - parsed, parseErr = RC.NewMAC(payload, target) case "SRC-IP-ASN": parsed, parseErr = RC.NewIPASN(payload, target, true, true) case "IP-CIDR", "IP-CIDR6":