diff --git a/component/js/function.go b/component/js/function.go new file mode 100644 index 00000000..55b8524a --- /dev/null +++ b/component/js/function.go @@ -0,0 +1,69 @@ +//go:build !no_script + +package js + +import ( + "github.com/Dreamacro/clash/component/resolver" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" + "net/netip" +) + +type Context struct { + runtime *goja.Runtime +} + +func (c *Context) Resolve(host string, dnsType C.DnsType) []string { + var ips []string + var ipAddrs []netip.Addr + var err error + switch dnsType { + case C.IPv4: + ipAddrs, err = resolver.ResolveAllIPv4(host) + case C.IPv6: + ipAddrs, err = resolver.ResolveAllIPv6(host) + case C.All: + ipAddrs, err = resolver.ResolveAllIP(host) + } + + if err != nil { + log.Errorln("Script resolve %s failed, error: %v", host, err) + return ips + } + + for _, addr := range ipAddrs { + ips = append(ips, addr.String()) + } + + return ips +} + +func newContext() require.ModuleLoader { + return func(runtime *goja.Runtime, object *goja.Object) { + ctx := Context{ + runtime: runtime, + } + + o := object.Get("exports").(*goja.Object) + o.Set("resolve", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 1 { + return runtime.ToValue([]string{}) + } + + host := call.Argument(0).String() + dnsType := C.IPv4 + if len(call.Arguments) == 2 { + dnsType = int(call.Argument(1).ToInteger()) + } + + ips := ctx.Resolve(host, C.DnsType(dnsType)) + return runtime.ToValue(ips) + }) + } +} + +func enable(rt *goja.Runtime) { + rt.Set("context", require.Require(rt, "context")) +} diff --git a/component/js/js.go b/component/js/js.go new file mode 100644 index 00000000..110c447e --- /dev/null +++ b/component/js/js.go @@ -0,0 +1,60 @@ +//go:build !no_script + +package js + +import ( + "github.com/Dreamacro/clash/log" + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/console" + "github.com/dop251/goja_nodejs/eventloop" + "github.com/dop251/goja_nodejs/require" +) + +func init() { + logPrinter := console.RequireWithPrinter(&JsLog{}) + require.RegisterNativeModule("console", logPrinter) + contextFuncLoader := newContext() + require.RegisterNativeModule("context", contextFuncLoader) +} + +func preSetting(rt *goja.Runtime) { + registry := new(require.Registry) + registry.Enable(rt) + + console.Enable(rt) + enable(rt) + eventloop.EnableConsole(true) +} + +func getLoop() *eventloop.EventLoop { + loop := eventloop.NewEventLoop(func(loop *eventloop.EventLoop) { + loop.Run(func(runtime *goja.Runtime) { + preSetting(runtime) + }) + }) + + return loop +} + +func compiler(name, code string) (*goja.Program, error) { + return goja.Compile(name, code, false) +} + +func run(loop *eventloop.EventLoop, program *goja.Program, args map[string]any, callback func(any, error)) { + loop.Run(func(runtime *goja.Runtime) { + for k, v := range args { + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + err := runtime.Set(k, v) + if err != nil { + log.Errorln("Args to script failed, %s", err.Error()) + } + } + + v, err := runtime.RunProgram(program) + if v == nil { + callback(nil, err) + } else { + callback(v.Export(), err) + } + }) +} diff --git a/component/js/log.go b/component/js/log.go new file mode 100644 index 00000000..3f839814 --- /dev/null +++ b/component/js/log.go @@ -0,0 +1,20 @@ +//go:build !no_script + +package js + +import "github.com/Dreamacro/clash/log" + +type JsLog struct { +} + +func (j JsLog) Log(s string) { + log.Infoln("[JS] %s", s) +} + +func (j JsLog) Warn(s string) { + log.Warnln("[JS] %s", s) +} + +func (j JsLog) Error(s string) { + log.Errorln("[JS] %s", s) +} diff --git a/component/js/script.go b/component/js/script.go new file mode 100644 index 00000000..3fafc36f --- /dev/null +++ b/component/js/script.go @@ -0,0 +1,34 @@ +//go:build !no_script + +package js + +import ( + "github.com/dop251/goja" + "sync" +) + +var JS sync.Map +var mux sync.Mutex + +func NewJS(name, code string) error { + program, err := compiler(name, code) + if err != nil { + return err + } + + if _, ok := JS.Load(name); !ok { + mux.Lock() + defer mux.Unlock() + if _, ok := JS.Load(name); !ok { + JS.Store(name, program) + } + } + + return nil +} + +func Run(name string, args map[string]any, callback func(any, error)) { + if value, ok := JS.Load(name); ok { + run(getLoop(), value.(*goja.Program), args, callback) + } +} diff --git a/component/js/script_no_script.go b/component/js/script_no_script.go new file mode 100644 index 00000000..6a190ef5 --- /dev/null +++ b/component/js/script_no_script.go @@ -0,0 +1,12 @@ +//go:build no_script + +package js + +import "fmt" + +func NewJS(name, code string) error { + fmt.Errorf("unsupported script on the build") +} + +func Run(name string, args map[string]any, callback func(any, error)) { +} diff --git a/constant/rule.go b/constant/rule.go index 2cc60cdd..535f0264 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -14,6 +14,7 @@ const ( SrcPort DstPort Process + Script ProcessPath RuleSet Network @@ -61,6 +62,8 @@ func (rt RuleType) String() string { return "RuleSet" case Network: return "Network" + case Script: + return "Script" case Uid: return "Uid" case INTYPE: diff --git a/constant/script.go b/constant/script.go new file mode 100644 index 00000000..74454779 --- /dev/null +++ b/constant/script.go @@ -0,0 +1,27 @@ +package constant + +type JSRuleMetadata struct { + Type string `json:"type"` + Network string `json:"network"` + Host string `json:"host"` + SrcIP string `json:"srcIP"` + DstIP string `json:"dstIP"` + SrcPort string `json:"srcPort"` + DstPort string `json:"dstPort"` + Uid *int32 `json:"uid"` + Process string `json:"process"` + ProcessPath string `json:"processPath"` +} + +type DnsType int + +const ( + IPv4 = 1 << iota + IPv6 + All +) + +type JSFunction interface { + //Resolve host to ip by Clash DNS + Resolve(host string, resolveType DnsType) []string +} diff --git a/go.mod b/go.mod index b99d1f06..73c75ceb 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.18 require ( github.com/Dreamacro/go-shadowsocks2 v0.1.8 - github.com/dlclark/regexp2 v1.4.0 + github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 + github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41 + github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.1 @@ -38,9 +40,9 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/google/btree v1.0.1 // indirect - github.com/kr/pretty v0.2.1 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect diff --git a/go.sum b/go.sum index 49033985..0d78cb61 100644 --- a/go.sum +++ b/go.sum @@ -20,11 +20,18 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41 h1:yRPjAkkuR/E/tsVG7QmhzEeEtD3P2yllxsT1/ftURb0= +github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -42,6 +49,8 @@ github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vz github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= @@ -100,12 +109,14 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucas-clemente/quic-go v0.27.0 h1:v6WY87q9zD4dKASbG8hy/LpzAVNzEQzw8sEIeloJsc4= github.com/lucas-clemente/quic-go v0.27.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -159,6 +170,8 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= @@ -318,6 +331,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab h1:eHo2TTVBaAPw9lDGK2Gb9GyPMXT6g7O63W6sx3ylbzU= golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -376,8 +390,10 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/rule/common/script.go b/rule/common/script.go new file mode 100644 index 00000000..55f422b7 --- /dev/null +++ b/rule/common/script.go @@ -0,0 +1,77 @@ +package common + +import ( + "github.com/Dreamacro/clash/component/js" + C "github.com/Dreamacro/clash/constant" + "github.com/gofrs/uuid" +) + +type Script struct { + *Base + adapter string + name string +} + +func (s *Script) RuleType() C.RuleType { + return C.Script +} + +func (s *Script) Match(metadata *C.Metadata) bool { + res := false + js.Run(s.name, map[string]any{ + "metadata": C.JSRuleMetadata{ + Host: metadata.Host, + Network: metadata.NetWork.String(), + Type: metadata.Type.String(), + SrcIP: metadata.SrcIP.String(), + SrcPort: metadata.SrcPort, + DstPort: metadata.DstPort, + Uid: metadata.Uid, + Process: metadata.Process, + ProcessPath: metadata.ProcessPath, + }, + }, func(a any, err error) { + if err != nil { + res = false + } + + r, ok := a.(bool) + if !ok { + res = false + } + res = r + }) + + return res +} + +func (s *Script) Adapter() string { + return s.adapter +} + +func (s *Script) Payload() string { + return s.adapter +} + +func (s *Script) ShouldResolveIP() bool { + return false +} + +func NewScript(script string, adapter string) (*Script, error) { + name, err := uuid.NewV4() + if err != nil { + return nil, err + } + + if err := js.NewJS(name.String(), script); err != nil { + return nil, err + } + + return &Script{ + Base: &Base{}, + adapter: adapter, + name: name.String(), + }, nil +} + +var _ C.Rule = (*Script)(nil) diff --git a/rule/ruleparser/ruleparser.go b/rule/ruleparser/ruleparser.go index 55f63f82..cd7c0a70 100644 --- a/rule/ruleparser/ruleparser.go +++ b/rule/ruleparser/ruleparser.go @@ -43,6 +43,8 @@ func ParseSameRule(tp, payload, target string, params []string) (parsed C.Rule, parsed, parseErr = RC.NewUid(payload, target) case "IN-TYPE": parsed, parseErr = RC.NewInType(payload, target) + case "SCRIPT": + parsed, parseErr = RC.NewScript(payload, target) default: parseErr = fmt.Errorf("unsupported rule type %s", tp) }