diff --git a/constant/rule.go b/constant/rule.go index 31702ddc..bfb52528 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -34,6 +34,8 @@ const ( AND OR NOT + SrcMAC + Schedule ) type RuleType int @@ -104,6 +106,10 @@ func (rt RuleType) String() string { return "OR" case NOT: return "NOT" + case SrcMAC: + return "SrcMAC" + case Schedule: + return "Schedule" default: return "Unknown" } diff --git a/go.mod b/go.mod index ff870121..157aff1b 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect diff --git a/go.sum b/go.sum index d7b3c640..b7dc67c5 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/rules/common/mac.go b/rules/common/mac.go new file mode 100644 index 00000000..3d391a78 --- /dev/null +++ b/rules/common/mac.go @@ -0,0 +1,127 @@ +package common + +import ( + "bytes" + "os/exec" + "regexp" + "runtime" + "strings" + "time" + + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/patrickmn/go-cache" + "golang.org/x/net/idna" +) + +var lc = cache.New(5*time.Minute, 10*time.Minute) +var arpCommand = "arp" +var arpVar = "-a" + +func init() { + switch os := runtime.GOOS; os { + case "linux": + arpCommand = "cat" + arpVar = "/proc/net/arp" + case "windows": + + default: + + } +} + +type SrcMAC struct { + *Base + mac string + adapter string +} + +func (d *SrcMAC) RuleType() C.RuleType { + return C.SrcMAC +} + +func (d *SrcMAC) Match(metadata *C.Metadata) (bool, string) { + arpTable, err := getARPTable(false) + if err != nil { + log.Errorln("can't initial arp table: %s", err) + return false, "" + } + + mac, exists := arpTable[metadata.SrcIP.String()] + if exists { + if mac == d.mac { + return true, d.adapter + } + } else { + arpTable, err := getARPTable(true) + if err != nil { + log.Errorln("can't initial arp table: %s", err) + return false, "" + } + mac, exists := arpTable[metadata.SrcIP.String()] + if exists { + if mac == d.mac { + return true, d.adapter + } + } else { + log.Errorln("can't find the IP address in arp table: %s", metadata.SrcIP.String()) + } + } + + return false, d.adapter +} + +func (d *SrcMAC) Adapter() string { + return d.adapter +} + +func (d *SrcMAC) Payload() string { + return d.mac +} + +func NewMAC(mac string, adapter string) *SrcMAC { + punycode, _ := idna.ToASCII(strings.ToLower(mac)) + return &SrcMAC{ + Base: &Base{}, + mac: punycode, + adapter: adapter, + } +} + +func getARPTable(forceReload bool) (map[string]string, error) { + + item, found := lc.Get("arpTable") + if found && !forceReload { + arpTable := item.(map[string]string) + //log.Infoln("get arpTable from cache") + return arpTable, nil + } + + // 执行arp命令 + cmd := exec.Command(arpCommand, arpVar) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + ipRegex := regexp.MustCompile(`(([0-9]{1,3}\.){3}[0-9]{1,3})`) + macRegex := regexp.MustCompile(`(?i)(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}`) + + // 解析arp命令的输出 + arpTable := make(map[string]string) + lines := strings.Split(out.String(), "\n") + for _, line := range lines { + ip := ipRegex.FindString(line) + mac := macRegex.FindString(line) + + if len(ip) > 0 && len(mac) > 0 { + punycode, _ := idna.ToASCII(strings.ToLower(mac)) + arpTable[ip] = punycode + } + } + lc.Set("arpTable", arpTable, cache.DefaultExpiration) + return arpTable, nil +} + +//var _ C.Rule = (*Mac)(nil) diff --git a/rules/common/schedule.go b/rules/common/schedule.go new file mode 100644 index 00000000..44166c63 --- /dev/null +++ b/rules/common/schedule.go @@ -0,0 +1,117 @@ +package common + +import ( + "fmt" + "strconv" + "strings" + "time" + + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "golang.org/x/net/idna" +) + +// make sure the system has install related zoneinfo for local time zone! +func init() { + log.Infoln("current system time is %s", time.Now().Format("2006-01-02 15:04:05")) +} + +type Schedule struct { + *Base + weekDayArr [7]bool + startHour int + startMinute int + endHour int + endMinute int + schedule string + adapter string +} + +func (d *Schedule) RuleType() C.RuleType { + return C.Schedule +} + +func (d *Schedule) Match(metadata *C.Metadata) (bool, string) { + now := time.Now() + //log.Infoln("system time is %", now.Format("2006-01-02 15:04:05.000 Mon Jan")) + if d.weekDayArr[now.Weekday()] { + startTime := time.Date(now.Year(), now.Month(), now.Day(), d.startHour, d.startMinute, 0, 0, now.Location()) + endTime := time.Date(now.Year(), now.Month(), now.Day(), d.endHour, d.endMinute, 59, 999999999, now.Location()) + if now.After(startTime) && now.Before(endTime) { + //log.Infoln("src ip %s in the time %d:%d~%d:%d. adapter is %s.", metadata.SrcIP.String(), d.startHour, d.startMinute, d.endHour, d.endMinute, d.adapter) + return true, d.adapter + } + } + return false, d.adapter +} + +func (d *Schedule) Adapter() string { + return d.adapter +} + +func (d *Schedule) Payload() string { + return d.schedule +} + +func NewSchedule(schedule string, adapter string) (*Schedule, error) { + punycode, _ := idna.ToASCII(strings.ToUpper(schedule)) + weekDayArr := [7]bool{false, false, false, false, false, false, false} + if len(punycode) != 19 { + return nil, fmt.Errorf("could you initial Schedule rule %s, the rule format is not correct!", punycode) + } + if punycode[0] == 'S' { + weekDayArr[0] = true + } + if punycode[1] == 'M' { + weekDayArr[1] = true + } + if punycode[2] == 'T' { + weekDayArr[2] = true + } + if punycode[3] == 'W' { + weekDayArr[3] = true + } + if punycode[4] == 'T' { + weekDayArr[4] = true + } + if punycode[5] == 'F' { + weekDayArr[5] = true + } + if punycode[6] == 'S' { + weekDayArr[6] = true + } + startHour, err := strconv.Atoi(punycode[8:10]) + if err != nil { + return nil, fmt.Errorf("could you initial Schedule rule %s, the time format is not correct!", punycode) + } + startMinute, err := strconv.Atoi(punycode[11:13]) + if err != nil { + return nil, fmt.Errorf("could you initial Schedule rule %s, the time format is not correct!", punycode) + } + endHour, err := strconv.Atoi(punycode[14:16]) + if err != nil { + return nil, fmt.Errorf("could you initial Schedule rule %s, the time format is not correct!", punycode) + } + endMinute, err := strconv.Atoi(punycode[17:19]) + if err != nil { + return nil, fmt.Errorf("could you initial Schedule rule %s, the time format is not correct!", punycode) + } + if startHour > endHour { + return nil, fmt.Errorf("could you initial Schedule rule %s, the end time should not be earlier than start time!", punycode) + } + if startHour == endHour && startMinute > endMinute { + return nil, fmt.Errorf("could you initial Schedule rule %s, the end time should not be earlier than start time!", punycode) + } + return &Schedule{ + Base: &Base{}, + weekDayArr: weekDayArr, + startHour: startHour, + startMinute: startMinute, + endHour: endHour, + endMinute: endMinute, + schedule: punycode, + adapter: adapter, + }, nil +} + +//var _ C.Rule = (*Schedule)(nil) diff --git a/rules/logic/logic.go b/rules/logic/logic.go index 6e672852..bfc3307f 100644 --- a/rules/logic/logic.go +++ b/rules/logic/logic.go @@ -6,10 +6,10 @@ import ( "strings" "sync" + list "github.com/bahlo/generic-list-go" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/rules/common" - - list "github.com/bahlo/generic-list-go" ) type Logic struct { diff --git a/rules/parser.go b/rules/parser.go index 4f7ddbe1..aa8481f1 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -75,6 +75,10 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] parsed, parseErr = logic.NewOR(payload, target, ParseRule) case "NOT": parsed, parseErr = logic.NewNOT(payload, target, ParseRule) + case "SRC-MAC": + parsed = RC.NewMAC(payload, target) + case "SCHEDULE": + parsed, parseErr = RC.NewSchedule(payload, target) case "RULE-SET": isSrc, noResolve := RC.ParseParams(params) parsed, parseErr = RP.NewRuleSet(payload, target, isSrc, noResolve)