diff --git a/constant/rule.go b/constant/rule.go index a91ee6cb07..6550f5c49b 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,6 +9,8 @@ const ( GEOSITE GEOIP SrcGEOIP + SrcMAC + DstMAC IPASN SrcIPASN IPCIDR @@ -54,6 +56,10 @@ func (rt RuleType) String() string { return "GeoIP" case SrcGEOIP: return "SrcGeoIP" + case SrcMAC: + return "SrcMAC" + case DstMAC: + return "DstMAC" case IPASN: return "IPASN" case SrcIPASN: diff --git a/rules/common/mac.go b/rules/common/mac.go new file mode 100644 index 0000000000..902a018c4b --- /dev/null +++ b/rules/common/mac.go @@ -0,0 +1,193 @@ +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 MacAddr struct { + *Base + mac string + adapter string + isSourceIP bool +} + +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/DST-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 *MacAddr) Match(metadata *C.Metadata) (bool, string) { + table := getArpTable() + 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.Infoln("can't find the IP address in arp table: %s", ip) + } + return false, d.adapter +} + +func (d *MacAddr) Adapter() string { + return d.adapter +} + +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, isSrc bool) (*MacAddr, error) { + macAddr := strings.ReplaceAll(strings.ToLower(mac), "-", ":") + if !macRegex.MatchString(macAddr) { + return nil, errors.New("mac address format error: " + mac) + } + return &MacAddr{ + Base: &Base{}, + mac: macAddr, + adapter: adapter, + isSourceIP: isSrc, + }, 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 9b1f552007..77a99ba5ef 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -26,6 +26,10 @@ 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)