├── .github └── workflows │ └── release.yml ├── LICENSE ├── Makefile ├── README.md ├── adapter ├── api.go ├── common.go ├── context.go ├── core.go ├── listener.go ├── log.go ├── plugin_executor.go ├── plugin_matcher.go ├── return_mode.go ├── upstream.go └── workflow.go ├── api └── api.go ├── cmd └── cdns │ ├── main.go │ └── version.go ├── config.yaml ├── constant ├── debug.go └── version.go ├── core ├── core.go ├── options.go └── upstream.go ├── docs ├── api │ └── api.md ├── example.md ├── global.md ├── index.md ├── listener │ ├── http.md │ ├── index.md │ ├── quic.md │ ├── tcp.md │ ├── tls.md │ └── udp.md ├── log │ └── log.md ├── ntp.md ├── plugin │ ├── executor │ │ ├── ecs.md │ │ ├── index.md │ │ ├── ipset.md │ │ ├── memcache.md │ │ ├── rdns.md │ │ ├── rediscache.md │ │ └── script.md │ └── matcher │ │ ├── domain.md │ │ ├── geosite.md │ │ ├── index.md │ │ ├── ip.md │ │ ├── maxminddb.md │ │ └── script.md ├── upstream │ ├── dhcp.md │ ├── hosts.md │ ├── https.md │ ├── index.md │ ├── parallel.md │ ├── querytest.md │ ├── quic.md │ ├── random.md │ ├── tcp.md │ ├── tls.md │ └── udp.md └── workflow │ ├── executor.md │ ├── index.md │ └── matcher.md ├── github_action.sh ├── go.mod ├── go.sum ├── listener ├── common.go ├── http.go ├── listener.go ├── quic.go ├── tcp.go ├── tls.go └── udp.go ├── log ├── broadcast.go ├── level.go ├── log.go ├── nop.go ├── simple.go └── tag.go ├── main.go ├── mkdocs.yml ├── ntp ├── ntp.go ├── time_stub.go ├── time_unix.go └── time_windows.go ├── plugin ├── executor.go ├── executor │ ├── ecs │ │ └── ecs.go │ ├── init.go │ ├── ipset │ │ ├── internal │ │ │ ├── ipset.go │ │ │ ├── ipset_linux.go │ │ │ └── ipset_other.go │ │ └── ipset.go │ ├── memcache │ │ ├── cachemap.go │ │ └── memcache.go │ ├── rdns │ │ └── rdns.go │ ├── rediscache │ │ └── rediscache.go │ └── script │ │ └── script.go ├── matcher.go └── matcher │ ├── domain │ └── domain.go │ ├── geosite │ ├── geosite.go │ ├── meta │ │ └── meta.go │ ├── meta_stub │ │ └── stub.go │ ├── sing │ │ ├── reader.go │ │ └── rule.go │ ├── sing_stub │ │ └── stub.go │ ├── v2xray │ │ └── v2xray.go │ └── v2xray_stub │ │ └── stub.go │ ├── init.go │ ├── ip │ └── ip.go │ ├── maxminddb │ ├── maxminddb.go │ └── reader.go │ └── script │ └── script.go ├── test ├── core.go ├── listener_test.go ├── server-cert.pem ├── server-key.pem └── upstream_test.go ├── upstream ├── bootstrap │ └── bootstrap.go ├── common.go ├── dhcp.go ├── fallback.go ├── hosts.go ├── https.go ├── parallel.go ├── pipeline │ ├── pipeline.go │ └── pool.go ├── pool │ └── pool.go ├── querytest.go ├── quic.go ├── random.go ├── tcp.go ├── tls.go ├── udp.go └── upstream.go ├── utils ├── chan.go ├── chi.go ├── compare.go ├── context.go ├── decode.go ├── dns.go ├── domain │ ├── domain.go │ └── trie.go ├── duration.go ├── graph.go ├── limit.go ├── listable.go ├── netip.go ├── network │ ├── basic │ │ ├── basic.go │ │ └── control │ │ │ ├── bind.go │ │ │ ├── bind_darwin.go │ │ │ ├── bind_linux.go │ │ │ ├── bind_other.go │ │ │ ├── bind_windows.go │ │ │ ├── control.go │ │ │ ├── mark_linux.go │ │ │ ├── mark_other.go │ │ │ ├── reuse_other.go │ │ │ ├── reuse_unix.go │ │ │ └── reuse_windows.go │ ├── common │ │ ├── dialer.go │ │ └── socksaddr.go │ ├── dialer.go │ ├── netinterface │ │ ├── interface.go │ │ ├── interface_darwin.go │ │ ├── interface_linux.go │ │ ├── interface_other.go │ │ ├── interface_windows.go │ │ └── internal │ │ │ └── winipcfg │ │ │ ├── interface_change_handler.go │ │ │ ├── luid.go │ │ │ ├── mksyscall.go │ │ │ ├── netsh.go │ │ │ ├── route_change_handler.go │ │ │ ├── types.go │ │ │ ├── types_32.go │ │ │ ├── types_64.go │ │ │ ├── types_test.go │ │ │ ├── types_test_32.go │ │ │ ├── types_test_64.go │ │ │ ├── unicast_address_change_handler.go │ │ │ ├── winipcfg.go │ │ │ ├── winipcfg_test.go │ │ │ └── zwinipcfg_windows.go │ └── socks5 │ │ ├── protocol.go │ │ ├── socks5.go │ │ └── udp.go ├── queue.go ├── random.go ├── result.go ├── stack.go ├── string.go └── task.go └── workflow ├── item_executor_rule_clean.go ├── item_executor_rule_fallback.go ├── item_executor_rule_go_to.go ├── item_executor_rule_jump_to.go ├── item_executor_rule_mark.go ├── item_executor_rule_metadata.go ├── item_executor_rule_parallel.go ├── item_executor_rule_plugin_executor.go ├── item_executor_rule_return.go ├── item_executor_rule_set_resp_ip.go ├── item_executor_rule_set_ttl.go ├── item_executor_rule_upstream.go ├── item_executor_rule_workflow_rules.go ├── item_matcher_rule_client_ip.go ├── item_matcher_rule_env.go ├── item_matcher_rule_has_resp_msg.go ├── item_matcher_rule_listener.go ├── item_matcher_rule_mark.go ├── item_matcher_rule_match_and.go ├── item_matcher_rule_match_or.go ├── item_matcher_rule_metadata.go ├── item_matcher_rule_plugin_matcher.go ├── item_matcher_rule_qname.go ├── item_matcher_rule_qtype.go ├── item_matcher_rule_resp_ip.go ├── rule.go ├── rule_exec.go ├── rule_item_exec.go ├── rule_item_match.go ├── rule_match_and.go ├── rule_match_or.go └── workflow.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | permissions: 10 | contents: write 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '^1.20.2' 21 | 22 | - name: Build 23 | run: | 24 | chmod +x ./github_action.sh 25 | ./github_action.sh 26 | 27 | - name: Release 28 | uses: softprops/action-gh-release@v1 29 | if: startsWith(github.ref, 'refs/tags/') 30 | with: 31 | files: ./build/* 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022 by yaott 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | 16 | In addition, no derivative work may use the name or imply association 17 | with this application without prior consent. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = cdns 2 | 3 | TAGS = $(shell git describe --tags --long) 4 | 5 | build: 6 | @go build -ldflags "-s -w -X 'github.com/rnetx/cdns/constant.Version=$(TAGS)' -buildid=" -o $(NAME) -v . 7 | 8 | fmt: 9 | @gofumpt -l -w . 10 | @gofmt -s -w . 11 | @gci write --custom-order -s standard -s "prefix(github.com/rnetx/)" -s "default" . 12 | 13 | fmt_install: 14 | go install -v mvdan.cc/gofumpt@latest 15 | go install -v github.com/daixiang0/gci@latest 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cdns 2 | 3 | ```cdns``` 是一个使用 Golang 编写的,高度自定义的 DNS 服务器 4 | 5 | --- 6 | 7 | ### 如何构建 8 | ``` 9 | make build 10 | ``` 11 | - Release 默认包含所有插件 12 | - 如果想去除不需用到的插件,可以编辑 ```plugin/matcher/init.go``` 或 ```plugin/executor/init.go``` 文件 13 | ``` 14 | plugin/matcher/init.go 在不需要的匹配插件前加 “//” 15 | plugin/executor/init.go 在不需要的执行插件前加 “//” 16 | 例如: 17 | _ "path/to/plugin-need" 18 | // _ "path/to/plugin-unneed" 19 | ``` 20 | 21 | ### 文档 22 | 23 | 具体介绍请查看[文档](https://rnetx.github.io/cdns/) 24 | 25 | ## 开源许可证 26 | 27 | cdns 使用 GPL-3.0 开源许可证,详细请参阅 [LICENSE](LICENSE) 文件。 28 | 29 | ## 感谢 30 | 31 | - [miekg/dns](https://github.com/miekg/dns) 32 | - [IrineSistiana/mosdns](https://github.com/IrineSistiana/mosdns) 33 | -------------------------------------------------------------------------------- /adapter/api.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | ) 6 | 7 | type APIHandler interface { 8 | APIHandler() chi.Router 9 | } 10 | -------------------------------------------------------------------------------- /adapter/common.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | type Starter interface { 4 | Start() error 5 | } 6 | 7 | type Closer interface { 8 | Close() error 9 | } 10 | 11 | func Start(v any) error { 12 | starter, isStarter := v.(Starter) 13 | if isStarter { 14 | return starter.Start() 15 | } 16 | return nil 17 | } 18 | 19 | func Close(v any) error { 20 | closer, isCloser := v.(Closer) 21 | if isCloser { 22 | return closer.Close() 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /adapter/context.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "math/rand" 7 | "net/netip" 8 | "time" 9 | 10 | "github.com/logrusorgru/aurora/v4" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | func randomID() uint32 { 15 | start := uint32(math.Pow(10, 8)) 16 | end := uint32(math.Pow(10, 9)) - 1 17 | diff := end - start 18 | return start + uint32(rand.Int63n(int64(diff))) 19 | } 20 | 21 | func idToColor(id uint32) aurora.Color { 22 | var color aurora.Color 23 | color = aurora.Color(uint8(id)) 24 | color %= 215 25 | row := uint(color / 36) 26 | column := uint(color % 36) 27 | var r, g, b float32 28 | r = float32(row * 51) 29 | g = float32(column / 6 * 51) 30 | b = float32((column % 6) * 51) 31 | luma := 0.2126*r + 0.7152*g + 0.0722*b 32 | if luma < 60 { 33 | row = 5 - row 34 | column = 35 - column 35 | color = aurora.Color(row*36 + column) 36 | } 37 | color += 16 38 | color = color << 16 39 | color |= 1 << 14 40 | return color 41 | } 42 | 43 | var _ LogContext = (*DNSContext)(nil) 44 | 45 | type DNSContext struct { 46 | ctx context.Context 47 | initTime time.Time 48 | id uint32 49 | color aurora.Color 50 | // 51 | listener string 52 | clientIP netip.Addr 53 | req *dns.Msg 54 | // 55 | resp *dns.Msg 56 | respUpstreamTag string 57 | mark uint64 58 | metadata map[string]string 59 | } 60 | 61 | func NewDNSContext(ctx context.Context, listener string, clientIP netip.Addr, req *dns.Msg) *DNSContext { 62 | c := &DNSContext{ 63 | ctx: ctx, 64 | initTime: time.Now(), 65 | id: randomID(), 66 | listener: listener, 67 | clientIP: clientIP, 68 | req: req, 69 | } 70 | return c 71 | } 72 | 73 | func (c *DNSContext) ID() uint32 { 74 | return c.id 75 | } 76 | 77 | func (c *DNSContext) SetID(id uint32) { 78 | c.id = id 79 | } 80 | 81 | func (c *DNSContext) Color() aurora.Color { 82 | if c.color == 0 { 83 | c.color = idToColor(c.id) 84 | } 85 | return c.color 86 | } 87 | 88 | func (c *DNSContext) FlushColor() { 89 | c.color = 0 90 | } 91 | 92 | func (c *DNSContext) InitTime() time.Time { 93 | return c.initTime 94 | } 95 | 96 | func (c *DNSContext) Duration() time.Duration { 97 | return time.Since(c.initTime) 98 | } 99 | 100 | func (c *DNSContext) Context() context.Context { 101 | return c.ctx 102 | } 103 | 104 | func (c *DNSContext) Clone() *DNSContext { 105 | newDNSContext := &DNSContext{ 106 | ctx: c.ctx, 107 | initTime: c.initTime, 108 | id: c.id, 109 | color: c.color, 110 | listener: c.listener, 111 | clientIP: c.clientIP, 112 | req: c.req.Copy(), 113 | mark: c.mark, 114 | } 115 | if c.resp != nil { 116 | newDNSContext.resp = c.resp.Copy() 117 | } 118 | if c.metadata != nil && len(c.metadata) > 0 { 119 | newDNSContext.metadata = make(map[string]string) 120 | for k, v := range c.metadata { 121 | newDNSContext.metadata[k] = v 122 | } 123 | } 124 | return newDNSContext 125 | } 126 | 127 | func (c *DNSContext) Listener() string { 128 | return c.listener 129 | } 130 | 131 | func (c *DNSContext) ClientIP() netip.Addr { 132 | return c.clientIP 133 | } 134 | 135 | func (c *DNSContext) ReqMsg() *dns.Msg { 136 | return c.req 137 | } 138 | 139 | func (c *DNSContext) RespMsg() *dns.Msg { 140 | return c.resp 141 | } 142 | 143 | func (c *DNSContext) SetRespMsg(resp *dns.Msg) { 144 | c.resp = resp 145 | } 146 | 147 | func (c *DNSContext) RespUpstreamTag() string { 148 | return c.respUpstreamTag 149 | } 150 | 151 | func (c *DNSContext) SetRespUpstreamTag(tag string) { 152 | c.respUpstreamTag = tag 153 | } 154 | 155 | func (c *DNSContext) Mark() uint64 { 156 | return c.mark 157 | } 158 | 159 | func (c *DNSContext) SetMark(mark uint64) { 160 | c.mark = mark 161 | } 162 | 163 | func (c *DNSContext) Metadata() map[string]string { 164 | if c.metadata == nil { 165 | c.metadata = make(map[string]string) 166 | } 167 | return c.metadata 168 | } 169 | -------------------------------------------------------------------------------- /adapter/core.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import "time" 4 | 5 | type Core interface { 6 | Closer 7 | Run() error 8 | GetListener(tag string) Listener 9 | GetListeners() []Listener 10 | GetUpstream(tag string) Upstream 11 | GetUpstreams() []Upstream 12 | GetWorkflow(tag string) Workflow 13 | GetWorkflows() []Workflow 14 | GetPluginMatcher(tag string) PluginMatcher 15 | GetPluginMatchers() []PluginMatcher 16 | GetPluginExecutor(tag string) PluginExecutor 17 | GetPluginExecutors() []PluginExecutor 18 | GetTimeFunc() func() time.Time 19 | } 20 | -------------------------------------------------------------------------------- /adapter/listener.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "net/netip" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | type Listener interface { 11 | Tag() string 12 | Type() string 13 | Handle(ctx context.Context, req *dns.Msg, clientAddr netip.AddrPort) *dns.Msg 14 | } 15 | -------------------------------------------------------------------------------- /adapter/log.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/logrusorgru/aurora/v4" 8 | ) 9 | 10 | type LogContext interface { 11 | ID() uint32 12 | Color() aurora.Color 13 | Duration() time.Duration 14 | } 15 | 16 | var logCtxKey = (*struct{})(nil) 17 | 18 | func SaveLogContext(ctx context.Context, logContext LogContext) context.Context { 19 | return context.WithValue(ctx, logCtxKey, logContext) 20 | } 21 | 22 | func LoadLogContext(ctx context.Context) LogContext { 23 | if ctx == nil { 24 | return nil 25 | } 26 | v := ctx.Value(logCtxKey) 27 | if v == nil { 28 | return nil 29 | } 30 | c, ok := v.(LogContext) 31 | if ok { 32 | return c 33 | } else { 34 | return nil 35 | } 36 | } 37 | 38 | type APILogContext struct { 39 | initTime time.Time 40 | id uint32 41 | color aurora.Color 42 | } 43 | 44 | func NewAPILogContext() *APILogContext { 45 | return &APILogContext{ 46 | initTime: time.Now(), 47 | id: randomID(), 48 | color: idToColor(randomID()), 49 | } 50 | } 51 | 52 | func (c *APILogContext) ID() uint32 { 53 | return c.id 54 | } 55 | 56 | func (c *APILogContext) Color() aurora.Color { 57 | return c.color 58 | } 59 | 60 | func (c *APILogContext) Duration() time.Duration { 61 | return time.Since(c.initTime) 62 | } 63 | -------------------------------------------------------------------------------- /adapter/plugin_executor.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import "context" 4 | 5 | type PluginExecutor interface { 6 | Tag() string 7 | Type() string 8 | LoadRunningArgs(ctx context.Context, args any) (uint16, error) 9 | Exec(ctx context.Context, dnsCtx *DNSContext, argsID uint16) (ReturnMode, error) 10 | } 11 | -------------------------------------------------------------------------------- /adapter/plugin_matcher.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import "context" 4 | 5 | type PluginMatcher interface { 6 | Tag() string 7 | Type() string 8 | LoadRunningArgs(ctx context.Context, args any) (uint16, error) 9 | Match(ctx context.Context, dnsCtx *DNSContext, argsID uint16) (bool, error) 10 | } 11 | -------------------------------------------------------------------------------- /adapter/return_mode.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | type ReturnMode int 4 | 5 | const ( 6 | ReturnModeUnknown ReturnMode = iota 7 | ReturnModeContinue 8 | ReturnModeReturnAll 9 | ReturnModeReturnOnce 10 | ) 11 | 12 | func (r ReturnMode) String() string { 13 | switch r { 14 | case ReturnModeUnknown: 15 | return "unknown" 16 | case ReturnModeContinue: 17 | return "continue" 18 | case ReturnModeReturnAll: 19 | return "return all" 20 | case ReturnModeReturnOnce: 21 | return "return once" 22 | default: 23 | return "unknown" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /adapter/upstream.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | type Upstream interface { 10 | Tag() string 11 | Type() string 12 | Dependencies() []string 13 | Exchange(ctx context.Context, req *dns.Msg) (*dns.Msg, error) 14 | StatisticalData() map[string]any 15 | } 16 | -------------------------------------------------------------------------------- /adapter/workflow.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import "context" 4 | 5 | type Workflow interface { 6 | Tag() string 7 | Check() error 8 | Exec(ctx context.Context, dnsCtx *DNSContext) (ReturnMode, error) 9 | } 10 | -------------------------------------------------------------------------------- /cmd/cdns/main.go: -------------------------------------------------------------------------------- 1 | package cdns 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "strconv" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/rnetx/cdns/constant" 12 | "github.com/rnetx/cdns/core" 13 | "github.com/rnetx/cdns/log" 14 | "github.com/rnetx/cdns/plugin" 15 | 16 | "github.com/spf13/cobra" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | var MainCommand = &cobra.Command{ 21 | Use: "cdns", 22 | Run: func(_ *cobra.Command, _ []string) { 23 | code := run() 24 | if code != 0 { 25 | os.Exit(code) 26 | } 27 | }, 28 | } 29 | 30 | var configPath string 31 | 32 | func init() { 33 | // 34 | { 35 | e, err := strconv.ParseBool(os.Getenv("CDNS_LISTENER_ENABLE_PANIC")) 36 | if err == nil && e { 37 | constant.ListenerEnablePainc = true 38 | } 39 | } 40 | // 41 | MainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.yaml", "config file path") 42 | MainCommand.AddCommand(versionCommand) 43 | } 44 | 45 | func run() int { 46 | raw, err := os.ReadFile(configPath) 47 | if err != nil { 48 | log.DefaultLogger.Errorf("read config file failed: %s, error: %s", configPath, err) 49 | return 1 50 | } 51 | var options core.Options 52 | err = yaml.Unmarshal(raw, &options) 53 | if err != nil { 54 | log.DefaultLogger.Errorf("parse config file failed: %s, error: %s", configPath, err) 55 | return 1 56 | } 57 | ctx, cancel := context.WithCancel(context.Background()) 58 | defer cancel() 59 | c, coreLogger, err := core.NewCore(ctx, options) 60 | if err != nil { 61 | log.DefaultLogger.Error(err) 62 | return 1 63 | } 64 | coreLogger.Infof("cdns %s", constant.Version) 65 | coreLogger.Infof("plugin matcher: %s", strings.Join(plugin.PluginMatcherTypes(), ", ")) 66 | coreLogger.Infof("plugin executor: %s", strings.Join(plugin.PluginExecutorTypes(), ", ")) 67 | if constant.ListenerEnablePainc { 68 | coreLogger.Infof("debug: listener enable painc") 69 | } 70 | go signalHandle(cancel, coreLogger) 71 | err = c.Run() 72 | if err != nil { 73 | return 1 74 | } 75 | return 0 76 | } 77 | 78 | func signalHandle(cancel context.CancelFunc, logger log.Logger) { 79 | signalChan := make(chan os.Signal, 1) 80 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, os.Interrupt) 81 | <-signalChan 82 | logger.Warn("receive signal, exiting...") 83 | cancel() 84 | } 85 | -------------------------------------------------------------------------------- /cmd/cdns/version.go: -------------------------------------------------------------------------------- 1 | package cdns 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/rnetx/cdns/constant" 8 | "github.com/rnetx/cdns/plugin" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var versionCommand = &cobra.Command{ 14 | Use: "version", 15 | Run: func(_ *cobra.Command, _ []string) { 16 | fmt.Println(fmt.Sprintf("cdns %s", constant.Version)) 17 | fmt.Println(fmt.Sprintf("plugin matcher: %s", strings.Join(plugin.PluginMatcherTypes(), ", "))) 18 | fmt.Println(fmt.Sprintf("plugin executor: %s", strings.Join(plugin.PluginExecutorTypes(), ", "))) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | log: 2 | level: info 3 | 4 | api: 5 | listen: 127.0.0.1:9088 6 | secret: admin 7 | 8 | upstreams: 9 | - tag: AliDNS 10 | type: tls 11 | address: 223.5.5.5 12 | 13 | - tag: OpenDNS 14 | type: tls 15 | address: 208.67.222.222 16 | 17 | - tag: GoogleDNS 18 | type: tls 19 | address: 8.8.8.8 20 | 21 | - tag: WorldDNS # 并发请求,防止应某些原因请求失败 22 | type: parallel 23 | upstreams: 24 | - OpenDNS 25 | - GoogleDNS 26 | 27 | plugin-matchers: 28 | - tag: geosite 29 | type: geosite 30 | args: 31 | type: sing 32 | path: /etc/cdns/geosite.db # 填写 geosite (sing-box) 位置 33 | code: gfw 34 | 35 | plugin-executors: 36 | - tag: cache 37 | type: memcache 38 | args: 39 | dump-file: /tmp/dns.cache 40 | dump-interval: 10s 41 | 42 | workflows: 43 | - tag: main 44 | rules: 45 | - exec: # 若命中缓存,则直接返回缓存结果 46 | - plugin: 47 | tag: cache 48 | args: 49 | mode: restore 50 | return: true 51 | 52 | - match-or: # 屏蔽 AAAA 和 HTTPS 请求 53 | - qtype: 54 | - 28 # AAAA 55 | - HTTPS 56 | exec: 57 | - return: success # 生成空请求 58 | 59 | - match-or: # GFW 列表采用 WorldDNS 查询,否则采用 AliDNS 查询 60 | - plugin: 61 | tag: geosite 62 | args: gfw 63 | exec: 64 | - upstream: WorldDNS 65 | else-exec: 66 | - upstream: AliDNS 67 | 68 | - exec: 69 | - plugin: # 缓存结果 70 | tag: cache 71 | args: 72 | mode: store 73 | 74 | 75 | listeners: 76 | - tag: listener-tcp 77 | type: tcp 78 | listen: :53 79 | workflow: main 80 | 81 | - tag: listener-udp 82 | type: udp 83 | listen: :53 84 | workflow: main 85 | -------------------------------------------------------------------------------- /constant/debug.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var ListenerEnablePainc = false 4 | -------------------------------------------------------------------------------- /constant/version.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var ( 4 | Version = "unknown" 5 | Author = "0xffffharry" 6 | ) 7 | -------------------------------------------------------------------------------- /core/options.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/rnetx/cdns/api" 5 | "github.com/rnetx/cdns/listener" 6 | "github.com/rnetx/cdns/ntp" 7 | "github.com/rnetx/cdns/plugin" 8 | "github.com/rnetx/cdns/upstream" 9 | "github.com/rnetx/cdns/workflow" 10 | ) 11 | 12 | type Options struct { 13 | Log LogOptions `yaml:"log,omitempty"` 14 | API *api.Options `yaml:"api,omitempty"` 15 | Upstreams []upstream.Options `yaml:"upstreams,omitempty"` 16 | Workflows []workflow.WorkflowOptions `yaml:"workflows,omitempty"` 17 | Listeners []listener.Options `yaml:"listeners,omitempty"` 18 | PluginMatchers []plugin.PluginMatcherOptions `yaml:"plugin-matchers,omitempty"` 19 | PluginExecutors []plugin.PluginExecutorOptions `yaml:"plugin-executors,omitempty"` 20 | NTP *ntp.NTPOptions `yaml:"ntp,omitempty"` 21 | } 22 | 23 | type LogOptions struct { 24 | Disabled bool `yaml:"disabled,omitempty"` 25 | Level string `yaml:"level,omitempty"` 26 | Output string `yaml:"output,omitempty"` 27 | DisableTimestamp bool `yaml:"disable-timestamp,omitempty"` 28 | DisableColor bool `yaml:"disable-color,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /core/upstream.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rnetx/cdns/adapter" 7 | "github.com/rnetx/cdns/utils" 8 | ) 9 | 10 | func sortUpstream(upstreams []adapter.Upstream) ([]adapter.Upstream, error) { 11 | nodeMap := make(map[string]*utils.GraphNode[adapter.Upstream]) 12 | for _, u := range upstreams { 13 | nodeMap[u.Tag()] = utils.NewGraphNode(u) 14 | } 15 | for _, u := range upstreams { 16 | node := nodeMap[u.Tag()] 17 | dependencies := u.Dependencies() 18 | if dependencies != nil && len(dependencies) > 0 { 19 | for _, tag := range dependencies { 20 | dpNode, ok := nodeMap[tag] 21 | if !ok { 22 | return nil, fmt.Errorf("upstream [%s] depend on upstream [%s], but upstream [%s] not found", u.Tag(), tag, tag) 23 | } 24 | dpNode.AddNext(node) 25 | node.AddPrev(dpNode) 26 | } 27 | } 28 | } 29 | q := utils.NewQueue[string]() 30 | for _, u := range upstreams { 31 | node := nodeMap[u.Tag()] 32 | if !node.HasPrev() { 33 | q.Push(u.Tag()) 34 | } 35 | } 36 | if q.Len() == 0 { 37 | // Circle 38 | target := upstreams[0] 39 | links := make([]string, 0) 40 | links = append(links, target.Tag()) 41 | findCircle(nodeMap, target, target, &links) 42 | return nil, fmt.Errorf("circle dependencies: %s", circleStr(links)) 43 | } 44 | sorted := make([]adapter.Upstream, 0, len(upstreams)) 45 | for q.Len() > 0 { 46 | data := q.Pop() 47 | node := nodeMap[data] 48 | sorted = append(sorted, node.Data()) 49 | delete(nodeMap, data) 50 | for _, next := range node.Next() { 51 | next.RemovePrev(node) 52 | if !next.HasPrev() { 53 | q.Push(next.Data().Tag()) 54 | } 55 | } 56 | } 57 | if len(sorted) < len(upstreams) { 58 | // Circle 59 | var target adapter.Upstream 60 | for _, v := range nodeMap { 61 | target = v.Data() 62 | break 63 | } 64 | links := make([]string, 0) 65 | links = append(links, target.Tag()) 66 | findCircle(nodeMap, target, target, &links) 67 | return nil, fmt.Errorf("circle dependencies: %s", circleStr(links)) 68 | } 69 | return sorted, nil 70 | } 71 | 72 | func findCircle(nodeMap map[string]*utils.GraphNode[adapter.Upstream], target adapter.Upstream, now adapter.Upstream, links *[]string) { 73 | nowNode := nodeMap[now.Tag()] 74 | for _, next := range nowNode.Next() { 75 | if next.Data() == now { 76 | return 77 | } 78 | var linksCopy []string 79 | for _, v := range *links { 80 | linksCopy = append(linksCopy, v) 81 | } 82 | linksCopy = append(linksCopy, next.Data().Tag()) 83 | findCircle(nodeMap, target, next.Data(), &linksCopy) 84 | if len(linksCopy) > 0 { 85 | *links = linksCopy 86 | return 87 | } 88 | } 89 | *links = nil 90 | } 91 | 92 | func circleStr(links []string) string { 93 | s := "" 94 | if len(links) > 0 { 95 | s += links[0] 96 | for i := 1; i < len(links); i++ { 97 | s += " -> " + links[i] 98 | } 99 | s += " -> " + links[0] 100 | } 101 | return s 102 | } 103 | -------------------------------------------------------------------------------- /docs/api/api.md: -------------------------------------------------------------------------------- 1 | # API 服务器 2 | 3 | ```API``` 服务器支持暴露 HTTP 接口,提供更多功能 4 | 5 | ```yaml 6 | api: 7 | listen: 127.0.0.1:8099 # HTTP 监听地址 8 | secret: admin # 鉴权密码,需设置 Header: Authorization Bearer ${secret} 9 | debug: false # 开启 pprof 10 | ``` 11 | 12 | 路径: 13 | 14 | - ```/debug``` ==> pprof 路径,只有在 debug: true 监听 15 | - ```/upstream``` ==> 获取所有 Upstream API 16 | - ```/upstream/${upstream-tag}``` ==> 获取 Upstream API 信息 17 | - ```/plugin/matcher``` ==> 获取所有 Plugin Matcher API 18 | - ```/plugin/matcher/${plugin-matcher-tag}``` ==> 获取 Plugin Matcher API 信息 19 | - ```/plugin/matcher/${plugin-matcher-tag}/help``` ==> 获取 Plugin Matcher API 所有接口信息 20 | - ```/plugin/executor``` ==> 获取所有 Plugin Executor API 21 | - ```/plugin/executor/${plugin-executor-tag}``` ==> 获取 Plugin Executor API 信息 22 | - ```/plugin/executor/${plugin-executor-tag}/help``` ==> 获取 Plugin Executor API 所有接口信息 23 | 24 | ### Upstream API 25 | 26 | GET /upstream 27 | 28 | 返回值: 29 | ```json5 30 | { 31 | "data": { 32 | "${upstream-tag}": { 33 | "tag": "${upstream-tag}", 34 | "type": "${upstream-type}", 35 | "data": { 36 | "total": 0, # 总请求数 37 | "success": 0 # 成功请求数 38 | } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | GET /upstream/${upstream-tag} 45 | 46 | 返回值: 47 | ```json5 48 | { 49 | "tag": "${upstream-tag}", 50 | "type": "${upstream-type}", 51 | "data": { 52 | "total": 0, # 总请求数 53 | "success": 0 # 成功请求数 54 | } 55 | } 56 | ``` 57 | 58 | ### Plugin Matcher API 59 | 60 | GET /plugin/matcher 61 | 62 | 返回值: 63 | ```json5 64 | { 65 | "data": { 66 | "${plugin-matcher-tag}": { 67 | "tag": "${plugin-matcher-tag}", 68 | "type": "${plugin-matcher-type}" 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | GET /plugin/matcher/${plugin-matcher-tag} 75 | 76 | 返回值: 77 | ```json5 78 | { 79 | "tag": "${plugin-matcher-tag}", 80 | "type": "${plugin-matcher-type}" 81 | } 82 | ``` 83 | 84 | GET /plugin/matcher/${plugin-matcher-tag}/help 85 | 86 | 返回值: 87 | ```json5 88 | { 89 | "/${path}": { 90 | "methods": [...], // GET POST ... 91 | "description": ... // API 介绍 92 | } 93 | } 94 | ``` 95 | 96 | ### Plugin Executor API 97 | 98 | GET /plugin/executor 99 | 100 | 返回值: 101 | ```json5 102 | { 103 | "data": { 104 | "${plugin-executor-tag}": { 105 | "tag": "${plugin-executor-tag}", 106 | "type": "${plugin-executor-type}" 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | GET /plugin/executor/${plugin-executor-tag} 113 | 114 | 返回值: 115 | ```json5 116 | { 117 | "tag": "${plugin-executor-tag}", 118 | "type": "${plugin-executor-type}" 119 | } 120 | ``` 121 | 122 | GET /plugin/executor/${plugin-executor-tag}/help 123 | 124 | 返回值: 125 | ```json5 126 | { 127 | "/${path}": { 128 | "methods": [...], // GET POST ... 129 | "description": ... // API 介绍 130 | } 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | # 示例配置 2 | 3 | ```yaml 4 | log: 5 | level: info 6 | 7 | api: 8 | listen: 127.0.0.1:9088 9 | secret: admin 10 | 11 | upstreams: 12 | - tag: AliDNS 13 | type: tls 14 | address: 223.5.5.5 15 | 16 | - tag: OpenDNS 17 | type: tls 18 | address: 208.67.222.222 19 | 20 | - tag: GoogleDNS 21 | type: tls 22 | address: 8.8.8.8 23 | 24 | - tag: WorldDNS # 并发请求,防止应某些原因请求失败 25 | type: parallel 26 | upstreams: 27 | - OpenDNS 28 | - GoogleDNS 29 | 30 | plugin-matchers: 31 | - tag: geosite 32 | type: geosite 33 | args: 34 | type: sing 35 | path: /etc/cdns/geosite.db # 填写 geosite (sing-box) 位置 36 | code: gfw 37 | 38 | plugin-executors: 39 | - tag: cache 40 | type: memcache 41 | args: 42 | dump-file: /tmp/dns.cache 43 | dump-interval: 10s 44 | 45 | workflows: 46 | - tag: main 47 | rules: 48 | - exec: # 若命中缓存,则直接返回缓存结果 49 | - plugin: 50 | tag: cache 51 | args: 52 | mode: restore 53 | return: true 54 | 55 | - match-or: # 屏蔽 AAAA 和 HTTPS 请求 56 | - qtype: 57 | - 28 # AAAA 58 | - HTTPS 59 | exec: 60 | - return: success # 生成空请求 61 | 62 | - match-or: # GFW 列表采用 WorldDNS 查询,否则采用 AliDNS 查询 63 | - plugin: 64 | tag: geosite 65 | args: gfw 66 | exec: 67 | - upstream: WorldDNS 68 | else-exec: 69 | - upstream: AliDNS 70 | 71 | - exec: 72 | - plugin: # 缓存结果 73 | tag: cache 74 | args: 75 | mode: store 76 | 77 | 78 | listeners: 79 | - tag: listener-tcp 80 | type: tcp 81 | listen: :53 82 | workflow: main 83 | 84 | - tag: listener-udp 85 | type: udp 86 | listen: :53 87 | workflow: main 88 | 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/global.md: -------------------------------------------------------------------------------- 1 | # 配置结构 2 | 3 | ```yaml 4 | log: 5 | 6 | api: 7 | 8 | ntp: 9 | 10 | upstreams: 11 | 12 | plugin-matchers: 13 | 14 | plugin-executors: 15 | 16 | workflows: 17 | 18 | listeners: 19 | ``` 20 | 21 | ### ```log``` 22 | 23 | 日志配置 24 | 25 | ### ```api``` 26 | 27 | ```HTTP API``` 配置 28 | 29 | ### ```ntp``` 30 | 31 | ```NTP``` 服务器配置 32 | 33 | ### ```upstreams``` 34 | 35 | 上游 ```DNS``` 服务器配置 36 | 37 | ### ```plugin-matchers``` 38 | 39 | 匹配器插件 40 | 41 | ### ```plugin-executors``` 42 | 43 | 执行器插件 44 | 45 | ### ```workflows``` 46 | 47 | 处理流程 48 | 49 | ### ```listeners``` 50 | 51 | 监听器 -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | ```cdns``` 是一个使用 Golang 编写的,高度自定义的 DNS 服务器 4 | 5 | 本项目使用 ```GPLv3``` 协议开源 6 | 7 | ## 感谢 8 | 9 | - [miekg/dns](https://github.com/miekg/dns) 10 | - [IrineSistiana/mosdns](https://github.com/IrineSistiana/mosdns) 11 | -------------------------------------------------------------------------------- /docs/listener/http.md: -------------------------------------------------------------------------------- 1 | # HTTP(S|3) 2 | 3 | ```yaml 4 | listeners: 5 | - tag: listener 6 | type: http 7 | deal-timeout: 20s # 处理超时时间 8 | listen: :443 # 监听地址,示例:127.0.0.1:53 [::1]:53 :53(监听[::]:53) 9 | # real-ip-header: X-Real-IP # 从请求头获取真实 IP 的字段,可选,默认为空,cdns 会自动从 X-Real-IP X-Forwarded-For 中获取真实 IP 10 | # trust-ip: 127.0.0.1 # 安全选项,可选,填写则只允许从指定 IP 访问读取真实 IP 11 | # path: /dns-query # 监听路径,可选,默认为 /dns-query 12 | # use-http3: false # 是否启用 HTTP/3,可选,默认为 false,填写 true 则必填 TLS 相关配置 13 | # enable-0rtt: false # 是否启用 0-RTT (QUIC),可选,默认为 false,仅在 use-http3: true 有效 14 | server-cert-file: /path/to/cert.pem # TLS 证书文件,可选,填写则使用 HTTPS 15 | server-key-file: /path/to/key.pem # TLS 私钥文件,可选,填写则使用 HTTPS 16 | # client-ca-file: /path/to/ca.pem # 客户端 CA 证书文件,用于 mTLS,可选,填写则使用 HTTPS 17 | # client-ca-file: # 支持多文件 18 | # - /path/to/ca1.pem 19 | # - /path/to/ca2.pem 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/listener/index.md: -------------------------------------------------------------------------------- 1 | # Listener 监听器 2 | 3 | 监听器会创建 DNS 服务器监听请求 4 | 5 | 目前支持以下监听器: 6 | 7 | - [UDP](udp) 8 | - [TCP](tcp) 9 | - [TLS (DoT | DNS Over TLS)](tls) 10 | - [HTTP(S|3) (DoH | DoH3 | DNS Over HTTPS | DNS Over HTTP/3)](http) 11 | - [QUIC (DoQ | DNS Over QUIC)](quic) 12 | -------------------------------------------------------------------------------- /docs/listener/quic.md: -------------------------------------------------------------------------------- 1 | # QUIC 2 | 3 | ```yaml 4 | listeners: 5 | - tag: listener 6 | type: quic 7 | deal-timeout: 20s # 处理超时时间 8 | listen: :853 # 监听地址,示例:127.0.0.1:53 [::1]:53 :53(监听[::]:53) 9 | idle-timeout: 60s # 连接空闲超时时间 10 | enable-0rtt: false # 是否启用 0-RTT (QUIC) 11 | server-cert-file: /path/to/cert.pem # TLS 证书文件 12 | server-key-file: /path/to/key.pem # TLS 私钥文件 13 | # client-ca-file: /path/to/ca.pem # 客户端 CA 证书文件,用于 mTLS 14 | # client-ca-file: # 支持多文件 15 | # - /path/to/ca1.pem 16 | # - /path/to/ca2.pem 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/listener/tcp.md: -------------------------------------------------------------------------------- 1 | # TCP 2 | 3 | ```yaml 4 | listeners: 5 | - tag: listener 6 | type: tcp 7 | deal-timeout: 20s # 处理超时时间 8 | listen: :6053 # 监听地址,示例:127.0.0.1:53 [::1]:53 :53(监听[::]:53) 9 | idle-timeout: 60s # 连接空闲超时时间 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/listener/tls.md: -------------------------------------------------------------------------------- 1 | # TLS 2 | 3 | ```yaml 4 | listeners: 5 | - tag: listener 6 | type: tls 7 | deal-timeout: 20s # 处理超时时间 8 | listen: :853 # 监听地址,示例:127.0.0.1:53 [::1]:53 :53(监听[::]:53) 9 | idle-timeout: 60s # 连接空闲超时时间 10 | server-cert-file: /path/to/cert.pem # TLS 证书文件 11 | server-key-file: /path/to/key.pem # TLS 私钥文件 12 | # client-ca-file: /path/to/ca.pem # 客户端 CA 证书文件,用于 mTLS 13 | # client-ca-file: # 支持多文件 14 | # - /path/to/ca1.pem 15 | # - /path/to/ca2.pem 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/listener/udp.md: -------------------------------------------------------------------------------- 1 | # UDP 2 | 3 | ```yaml 4 | listeners: 5 | - tag: listener 6 | type: udp 7 | deal-timeout: 20s # 处理超时时间 8 | listen: :6053 # 监听地址,示例:127.0.0.1:53 [::1]:53 :53(监听[::]:53) 9 | 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/log/log.md: -------------------------------------------------------------------------------- 1 | # Log 日志 2 | 3 | ```yaml 4 | log: 5 | disabled: false # 是否禁用日志输出 6 | level: info # 日志等级,可选 debug | info | warn | error | fatal 7 | output: /path/to/file.log # 日志文件,可选 stdout:标准输出 ,stderr:错误输出 8 | disable-timestamp: false # 禁用时间戳信息 9 | disable-color: false # 禁用颜色输出,当 output 为文件时默认禁用 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/ntp.md: -------------------------------------------------------------------------------- 1 | # NTP 服务器 2 | 3 | 当本地服务器因某些原因无法校准正确的时间,而 ```Upstream``` 中含有使用 ```TLS``` 的 ```Upstream``` ,在建立连接时会因为时间不准确而失败,可以通过 NTP 服务器来校准时间。 4 | 5 | ```yaml 6 | ntp: 7 | server: ntp.aliyun.com # NTP 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 upstream 8 | # interval: 600s # 校准时间间隔,默认 30min 9 | # upstream: upstream # 上游服务器标签,用于解析 NTP 服务器地址,仅支持 TCP/UDP ,或者不需要依赖(间接依赖)TLS 的其他上游服务器,配置错误会导致回环,谨慎配置! 10 | # write-to-system: false # 是否将时间写入系统中,仅支持 Unix 或 Windows 系统,可能需要系统高级权限 11 | # 12 | # 以下配置用于 NTP 请求,可选 13 | # 14 | # bind-interface: eth0 # 绑定网卡 15 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 16 | # bind-ipv6: :: # 绑定本地 IPv6 地址 17 | # so-mark: 255 # 设置 SO_MARK (Linux) 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/plugin/executor/ecs.md: -------------------------------------------------------------------------------- 1 | # ECS 2 | 3 | ECS 可以附加 ```edns client subnet``` 记录 4 | 5 | ```yaml 6 | plugin-executors: 7 | - tag: plugin 8 | type: ecs 9 | args: 10 | ipv4: 192.168.1.1 11 | ipv6: 2001:db8::1 12 | mask4: 24 13 | mask6: 60 14 | 15 | workflows: 16 | - tag: default 17 | rules: 18 | - exec: 19 | - plugin: 20 | tag: plugin 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/plugin/executor/index.md: -------------------------------------------------------------------------------- 1 | # Plugin Executor 执行器插件 2 | 3 | ```yaml 4 | plugin-executors: 5 | - tag: plugin # 插件标签 6 | type: ${type} # 插件类型 7 | args: ... # 插件参数 8 | 9 | workflows: 10 | - tag: default 11 | rules: 12 | - match-and: 13 | - plugin: 14 | tag: plugin # 执行器插件标签 15 | args: ... # 运行时插件参数 16 | ``` 17 | 18 | 目前支持的匹配器插件列表: 19 | 20 | - [memcache](memcache) 21 | - [rediscache](rediscache) 22 | - [script](script) 23 | - [ecs](ecs) 24 | - [ipset](ipset) 25 | - [rdns](rdns) 26 | -------------------------------------------------------------------------------- /docs/plugin/executor/ipset.md: -------------------------------------------------------------------------------- 1 | # IPSet 2 | 3 | IPSet 可以将 ```IP``` 添加到 IPSet,仅支持 Linux 4 | 5 | ```yaml 6 | plugin-executors: 7 | - tag: plugin 8 | type: ipset 9 | args: 10 | name4: set4 # IPv4 IPSet 名称 11 | name6: set6 # IPv6 IPSet 名称 12 | mask4: 32 # IPv4 IPSet 掩码 13 | mask6: 128 # IPv6 IPSet 掩码 14 | ttl4: 600s # IPv4 IPSet TTL 15 | ttl6: 600s # IPv6 IPSet TTL 16 | create4: false # 是否在启动时创建 17 | create6: false # 是否在启动时创建 18 | destroy4: false # 是否在停止时销毁 19 | destroy6: false # 是否在停止时销毁 20 | 21 | workflows: 22 | - tag: default 23 | rules: 24 | - exec: 25 | - plugin: 26 | tag: plugin 27 | # args: 28 | # use-client-ip: false # 使用客户端 IP,而非 DNS 返回的 IP 29 | ``` 30 | 31 | ### API 32 | 33 | GET /flush 34 | 35 | 清空 IPSet 36 | 37 | 返回状态:204 38 | -------------------------------------------------------------------------------- /docs/plugin/executor/memcache.md: -------------------------------------------------------------------------------- 1 | # MemCache 缓存 2 | 3 | MemCache 缓存可以缓存返回的结果,提高性能 4 | 5 | ```yaml 6 | plugin-executors: 7 | - tag: plugin 8 | type: memcache 9 | args: 10 | dump-path: /path/to/rule # 缓存文件,可选 11 | dump-interval: 0 # 自动缓存时间间隔 12 | 13 | workflows: 14 | - tag: default 15 | rules: 16 | - exec: 17 | - plugin: 18 | tag: plugin 19 | args: 20 | mode: store # 缓存结果 21 | return: true # 缓存成功后,终止所有处理流程,并返回 22 | 23 | - exec: 24 | - plugin: 25 | tag: plugin 26 | args: 27 | mode: restore # 从缓存获取结果 28 | return: true # 获取缓存成功后,终止所有处理流程,并返回 29 | ``` 30 | 31 | ### API 32 | 33 | GET /dump 34 | 35 | 将缓存保持到本地文件 36 | 37 | 返回状态:204 38 | 39 | GET | DELETE /flush 40 | 41 | 删除所有内存中的缓存 42 | 43 | 返回状态:204 44 | -------------------------------------------------------------------------------- /docs/plugin/executor/rdns.md: -------------------------------------------------------------------------------- 1 | # rDNS IP 反查 2 | 3 | rDNS 用于查询 IP 的反向域名,通常用于局域网内 4 | 5 | ```yaml 6 | plugin-executors: 7 | - tag: plugin 8 | type: rdns 9 | 10 | workflows: 11 | - tag: default 12 | rules: 13 | - exec: 14 | - plugin: 15 | tag: plugin 16 | args: # 键值对:IP|CIDR: 上游服务器 Tag 17 | '*': upstream-A # '*' 用于匹配任意 IP 18 | '192.168.1.0/24': upstream-Local 19 | 'fd00::/8': upstream-Local 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/plugin/executor/rediscache.md: -------------------------------------------------------------------------------- 1 | # RedisCache 缓存 2 | 3 | RedisCache 缓存可以缓存返回的结果,提高性能,使用 Redis 作为存储机制 4 | 5 | ```yaml 6 | plugin-executors: 7 | - tag: plugin 8 | type: rediscache 9 | args: 10 | address: 127.0.0.1:6379 # Redis 地址,支持 Unix Socket 11 | password: '' # Redis 密码 12 | db: 0 # Redis DB 13 | 14 | workflows: 15 | - tag: default 16 | rules: 17 | - exec: 18 | - plugin: 19 | tag: plugin 20 | args: 21 | mode: store # 缓存结果 22 | return: true # 缓存成功后,终止所有处理流程,并返回 23 | 24 | - exec: 25 | - plugin: 26 | tag: plugin 27 | args: 28 | mode: restore # 从缓存获取结果 29 | return: true # 获取缓存成功后,终止所有处理流程,并返回 30 | ``` 31 | 32 | ### API 33 | 34 | GET | DELETE /flush 35 | 36 | 删除所有 Redis 中的缓存 37 | 38 | 返回状态:204 39 | -------------------------------------------------------------------------------- /docs/plugin/executor/script.md: -------------------------------------------------------------------------------- 1 | # Script 脚本 2 | 3 | Script 脚本可以触发脚本运行 4 | 5 | Script 会将当前请求各种信息存储环境变量中 6 | 7 | 支持的信息: 8 | 9 | - ```CDNS_ID``` ==> 请求的 ID 10 | - ```CDNS_INIT_TIME``` ==> 请求初始化的时间 11 | - ```CDNS_LISTENER``` ==> 请求来源的监听器标签 12 | - ```CDNS_CLIENT_IP``` ==> 请求来源的 ```IP``` 13 | - ```CDNS_REQ_QNAME``` ==> 请求的 ```QName``` 14 | - ```CDNS_REQ_QTYPE``` ==> 请求的 ```QType```,例如:AAAA 15 | - ```CDNS_REQ_QCLASS``` ==> 请求的 ```QClass```,例如:IN 16 | - ```CDNS_RESP_IP_LEN``` ==> 返回的 ```IP``` 个数,仅当请求类型为 A | AAAA 时有效 17 | - ```CDNS_RESP_IP_${i}``` ==> 返回的 ```IP```,从 1 开始 18 | - ```CDNS_RESP_UPSTREAM_TAG``` ==> 请求的上游服务器标签 19 | - ```CDNS_MARK``` ==> 请求上下文的 ```Mark``` 20 | - ```CDNS_METADATA_${KEY}``` ==> 请求上下文的 ```Metadata```,字符串全部大写 21 | 22 | --- 23 | 24 | - Script 还支持替换 ```args``` 中的字符串,只需要在 ```args``` 设置 {KEY} 即可 25 | 26 | ```yaml 27 | plugin-executors: 28 | - tag: plugin 29 | type: script 30 | args: 31 | command: bash 32 | # args: '-c' 33 | # args: 34 | # - '-c' 35 | # - '-b' 36 | 37 | workflows: 38 | - tag: default 39 | rules: 40 | - exec: 41 | - plugin: 42 | tag: plugin 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/plugin/matcher/domain.md: -------------------------------------------------------------------------------- 1 | # Domain 域名匹配 2 | 3 | 域名匹配器可以灵活匹配域名,提供比 ```qname``` 更灵活的规则机制 4 | 5 | ```yaml 6 | plugin-matchers: 7 | - tag: plugin 8 | type: domain 9 | args: 10 | rule: 'full:google.com' # 匹配规则 11 | # rule: 12 | # - 'github.com' # 等效于 suffix:github.com 13 | # - 'full:google.com' 14 | # - 'suffix:google.com' 15 | # - 'keyword:google' 16 | # - 'regex:google' 17 | file: /path/to/rule # 规则文件 18 | # file: 19 | # - /path/to/file1 20 | # - /path/to/file2 21 | 22 | workflows: 23 | - tag: default 24 | rules: 25 | - match-and: 26 | - plugin: 27 | tag: plugin 28 | exec: 29 | ... 30 | ``` 31 | 32 | ### API 33 | 34 | GET /reload 35 | 36 | 重新加载规则文件 37 | 38 | 返回状态:204 39 | -------------------------------------------------------------------------------- /docs/plugin/matcher/geosite.md: -------------------------------------------------------------------------------- 1 | # GeoSite 域名匹配 2 | 3 | GeoSite 匹配器可以灵活匹配域名,提供比 ```qname``` 更灵活的规则机制 4 | 5 | ```yaml 6 | plugin-matchers: 7 | - tag: plugin 8 | type: geosite 9 | args: 10 | path: /path/to/file # 规则文件 11 | type: sing # geosite 文件类型,必填,可选 sing | meta | v2ray 12 | code: cn # 载入的标签,为空载入所有标签,这会增加内存占用,只当 type: sing | v2ray 生效 13 | # code: # 载入多个标签 14 | # - cn 15 | # - google 16 | 17 | workflows: 18 | - tag: default 19 | rules: 20 | - match-and: 21 | - plugin: 22 | tag: plugin 23 | args: cn # 匹配的标签,只当 type: sing | v2ray 生效 24 | # args: cn,google # 多个匹配的标签 25 | # args: # 多个匹配的标签 26 | # - cn 27 | # - google,netflix 28 | # args: # 这样也可以 29 | # code: cn 30 | # args: # 这样也可以 31 | # code: 32 | # - cn 33 | # - google 34 | exec: 35 | ... 36 | ``` 37 | 38 | ### API 39 | 40 | GET /reload 41 | 42 | 重新加载规则文件 43 | 44 | 返回状态:204 45 | 46 | ### 注意 47 | 48 | Release 构建的二进制文件默认包含三种不同类型的 ```geosite```,文件体积可能很大,你可以使用 ```UPX``` 工具压缩。如果文件大小依然无法接受,可以自行编译去除不需要的 ```geosite``` 类型。 49 | 50 | 方法:在 ```plugin/matcher/geosite/geosite.go``` 中修改 ```import``` 即可 51 | 52 | ```go 53 | // 取消前面的注释即可 54 | import ( 55 | "github.com/rnetx/cdns/plugin/matcher/geosite/meta" // 添加 meta 格式支持 56 | // meta "github.com/rnetx/cdns/plugin/matcher/geosite/meta_stub" // 不添加 meta 格式支持 57 | 58 | "github.com/rnetx/cdns/plugin/matcher/geosite/sing" // 添加 sing 格式支持 59 | // sing "github.com/rnetx/cdns/plugin/matcher/geosite/sing_stub" // 不添加 sing 格式支持 60 | 61 | "github.com/rnetx/cdns/plugin/matcher/geosite/v2xray" // 添加 v2ray 格式支持 62 | // v2xray "github.com/rnetx/cdns/plugin/matcher/geosite/v2xray_stub" // 不添加 v2ray 格式支持 63 | ) 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/plugin/matcher/index.md: -------------------------------------------------------------------------------- 1 | # Plugin Matcher 匹配器插件 2 | 3 | ```yaml 4 | plugin-matchers: 5 | - tag: plugin # 插件标签 6 | type: ${type} # 插件类型 7 | args: ... # 插件参数 8 | 9 | workflows: 10 | - tag: default 11 | rules: 12 | - match-and: 13 | - plugin: 14 | tag: plugin # 匹配器插件标签 15 | args: ... # 运行时插件参数 16 | ``` 17 | 18 | 目前支持的匹配器插件列表: 19 | 20 | - [domain](domain) 21 | - [ip](ip) 22 | - [geosite](geosite) 23 | - [maxminddb](maxminddb) 24 | - [script](script) 25 | -------------------------------------------------------------------------------- /docs/plugin/matcher/ip.md: -------------------------------------------------------------------------------- 1 | # IP 匹配 2 | 3 | ```IP``` 匹配器可以灵活匹配返回的 ```IP```,提供比 ```resp-ip``` 更灵活的规则机制 4 | 5 | ```yaml 6 | plugin-matchers: 7 | - tag: plugin 8 | type: ip 9 | args: 10 | rule: '192.168.1.1' # 匹配规则 11 | # rule: 12 | # - '192.168.1.1' 13 | # - '192.168.1.0/24' 14 | file: /path/to/rule # 规则文件 15 | # file: 16 | # - /path/to/file1 17 | # - /path/to/file2 18 | 19 | workflows: 20 | - tag: default 21 | rules: 22 | - match-and: 23 | - plugin: 24 | tag: plugin 25 | exec: 26 | ... 27 | ``` 28 | 29 | ### API 30 | 31 | GET /reload 32 | 33 | 重新加载规则文件 34 | 35 | 返回状态:204 36 | -------------------------------------------------------------------------------- /docs/plugin/matcher/maxminddb.md: -------------------------------------------------------------------------------- 1 | # MaxmindDB IP 匹配 2 | 3 | MaxmindDB 匹配器可以灵活匹配返回的 ```IP```,提供比 ```resp-ip``` 更灵活的规则机制 4 | 5 | ```yaml 6 | plugin-matchers: 7 | - tag: plugin 8 | type: maxminddb 9 | args: 10 | path: /path/to/file # maxminddb 文件 11 | type: sing # MaxmindDB 文件类型,可选 sing | meta | geolite2-country 12 | 13 | workflows: 14 | - tag: default 15 | rules: 16 | - match-and: 17 | - plugin: 18 | tag: plugin 19 | args: cn # 匹配的标签 20 | # args: cn,google # 多个匹配的标签 21 | # args: # 多个匹配的标签 22 | # - cn 23 | # - google,netflix 24 | # args: # 这样也可以 25 | # code: cn 26 | # args: # 这样也可以 27 | # code: 28 | # - cn 29 | # - google 30 | exec: 31 | ... 32 | ``` 33 | 34 | ### API 35 | 36 | GET /reload 37 | 38 | 重新加载规则文件 39 | 40 | 返回状态:204 41 | -------------------------------------------------------------------------------- /docs/plugin/matcher/script.md: -------------------------------------------------------------------------------- 1 | # Script 脚本匹配 2 | 3 | 脚本匹配器可以根据脚本运行结果进行匹配 4 | 5 | 脚本需要在标准输出返回并且只能返回以下内容: 6 | ``` 7 | 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False 8 | ``` 9 | 10 | ```yaml 11 | plugin-matchers: 12 | - tag: plugin 13 | type: script 14 | args: 15 | command: bash 16 | # args: '-c' 17 | # args: 18 | # - '-b' 19 | # - '-b' 20 | interval: 300s # 脚本执行间隔 21 | 22 | workflows: 23 | - tag: default 24 | rules: 25 | - match-and: 26 | - plugin: 27 | tag: plugin 28 | exec: 29 | ... 30 | ``` 31 | 32 | ### API 33 | 34 | GET /run 35 | 36 | 手动执行一遍脚本 37 | 38 | 返回状态:204 39 | 40 | GET /result 41 | 42 | 获取当前状态 43 | 44 | 返回值: 45 | ```json5 46 | { 47 | "result": true 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/upstream/dhcp.md: -------------------------------------------------------------------------------- 1 | # DHCP 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: dhcp 7 | interface: eth0 # 绑定的网卡,留空自动选择,可能会失败 8 | # check-interval: 10m # 检查间隔,默认 10 分钟 9 | # 10 | # 以下配置是创建 UDP DNS 服务器时使用配置 11 | # 12 | # connect-timeout: 30s # 连接超时时间 13 | # idle-timeout: 60s # 连接空闲超时时间 14 | # edns0: false # 启用 EDNS0 支持,详情参考 https://github.com/IrineSistiana/udpme 15 | # enable-pipeline: false # 是否启用 Pipeline (TCP) 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/upstream/hosts.md: -------------------------------------------------------------------------------- 1 | # Hosts 2 | 3 | 根据规则返回指定 IPv4 / IPv6 地址,仅支持 (A | AAAA) 请求。没有规则匹配的请求或者非 (A | AAAA) 请求将发送到 fallback 上游服务器 4 | 5 | ```yaml 6 | upstreams: 7 | - tag: upstream 8 | type: hosts 9 | fallback: upstream-fallback # 没有规则匹配的请求或者非 (A | AAAA) 请求将发送到 fallback 上游服务器 10 | rule: # 规则,键值对(正则表达式字符串 => IP / CIDR) 11 | '^example.*': 192.168.1.1 12 | 'cloudflare': # 可以设置多个地址 13 | - 192.168.1.1 14 | - 192.168.1.0/24 # 支持 CIDR ,会随机从这个范围中选择一个 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/upstream/https.md: -------------------------------------------------------------------------------- 1 | # HTTPS(3) 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: https 7 | address: 223.5.5.5 # 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 bootstrap 或(和)socks5 8 | # connect-timeout: 30s # 连接超时时间 9 | # idle-timeout: 60s # 连接空闲超时时间 10 | # use-http3: false # 是否使用 HTTP/3 11 | # use-post: false # 是否使用 POST 方法发送请求 12 | # path: /dns-query # HTTP 路径,默认为 /dns-query 13 | # headers: # HTTP Header 14 | # User-Agent: cdns 15 | # servername: '' # TLS SNI,若为空,则设置为 address 16 | # insecure: false # 不验证服务器证书,不安全!强烈建议不设置! 17 | # server-ca-file: /path/to/ca.pem # 用于验证服务器证书的 CA 证书 18 | # server-ca-file: # 支持多文件 19 | # - /path/to/ca1.pem 20 | # - /path/to/ca2.pem 21 | # client-cert-file: /path/to/cert.pem # TLS 客户端证书文件,用于 mTLS 22 | # client-key-file: /path/to/key.pem # TLS 客户端证书文件,用于 mTLS 23 | # bootstrap: # 当 address 是域名时,使用 bootstrap 中的上游服务器解析域名 24 | # upstream: bootstrap-upstream # 上游服务器标签 25 | # strategy: '' # 解析策略,可选 prefer-ipv4 | prefer-ipv6 | only-ipv4 | only-ipv6 ,默认为 prefer-ipv4 26 | # bind-interface: eth0 # 绑定网卡 27 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 28 | # bind-ipv6: :: # 绑定本地 IPv6 地址 29 | # so-mark: 255 # 设置 SO_MARK (Linux) 30 | # socks5: # 使用 SOCKS5 代理 31 | # address: 127.0.0.1:1080 # SOCKS5 服务器地址,格式:IP:端口 32 | # username: '' # SOCKS5 用户名 33 | # password: '' # SOCKS5 密码 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/upstream/index.md: -------------------------------------------------------------------------------- 1 | # Upstream 上游服务器 2 | 3 | 上游服务器负责向上游服务器转发请求,将上游服务器的响应返回 4 | 5 | 目前支持以下上游服务器: 6 | 7 | - [UDP](udp) 8 | - [TCP](tcp) 9 | - [TLS (DoT | DNS Over TLS)](tls) 10 | - [HTTPS(3) (DoH | DoH3 | DNS Over HTTPS | DNS Over HTTP/3)](https) 11 | - [QUIC (DoQ | DNS Over QUIC)](quic) 12 | 13 | - [Parallel](parallel) 14 | - [Random](random) 15 | - [QueryTest](querytest) 16 | - [Hosts](hosts) 17 | - [DHCP](dhcp) 18 | -------------------------------------------------------------------------------- /docs/upstream/parallel.md: -------------------------------------------------------------------------------- 1 | # Parallel 2 | 3 | 请求并发发送到上游服务器,取最先返回的结果 4 | 5 | ```yaml 6 | upstreams: 7 | - tag: upstream 8 | type: parallel 9 | upstreams: 10 | - upstream-a 11 | - upstream-b 12 | - upstream-c 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/upstream/querytest.md: -------------------------------------------------------------------------------- 1 | # QueryTest 2 | 3 | 测试所有上游服务器,并选择延迟最低的上游服务器 4 | 5 | ```yaml 6 | upstreams: 7 | - tag: upstream 8 | type: querytest 9 | upstreams: 10 | - upstream-a 11 | - upstream-b 12 | - upstream-c 13 | test-domain: www.example.com # 测试域名 14 | test-interval: 600s # 测试间隔时间 15 | tolerance: 3ms # 比当前最佳上游服务器延迟低超过 tolerance 才会选择 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/upstream/quic.md: -------------------------------------------------------------------------------- 1 | # QUIC 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: quic 7 | address: 223.5.5.5 # 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 bootstrap 或(和)socks5 8 | # connect-timeout: 30s # 连接超时时间 9 | # idle-timeout: 60s # 连接空闲超时时间 10 | # servername: '' # TLS SNI,若为空,则设置为 address 11 | # insecure: false # 不验证服务器证书,不安全!强烈建议不设置! 12 | # server-ca-file: /path/to/ca.pem # 用于验证服务器证书的 CA 证书 13 | # server-ca-file: # 支持多文件 14 | # - /path/to/ca1.pem 15 | # - /path/to/ca2.pem 16 | # client-cert-file: /path/to/cert.pem # TLS 客户端证书文件,用于 mTLS 17 | # client-key-file: /path/to/key.pem # TLS 客户端证书文件,用于 mTLS 18 | # bootstrap: # 当 address 是域名时,使用 bootstrap 中的上游服务器解析域名 19 | # upstream: bootstrap-upstream # 上游服务器标签 20 | # strategy: '' # 解析策略,可选 prefer-ipv4 | prefer-ipv6 | only-ipv4 | only-ipv6 ,默认为 prefer-ipv4 21 | # bind-interface: eth0 # 绑定网卡 22 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 23 | # bind-ipv6: :: # 绑定本地 IPv6 地址 24 | # so-mark: 255 # 设置 SO_MARK (Linux) 25 | # socks5: # 使用 SOCKS5 代理 26 | # address: 127.0.0.1:1080 # SOCKS5 服务器地址,格式:IP:端口 27 | # username: '' # SOCKS5 用户名 28 | # password: '' # SOCKS5 密码 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/upstream/random.md: -------------------------------------------------------------------------------- 1 | # Random 2 | 3 | 随机将请求发送到上游服务器 4 | 5 | ```yaml 6 | upstreams: 7 | - tag: upstream 8 | type: random 9 | upstreams: 10 | - upstream-a 11 | - upstream-b 12 | - upstream-c 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/upstream/tcp.md: -------------------------------------------------------------------------------- 1 | # TCP 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: tcp 7 | address: 223.5.5.5 # 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 bootstrap 或(和)socks5 8 | # connect-timeout: 30s # 连接超时时间 9 | # idle-timeout: 60s # 连接空闲超时时间 10 | # enable-pipeline: false # 是否启用 Pipeline (TCP) 11 | # bootstrap: # 当 address 是域名时,使用 bootstrap 中的上游服务器解析域名 12 | # upstream: bootstrap-upstream # 上游服务器标签 13 | # strategy: '' # 解析策略,可选 prefer-ipv4 | prefer-ipv6 | only-ipv4 | only-ipv6 ,默认为 prefer-ipv4 14 | # bind-interface: eth0 # 绑定网卡 15 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 16 | # bind-ipv6: :: # 绑定本地 IPv6 地址 17 | # so-mark: 255 # 设置 SO_MARK (Linux) 18 | # socks5: # 使用 SOCKS5 代理 19 | # address: 127.0.0.1:1080 # SOCKS5 服务器地址,格式:IP:端口 20 | # username: '' # SOCKS5 用户名 21 | # password: '' # SOCKS5 密码 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/upstream/tls.md: -------------------------------------------------------------------------------- 1 | # TLS 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: tls 7 | address: 223.5.5.5 # 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 bootstrap 或(和)socks5 8 | # connect-timeout: 30s # 连接超时时间 9 | # idle-timeout: 60s # 连接空闲超时时间 10 | # enable-pipeline: false # 是否启用 Pipeline (TCP) 11 | # servername: '' # TLS SNI,若为空,则设置为 address 12 | # insecure: false # 不验证服务器证书,不安全!强烈建议不设置! 13 | # server-ca-file: /path/to/ca.pem # 用于验证服务器证书的 CA 证书 14 | # server-ca-file: # 支持多文件 15 | # - /path/to/ca1.pem 16 | # - /path/to/ca2.pem 17 | # client-cert-file: /path/to/cert.pem # TLS 客户端证书文件,用于 mTLS 18 | # client-key-file: /path/to/key.pem # TLS 客户端证书文件,用于 mTLS 19 | # bootstrap: # 当 address 是域名时,使用 bootstrap 中的上游服务器解析域名 20 | # upstream: bootstrap-upstream # 上游服务器标签 21 | # strategy: '' # 解析策略,可选 prefer-ipv4 | prefer-ipv6 | only-ipv4 | only-ipv6 ,默认为 prefer-ipv4 22 | # bind-interface: eth0 # 绑定网卡 23 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 24 | # bind-ipv6: :: # 绑定本地 IPv6 地址 25 | # so-mark: 255 # 设置 SO_MARK (Linux) 26 | # socks5: # 使用 SOCKS5 代理 27 | # address: 127.0.0.1:1080 # SOCKS5 服务器地址,格式:IP:端口 28 | # username: '' # SOCKS5 用户名 29 | # password: '' # SOCKS5 密码 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/upstream/udp.md: -------------------------------------------------------------------------------- 1 | # UDP 2 | 3 | ```yaml 4 | upstreams: 5 | - tag: upstream 6 | type: udp 7 | address: 223.5.5.5 # 服务器地址,支持域名|域名:端口|IP|IP:端口,若服务器地址是域名,必须设置 bootstrap 或(和)socks5 8 | # connect-timeout: 30s # 连接超时时间 9 | # idle-timeout: 60s # 连接空闲超时时间 10 | # edns0: false # 启用 EDNS0 支持,详情参考 https://github.com/IrineSistiana/udpme 11 | # enable-pipeline: false # 是否启用 Pipeline (TCP) 12 | # bootstrap: # 当 address 是域名时,使用 bootstrap 中的上游服务器解析域名 13 | # upstream: bootstrap-upstream # 上游服务器标签 14 | # strategy: '' # 解析策略,可选 prefer-ipv4 | prefer-ipv6 | only-ipv4 | only-ipv6 ,默认为 prefer-ipv4 15 | # bind-interface: eth0 # 绑定网卡 16 | # bind-ipv4: 0.0.0.0 # 绑定本地 IPv4 地址 17 | # bind-ipv6: :: # 绑定本地 IPv6 地址 18 | # so-mark: 255 # 设置 SO_MARK (Linux) 19 | # socks5: # 使用 SOCKS5 代理 20 | # address: 127.0.0.1:1080 # SOCKS5 服务器地址,格式:IP:端口 21 | # username: '' # SOCKS5 用户名 22 | # password: '' # SOCKS5 密码 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/workflow/index.md: -------------------------------------------------------------------------------- 1 | # Workflow 处理流程 2 | 3 | ```Workflow``` 是 ```cdns``` 的核心,用户可以自定义 ```Workflow``` 的处理流程,```cdns``` 会按照用户定义的流程处理请求 4 | 5 | 格式示例: 6 | ```yaml 7 | workflows: 8 | - tag: default 9 | rules: 10 | - match-and: 11 | ... 12 | exec: 13 | ... 14 | 15 | - match-or: 16 | ... 17 | exec: 18 | ... 19 | 20 | - exec: 21 | ... 22 | 23 | ``` 24 | 25 | ```Workflow``` 基于逻辑处理机制,类似于 ```if-else```,```Workflow``` 定义了三种逻辑处理机制: 26 | 27 | ``` 28 | match-and: match-or: exec: 29 | array[匹配器] array[匹配器] array[执行器] 30 | exec: exec: 31 | array[执行器] array[执行器] 32 | else-exec: else-exec: 33 | array[执行器] array[执行器] 34 | ``` 35 | 36 | - ```match-and```:当**所有** ```match-and``` 中的匹配器**都满足**时,执行 ```exec``` 中的执行器,否则执行 ```else-exec``` 中的执行器 37 | 38 | - ```match-or```:当**任意一个** ```match-or``` 中的匹配器**满足**时,执行 ```exec``` 中的执行器,否则执行 ```else-exec``` 中的执行器 39 | 40 | - ```exec```:执行 ```exec``` 中的执行器 41 | 42 | - ```match-and``` / ```match-or``` / ```exec``` / ```else-exec``` 所有匹配器或者执行器是数组类型,并且匹配/执行顺序是数组中的顺序 43 | -------------------------------------------------------------------------------- /github_action.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | NAME=cdns 3 | VERSION=$(git describe --tags --long) 4 | 5 | if [ -d build ] 6 | then 7 | rm -rf build 8 | fi 9 | mkdir build 10 | 11 | build() { 12 | filename=$1 13 | go build -o ./${filename} -v -trimpath -ldflags "-X 'github.com/rnetx/cdns/constant.Version=${VERSION}' -s -w -buildid=" . 14 | tar -czf ./build/${filename}.tar.gz ${filename} LICENSE README.md 15 | rm -rf ./${filename} 16 | sha256sum ./build/${filename}.tar.gz > ./build/${filename}.tar.gz.sha256 17 | echo "Build $filename OK!!" 18 | } 19 | 20 | build_windows() { 21 | filename=$1 22 | go build -o ./${filename}.exe -v -trimpath -ldflags "-X 'github.com/rnetx/cdns/constant.Version=${VERSION}' -s -w -buildid=" . 23 | zip ./build/${filename}.zip ${filename}.exe LICENSE README.md 24 | rm -rf ./${filename}.exe 25 | sha256sum ./build/${filename}.zip > ./build/${filename}.zip.sha256 26 | echo "Build $filename OK!!" 27 | } 28 | 29 | # build command 30 | # linux 31 | GOARCH=amd64 GOOS=linux build "${NAME}-linux-amd64" 32 | GOARCH=amd64 GOOS=linux GOAMD64=v3 build "${NAME}-linux-amd64-v3" 33 | GOARCH=arm64 GOOS=linux build "${NAME}-linux-arm64" 34 | GOARCH=arm GOOS=linux GOARM=7 build "${NAME}-linux-armv7" 35 | GOARCH=arm GOOS=linux GOARM=6 build "${NAME}-linux-armv6" 36 | GOARCH=arm GOOS=linux GOARM=5 build "${NAME}-linux-mips" 37 | GOARCH=mips GOMIPS=softfloat GOOS=linux build "${NAME}-linux-mips-softfloat" 38 | GOARCH=mips GOMIPS=hardfloat GOOS=linux build "${NAME}-linux-mips-hardfloat" 39 | GOARCH=mipsle GOMIPS=softfloat GOOS=linux build "${NAME}-linux-mipsle-softfloat" 40 | GOARCH=mipsle GOMIPS=hardfloat GOOS=linux build "${NAME}-linux-mipsle-hardfloat" 41 | GOARCH=mips64 GOOS=linux build "${NAME}-linux-mips64" 42 | GOARCH=mips64le GOOS=linux build "${NAME}-linux-mips64le" 43 | GOARCH=riscv64 GOOS=linux build "${NAME}-linux-riscv64" 44 | # windows 45 | GOARCH=386 GOOS=windows build_windows "${NAME}-windows-386" 46 | GOARCH=amd64 GOOS=windows build_windows "${NAME}-windows-amd64" 47 | GOARCH=amd64 GOOS=windows GOAMD64=v3 build_windows "${NAME}-windows-amd64-v3" 48 | GOARCH=arm64 GOOS=windows build_windows "${NAME}-windows-arm64" 49 | GOARCH=arm GOOS=windows GOARM=7 build_windows "${NAME}-windows-armv7" 50 | # darwin 51 | GOARCH=arm64 GOOS=darwin build "${NAME}-darwin-arm64" 52 | GOARCH=amd64 GOOS=darwin build "${NAME}-darwin-amd64" 53 | 54 | echo "Build ALL OK!!" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rnetx/cdns 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/logrusorgru/aurora/v4 v4.0.0 7 | github.com/miekg/dns v1.1.56 8 | github.com/quic-go/quic-go v0.39.3 9 | github.com/redis/go-redis/v9 v9.2.1 10 | github.com/spf13/cobra v1.7.0 11 | golang.org/x/sys v0.13.0 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | require ( 16 | github.com/adrg/xdg v0.4.0 // indirect 17 | github.com/beevik/ntp v1.3.0 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/gobwas/httphead v0.1.0 // indirect 21 | github.com/gobwas/pool v0.2.1 // indirect 22 | github.com/golang/protobuf v1.5.3 // indirect 23 | github.com/jackpal/gateway v1.0.10 // indirect 24 | github.com/josharian/native v1.1.0 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 26 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect 27 | github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect 28 | google.golang.org/protobuf v1.31.0 // indirect 29 | ) 30 | 31 | require ( 32 | github.com/dlclark/regexp2 v1.10.0 33 | github.com/go-chi/chi/v5 v5.0.10 34 | github.com/go-chi/cors v1.2.1 35 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 36 | github.com/gobwas/ws v1.3.1 37 | github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c 40 | github.com/kr/pretty v0.3.1 // indirect 41 | github.com/onsi/ginkgo/v2 v2.10.0 // indirect 42 | github.com/oschwald/maxminddb-golang v1.12.0 43 | github.com/quic-go/qpack v0.4.0 // indirect 44 | github.com/quic-go/qtls-go1-20 v0.3.4 // indirect 45 | github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | github.com/v2fly/v2ray-core/v5 v5.10.1 48 | go.uber.org/mock v0.3.0 // indirect 49 | golang.org/x/crypto v0.14.0 // indirect 50 | golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect 51 | golang.org/x/mod v0.13.0 // indirect 52 | golang.org/x/net v0.17.0 // indirect 53 | golang.org/x/text v0.13.0 // indirect 54 | golang.org/x/tools v0.14.0 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | ) 12 | 13 | type Options struct { 14 | Tag string 15 | Type string 16 | DealTimeout time.Duration 17 | Workflow string 18 | 19 | UDPOptions *UDPListenerOptions 20 | TCPOptions *TCPListenerOptions 21 | TLSOptions *TLSListenerOptions 22 | HTTPOptions *HTTPListenerOptions 23 | QUICOptions *QUICListenerOptions 24 | } 25 | 26 | type _Options struct { 27 | Tag string `yaml:"tag"` 28 | Type string `yaml:"type"` 29 | DealTimeout utils.Duration `yaml:"deal-timeout"` 30 | Workflow string `yaml:"workflow"` 31 | } 32 | 33 | func (o *Options) UnmarshalYAML(unmarshal func(interface{}) error) error { 34 | var _o _Options 35 | err := unmarshal(&_o) 36 | if err != nil { 37 | return err 38 | } 39 | var data any 40 | switch _o.Type { 41 | case UDPListenerType: 42 | o.UDPOptions = &UDPListenerOptions{} 43 | data = o.UDPOptions 44 | case TCPListenerType: 45 | o.TCPOptions = &TCPListenerOptions{} 46 | data = o.TCPOptions 47 | case TLSListenerType: 48 | o.TLSOptions = &TLSListenerOptions{} 49 | data = o.TLSOptions 50 | case HTTPListenerType: 51 | o.HTTPOptions = &HTTPListenerOptions{} 52 | data = o.HTTPOptions 53 | case QUICListenerType: 54 | o.QUICOptions = &QUICListenerOptions{} 55 | data = o.QUICOptions 56 | default: 57 | return fmt.Errorf("unknown listener type: %s", _o.Type) 58 | } 59 | err = unmarshal(data) 60 | if err != nil { 61 | return err 62 | } 63 | o.Type = _o.Type 64 | o.Tag = _o.Tag 65 | o.DealTimeout = time.Duration(_o.DealTimeout) 66 | o.Workflow = _o.Workflow 67 | return nil 68 | } 69 | 70 | func NewListener(ctx context.Context, core adapter.Core, logger log.Logger, tag string, options Options) (adapter.Listener, error) { 71 | var ( 72 | l adapter.Listener 73 | err error 74 | ) 75 | switch options.Type { 76 | case UDPListenerType: 77 | l, err = NewUDPListener(ctx, core, logger, tag, *options.UDPOptions, options.Workflow) 78 | case TCPListenerType: 79 | l, err = NewTCPListener(ctx, core, logger, tag, *options.TCPOptions, options.Workflow) 80 | case TLSListenerType: 81 | l, err = NewTLSListener(ctx, core, logger, tag, *options.TLSOptions, options.Workflow) 82 | case HTTPListenerType: 83 | l, err = NewHTTPListener(ctx, core, logger, tag, *options.HTTPOptions, options.Workflow) 84 | case QUICListenerType: 85 | l, err = NewQUICListener(ctx, core, logger, tag, *options.QUICOptions, options.Workflow) 86 | default: 87 | return nil, fmt.Errorf("unknown listener type: %s", options.Type) 88 | } 89 | if err != nil { 90 | return nil, err 91 | } 92 | dealTimeout := options.DealTimeout 93 | if dealTimeout <= 0 { 94 | dealTimeout = DefaultDealTimeout 95 | } 96 | l = &GenericListener{ 97 | dealTimeout: dealTimeout, 98 | Listener: l, 99 | } 100 | return l, nil 101 | } 102 | -------------------------------------------------------------------------------- /log/broadcast.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/utils" 10 | ) 11 | 12 | type BroadcastMessage struct { 13 | Time time.Time `json:"time"` 14 | Level Level `json:"level"` 15 | Message string `json:"message"` 16 | ContextID uint32 `json:"context_id,omitempty"` 17 | ContextDuration time.Duration `json:"context_duration,omitempty"` 18 | } 19 | 20 | type BroadcastLogger struct { 21 | logger basicLogger 22 | m sync.Map 23 | Logger 24 | } 25 | 26 | func NewBroadcastLogger(logger Logger) *BroadcastLogger { 27 | s := &BroadcastLogger{ 28 | logger: logger.basicLogger(), 29 | } 30 | s.Logger = newExportLogger(s) 31 | return s 32 | } 33 | 34 | func (s *BroadcastLogger) Close() { 35 | s.m.Range(func(key, value any) bool { 36 | ch := value.(*utils.SafeChan[BroadcastMessage]) 37 | ch.Close() 38 | return true 39 | }) 40 | } 41 | 42 | func (s *BroadcastLogger) level() Level { 43 | return s.logger.level() 44 | } 45 | 46 | func (s *BroadcastLogger) disableColor() bool { 47 | return s.logger.disableColor() 48 | } 49 | 50 | func (s *BroadcastLogger) print(level Level, msg string) { 51 | go s.sendToBroadcast(&BroadcastMessage{ 52 | Time: time.Now(), 53 | Level: level, 54 | Message: msg, 55 | }) 56 | s.logger.print(level, msg) 57 | } 58 | 59 | func (s *BroadcastLogger) printContext(ctx context.Context, level Level, msg string) { 60 | var ( 61 | contextID uint32 62 | contextDuration time.Duration 63 | ) 64 | logContext := adapter.LoadLogContext(ctx) 65 | if logContext != nil { 66 | contextID = logContext.ID() 67 | contextDuration = logContext.Duration() 68 | } 69 | go s.sendToBroadcast(&BroadcastMessage{ 70 | Time: time.Now(), 71 | Level: level, 72 | Message: msg, 73 | ContextID: contextID, 74 | ContextDuration: contextDuration, 75 | }) 76 | s.logger.printContext(ctx, level, msg) 77 | } 78 | 79 | func (s *BroadcastLogger) sendToBroadcast(msg *BroadcastMessage) { 80 | s.m.Range(func(key, value any) bool { 81 | ctx := key.(context.Context) 82 | ch := value.(*utils.SafeChan[BroadcastMessage]) 83 | select { 84 | case <-ctx.Done(): 85 | s.m.Delete(key) 86 | ch.Close() 87 | case ch.SendChan() <- *msg: 88 | default: 89 | if ch.Counter() == 1 { 90 | s.m.Delete(key) 91 | } 92 | } 93 | return true 94 | }) 95 | } 96 | 97 | func (s *BroadcastLogger) Register(ctx context.Context) *utils.SafeChan[BroadcastMessage] { 98 | ch := utils.NewSafeChan[BroadcastMessage](1) 99 | s.m.Store(ctx, ch) 100 | return ch 101 | } 102 | 103 | func (s *BroadcastLogger) Unregister(ctx context.Context) { 104 | v, ok := s.m.LoadAndDelete(ctx) 105 | if ok { 106 | ch := v.(*utils.SafeChan[BroadcastMessage]) 107 | ch.Close() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /log/level.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/logrusorgru/aurora/v4" 7 | ) 8 | 9 | type Level int 10 | 11 | func (l Level) MarshalText() ([]byte, error) { 12 | return []byte(l.LowString()), nil 13 | } 14 | 15 | const ( 16 | LevelDebug Level = iota 17 | LevelInfo 18 | LevelWarn 19 | LevelError 20 | LevelFatal 21 | ) 22 | 23 | func (l Level) LowString() string { 24 | switch l { 25 | case LevelDebug: 26 | return "debug" 27 | case LevelInfo: 28 | return "info" 29 | case LevelWarn: 30 | return "warn" 31 | case LevelError: 32 | return "error" 33 | case LevelFatal: 34 | return "fatal" 35 | default: 36 | return "unknown" 37 | } 38 | } 39 | 40 | func (l Level) String() string { 41 | switch l { 42 | case LevelDebug: 43 | return "Debug" 44 | case LevelInfo: 45 | return "Info" 46 | case LevelWarn: 47 | return "Warn" 48 | case LevelError: 49 | return "Error" 50 | case LevelFatal: 51 | return "Fatal" 52 | default: 53 | return "Unknown" 54 | } 55 | } 56 | 57 | func (l Level) ColorString() string { 58 | switch l { 59 | case LevelDebug: 60 | return aurora.Blue("Debug").String() 61 | case LevelInfo: 62 | return aurora.Green("Info").String() 63 | case LevelWarn: 64 | return aurora.Yellow("Warn").String() 65 | case LevelError: 66 | return aurora.Red("Error").String() 67 | case LevelFatal: 68 | return aurora.Magenta("Fatal").String() 69 | default: 70 | return "Unknown" 71 | } 72 | } 73 | 74 | func ParseLevelString(s string) (Level, error) { 75 | var level Level 76 | switch s { 77 | case "debug", "Debug": 78 | level = LevelDebug 79 | case "info", "Info": 80 | level = LevelInfo 81 | case "warn", "Warn", "warning", "Warning": 82 | level = LevelWarn 83 | case "error", "Error": 84 | level = LevelError 85 | case "fatal", "Fatal": 86 | level = LevelFatal 87 | default: 88 | return 0, fmt.Errorf("invalid level: %s", s) 89 | } 90 | return level, nil 91 | } 92 | -------------------------------------------------------------------------------- /log/nop.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type NopLogger struct { 9 | Logger 10 | } 11 | 12 | func NewNopLogger() Logger { 13 | n := &NopLogger{} 14 | n.Logger = newExportLogger(n) 15 | return n 16 | } 17 | 18 | func (l *NopLogger) level() Level { 19 | return LevelFatal 20 | } 21 | 22 | func (l *NopLogger) disableColor() bool { 23 | return true 24 | } 25 | 26 | func (l *NopLogger) print(_ Level, _ string) {} 27 | 28 | func (l *NopLogger) printContext(_ context.Context, _ Level, _ string) {} 29 | 30 | func (l *NopLogger) SetTimeFunc(_ func() time.Time) {} 31 | -------------------------------------------------------------------------------- /log/simple.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/rnetx/cdns/adapter" 11 | 12 | "github.com/logrusorgru/aurora/v4" 13 | ) 14 | 15 | var DefaultLogger Logger 16 | 17 | func init() { 18 | DefaultLogger = NewSimpleLogger(os.Stdout, LevelInfo, false, false) 19 | } 20 | 21 | var ( 22 | _ basicLogger = (*SimpleLogger)(nil) 23 | _ Logger = (*SimpleLogger)(nil) 24 | ) 25 | 26 | type SimpleLogger struct { 27 | writer io.Writer 28 | _level Level 29 | disableTimestamp bool 30 | _disableColor bool 31 | timeFunc func() time.Time 32 | Logger 33 | } 34 | 35 | func NewSimpleLogger(writer io.Writer, level Level, disableTimestamp bool, disableColor bool) Logger { 36 | s := &SimpleLogger{ 37 | writer: writer, 38 | _level: level, 39 | disableTimestamp: disableTimestamp, 40 | _disableColor: disableColor, 41 | } 42 | s.Logger = newExportLogger(s) 43 | return s 44 | } 45 | 46 | func (l *SimpleLogger) level() Level { 47 | return l._level 48 | } 49 | 50 | func (l *SimpleLogger) disableColor() bool { 51 | return l._disableColor 52 | } 53 | 54 | func (l *SimpleLogger) print(level Level, msg string) { 55 | if level < l._level { 56 | return 57 | } 58 | m := "" 59 | if !l.disableTimestamp { 60 | m += fmt.Sprintf("[%s] ", l.timeNow().Format(time.DateTime)) 61 | } 62 | if !l._disableColor { 63 | m += fmt.Sprintf("[%s] ", level.ColorString()) 64 | } else { 65 | m += fmt.Sprintf("[%s] ", level.String()) 66 | } 67 | m += msg 68 | fmt.Fprintln(l.writer, m) 69 | } 70 | 71 | func (l *SimpleLogger) printContext(ctx context.Context, level Level, msg string) { 72 | if level < l._level { 73 | return 74 | } 75 | m := "" 76 | if !l.disableTimestamp { 77 | m += fmt.Sprintf("[%s] ", l.timeNow().Format(time.DateTime)) 78 | } 79 | if !l._disableColor { 80 | m += fmt.Sprintf("[%s] ", level.ColorString()) 81 | } else { 82 | m += fmt.Sprintf("[%s] ", level.String()) 83 | } 84 | logContext := adapter.LoadLogContext(ctx) 85 | if logContext != nil { 86 | if !l._disableColor { 87 | m += fmt.Sprintf("[%s] ", aurora.Colorize(fmt.Sprintf("%d %dms", logContext.ID(), logContext.Duration().Milliseconds()), logContext.Color())) 88 | } else { 89 | m += fmt.Sprintf("[%d %dms] ", logContext.ID(), logContext.Duration().Milliseconds()) 90 | } 91 | } 92 | m += msg 93 | fmt.Fprintln(l.writer, m) 94 | } 95 | 96 | func (l *SimpleLogger) SetTimeFunc(f func() time.Time) { 97 | l.timeFunc = f 98 | } 99 | 100 | func (l *SimpleLogger) timeNow() time.Time { 101 | timeFunc := l.timeFunc 102 | if timeFunc == nil { 103 | timeFunc = time.Now 104 | } 105 | return timeFunc() 106 | } 107 | -------------------------------------------------------------------------------- /log/tag.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | 9 | "github.com/logrusorgru/aurora/v4" 10 | ) 11 | 12 | var ( 13 | _ basicLogger = (*TagLogger)(nil) 14 | _ Logger = (*TagLogger)(nil) 15 | ) 16 | 17 | type TagLogger struct { 18 | logger basicLogger 19 | tag string 20 | color aurora.Color 21 | Logger 22 | } 23 | 24 | func NewTagLogger(logger Logger, tag string, color aurora.Color) *TagLogger { 25 | t := &TagLogger{ 26 | logger: logger.basicLogger(), 27 | tag: tag, 28 | color: color, 29 | } 30 | t.Logger = newExportLogger(t) 31 | return t 32 | } 33 | 34 | func (t *TagLogger) level() Level { 35 | return t.logger.level() 36 | } 37 | 38 | func (t *TagLogger) disableColor() bool { 39 | return t.logger.disableColor() 40 | } 41 | 42 | func (t *TagLogger) print(level Level, msg string) { 43 | if level < t.logger.level() { 44 | return 45 | } 46 | m := "" 47 | if !t.logger.disableColor() && t.color != 0 { 48 | m += fmt.Sprintf("[%s] ", aurora.Colorize(t.tag, t.color)) 49 | } else { 50 | m += fmt.Sprintf("[%s] ", t.tag) 51 | } 52 | m += msg 53 | t.logger.print(level, m) 54 | } 55 | 56 | func (t *TagLogger) printContext(ctx context.Context, level Level, msg string) { 57 | if level < t.logger.level() { 58 | return 59 | } 60 | m := "" 61 | if !t.logger.disableColor() && t.color != 0 { 62 | m += fmt.Sprintf("[%s] ", aurora.Colorize(t.tag, t.color)) 63 | } else { 64 | m += fmt.Sprintf("[%s] ", t.tag) 65 | } 66 | logContext := adapter.LoadLogContext(ctx) 67 | if logContext != nil { 68 | if !t.logger.disableColor() { 69 | m += fmt.Sprintf("[%s] ", aurora.Colorize(fmt.Sprintf("%d %dms", logContext.ID(), logContext.Duration().Milliseconds()), logContext.Color())) 70 | } else { 71 | m += fmt.Sprintf("[%d %dms] ", logContext.ID(), logContext.Duration().Milliseconds()) 72 | } 73 | } 74 | m += msg 75 | t.logger.print(level, m) 76 | } 77 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/rnetx/cdns/cmd/cdns" 4 | 5 | func main() { 6 | cdns.MainCommand.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: cdns 2 | site_url: https://github.com/rnetx/cdns 3 | site_author: 0xffffhary 4 | site_description: cdns wiki 5 | repo_url: https://github.com/rnetx/cdns 6 | 7 | theme: 8 | name: material 9 | features: 10 | - navigation.sections 11 | - navigation.path 12 | - navigation.expand 13 | - navigation.indexes 14 | - navigation.tabs 15 | - toc.follow 16 | - toc.integrate 17 | - header.autohide 18 | palette: 19 | - media: "(prefers-color-scheme: light)" 20 | scheme: default 21 | primary: white 22 | accent: blue 23 | toggle: 24 | icon: material/brightness-7 25 | name: Switch to dark mode 26 | 27 | - media: "(prefers-color-scheme: dark)" 28 | scheme: slate 29 | primary: black 30 | accent: deep purple 31 | toggle: 32 | icon: material/brightness-4 33 | name: Switch to system preference 34 | 35 | markdown_extensions: 36 | - pymdownx.highlight: 37 | anchor_linenums: true 38 | - pymdownx.superfences 39 | - toc: 40 | permalink: true 41 | toc_depth: 3 42 | anchorlink: true 43 | - admonition 44 | - pymdownx.details 45 | - pymdownx.superfences 46 | - pymdownx.critic 47 | - pymdownx.caret 48 | - pymdownx.keys 49 | - pymdownx.mark 50 | - pymdownx.tilde 51 | - pymdownx.superfences: 52 | custom_fences: 53 | - name: mermaid 54 | class: mermaid 55 | format: !!python/name:pymdownx.superfences.fence_code_format 56 | - attr_list 57 | - md_in_html 58 | - footnotes 59 | - def_list 60 | 61 | nav: 62 | - '基本配置': 63 | - '简介': 64 | - index.md 65 | - '示例配置': example.md 66 | - '配置结构': global.md 67 | - '日志配置 (Log)': log/log.md 68 | - 'API 配置 (API)': api/api.md 69 | - 'NTP 服务器配置 (NTP)': ntp.md 70 | - '上游服务器 (Upstream)': 71 | - upstream/index.md 72 | - 'TCP': upstream/tcp.md 73 | - 'UDP': upstream/udp.md 74 | - 'TLS': upstream/tls.md 75 | - 'HTTPS': upstream/https.md 76 | - 'QUIC': upstream/quic.md 77 | - 'Parallel': upstream/parallel.md 78 | - 'Random': upstream/random.md 79 | - 'QueryTest': upstream/querytest.md 80 | - 'Hosts': upstream/hosts.md 81 | - 'DHCP': upstream/dhcp.md 82 | - '监听器 (Listener)': 83 | - listener/index.md 84 | - 'TCP': listener/tcp.md 85 | - 'UDP': listener/udp.md 86 | - 'TLS': listener/tls.md 87 | - 'HTTP': listener/http.md 88 | - 'QUIC': listener/quic.md 89 | - '工作流程 (Workflow)': 90 | - workflow/index.md 91 | - '匹配器': workflow/matcher.md 92 | - '执行器': workflow/executor.md 93 | - '匹配器插件': 94 | - plugin/matcher/index.md 95 | - 'geosite': plugin/matcher/geosite.md 96 | - 'maxminddb': plugin/matcher/maxminddb.md 97 | - 'domain': plugin/matcher/domain.md 98 | - 'ip': plugin/matcher/ip.md 99 | - 'script': plugin/matcher/script.md 100 | - '执行器插件': 101 | - plugin/executor/index.md 102 | - 'memcache': plugin/executor/memcache.md 103 | - 'rediscache': plugin/executor/rediscache.md 104 | - 'script': plugin/executor/script.md 105 | - 'ecs': plugin/executor/ecs.md 106 | - 'ipset': plugin/executor/ipset.md 107 | - 'rdns': plugin/executor/rdns.md -------------------------------------------------------------------------------- /ntp/time_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !(windows || linux || darwin) 2 | 3 | package ntp 4 | 5 | import "time" 6 | 7 | func SetSystemTime(nowTime time.Time) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /ntp/time_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package ntp 4 | 5 | import ( 6 | "time" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func SetSystemTime(nowTime time.Time) error { 12 | timeVal := unix.NsecToTimeval(nowTime.UnixNano()) 13 | return unix.Settimeofday(&timeVal) 14 | } 15 | -------------------------------------------------------------------------------- /ntp/time_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package ntp 4 | 5 | import ( 6 | "time" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | func SetSystemTime(nowTime time.Time) error { 13 | var systemTime windows.Systemtime 14 | systemTime.Year = uint16(nowTime.Year()) 15 | systemTime.Month = uint16(nowTime.Month()) 16 | systemTime.Day = uint16(nowTime.Day()) 17 | systemTime.Hour = uint16(nowTime.Hour()) 18 | systemTime.Minute = uint16(nowTime.Minute()) 19 | systemTime.Second = uint16(nowTime.Second()) 20 | systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000) 21 | 22 | dllKernel32 := windows.NewLazySystemDLL("kernel32.dll") 23 | proc := dllKernel32.NewProc("SetSystemTime") 24 | 25 | _, _, err := proc.Call( 26 | uintptr(unsafe.Pointer(&systemTime)), 27 | ) 28 | 29 | if err != nil && err.Error() != "The operation completed successfully." { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /plugin/executor.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | ) 11 | 12 | type PluginExecutorOptions struct { 13 | Tag string `yaml:"tag"` 14 | Type string `yaml:"type"` 15 | Args any `yaml:"args,omitempty"` 16 | } 17 | 18 | var executorMap sync.Map 19 | 20 | type PluginExecutorFactory func(ctx context.Context, core adapter.Core, logger log.Logger, tag string, args any) (adapter.PluginExecutor, error) 21 | 22 | func RegisterPluginExecutor(_type string, factory PluginExecutorFactory) { 23 | executorMap.Store(_type, factory) 24 | } 25 | 26 | func NewPluginExecutor(ctx context.Context, core adapter.Core, logger log.Logger, tag string, _type string, args any) (adapter.PluginExecutor, error) { 27 | v, ok := executorMap.Load(_type) 28 | if !ok { 29 | return nil, fmt.Errorf("unknown plugin executor type: %s", _type) 30 | } 31 | f := v.(PluginExecutorFactory) 32 | return f(ctx, core, logger, tag, args) 33 | } 34 | 35 | func PluginExecutorTypes() []string { 36 | var types []string 37 | executorMap.Range(func(key any, value any) bool { 38 | types = append(types, key.(string)) 39 | return true 40 | }) 41 | return types 42 | } 43 | -------------------------------------------------------------------------------- /plugin/executor/init.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | _ "github.com/rnetx/cdns/plugin/executor/ecs" 5 | _ "github.com/rnetx/cdns/plugin/executor/ipset" 6 | _ "github.com/rnetx/cdns/plugin/executor/memcache" 7 | _ "github.com/rnetx/cdns/plugin/executor/rdns" 8 | _ "github.com/rnetx/cdns/plugin/executor/rediscache" 9 | _ "github.com/rnetx/cdns/plugin/executor/script" 10 | ) 11 | 12 | func Do() {} 13 | -------------------------------------------------------------------------------- /plugin/executor/ipset/internal/ipset.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net/netip" 5 | "time" 6 | ) 7 | 8 | type IPSet interface { 9 | Create(name string, ttl time.Duration) error 10 | Close() error 11 | AddAddr(name string, addr netip.Addr, ttl time.Duration) error 12 | AddPrefix(name string, prefix netip.Prefix, ttl time.Duration) error 13 | DelAddr(name string, addr netip.Addr) error 14 | DelPrefix(name string, prefix netip.Prefix) error 15 | Flushall(name string) error 16 | Destroy(name string) error 17 | } 18 | -------------------------------------------------------------------------------- /plugin/executor/ipset/internal/ipset_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package internal 4 | 5 | import ( 6 | "net/netip" 7 | "time" 8 | 9 | "github.com/sagernet/netlink" 10 | ) 11 | 12 | var _ IPSet = (*IPSetLinux)(nil) 13 | 14 | type IPSetLinux struct { 15 | handler *netlink.Handle 16 | } 17 | 18 | func New() (IPSet, error) { 19 | handler, err := netlink.NewHandle() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &IPSetLinux{ 24 | handler: handler, 25 | }, nil 26 | } 27 | 28 | func (i *IPSetLinux) Close() error { 29 | i.handler.Close() 30 | return nil 31 | } 32 | 33 | func (i *IPSetLinux) Create(name string, ttl time.Duration) error { 34 | if ttl < 0 { 35 | ttl = 0 36 | } 37 | timeout := uint32(ttl.Seconds()) 38 | return i.handler.IpsetCreate(name, "hash:net", netlink.IpsetCreateOptions{ 39 | Replace: true, 40 | Skbinfo: true, 41 | Revision: 1, 42 | Timeout: &timeout, 43 | }) 44 | } 45 | 46 | func (i *IPSetLinux) AddAddr(name string, addr netip.Addr, ttl time.Duration) error { 47 | if ttl < 0 { 48 | ttl = 0 49 | } 50 | timeout := uint32(ttl.Seconds()) 51 | return i.handler.IpsetAdd(name, &netlink.IPSetEntry{ 52 | Replace: true, 53 | IP: addr.AsSlice(), 54 | Timeout: &timeout, 55 | }) 56 | } 57 | 58 | func (i *IPSetLinux) AddPrefix(name string, prefix netip.Prefix, ttl time.Duration) error { 59 | if ttl < 0 { 60 | ttl = 0 61 | } 62 | timeout := uint32(ttl.Seconds()) 63 | return i.handler.IpsetAdd(name, &netlink.IPSetEntry{ 64 | Replace: true, 65 | IP: prefix.Addr().AsSlice(), 66 | CIDR: uint8(prefix.Bits()), 67 | Timeout: &timeout, 68 | }) 69 | } 70 | 71 | func (i *IPSetLinux) DelAddr(name string, addr netip.Addr) error { 72 | return i.handler.IpsetDel(name, &netlink.IPSetEntry{ 73 | IP: addr.AsSlice(), 74 | }) 75 | } 76 | 77 | func (i *IPSetLinux) DelPrefix(name string, prefix netip.Prefix) error { 78 | return i.handler.IpsetDel(name, &netlink.IPSetEntry{ 79 | IP: prefix.Addr().AsSlice(), 80 | CIDR: uint8(prefix.Bits()), 81 | }) 82 | } 83 | 84 | func (i *IPSetLinux) Flushall(name string) error { 85 | return i.handler.IpsetFlush(name) 86 | } 87 | 88 | func (i *IPSetLinux) Destroy(name string) error { 89 | return i.handler.IpsetDestroy(name) 90 | } 91 | -------------------------------------------------------------------------------- /plugin/executor/ipset/internal/ipset_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | "net/netip" 8 | "time" 9 | ) 10 | 11 | var ErrOsUnsupported = errors.New("os unsupported") 12 | 13 | var _ IPSet = (*IPSetOther)(nil) 14 | 15 | type IPSetOther struct{} 16 | 17 | func New() (*IPSetOther, error) { 18 | return &IPSetOther{}, nil 19 | } 20 | 21 | func (i *IPSetOther) Close() error { 22 | return ErrOsUnsupported 23 | } 24 | 25 | func (i *IPSetOther) Create(_ string, _ time.Duration) error { 26 | return ErrOsUnsupported 27 | } 28 | 29 | func (i *IPSetOther) AddAddr(_ string, _ netip.Addr, _ time.Duration) error { 30 | return ErrOsUnsupported 31 | } 32 | 33 | func (i *IPSetOther) AddPrefix(_ string, _ netip.Prefix, _ time.Duration) error { 34 | return ErrOsUnsupported 35 | } 36 | 37 | func (i *IPSetOther) DelAddr(_ string, _ netip.Addr) error { 38 | return ErrOsUnsupported 39 | } 40 | 41 | func (i *IPSetOther) DelPrefix(_ string, _ netip.Prefix) error { 42 | return ErrOsUnsupported 43 | } 44 | 45 | func (i *IPSetOther) Flushall(_ string) error { 46 | return ErrOsUnsupported 47 | } 48 | 49 | func (i *IPSetOther) Destroy(_ string) error { 50 | return ErrOsUnsupported 51 | } 52 | -------------------------------------------------------------------------------- /plugin/executor/memcache/cachemap.go: -------------------------------------------------------------------------------- 1 | package memcache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "sync" 7 | "time" 8 | 9 | "github.com/rnetx/cdns/utils" 10 | ) 11 | 12 | type Item[T any] struct { 13 | Value T `json:"value"` 14 | TTL utils.Duration `json:"ttl"` 15 | Deadline time.Time `json:"-"` 16 | } 17 | 18 | type CacheMap[T any] struct { 19 | ctx context.Context 20 | cancel context.CancelFunc 21 | m map[string]*Item[T] 22 | lock sync.RWMutex 23 | closeDone chan struct{} 24 | } 25 | 26 | func NewCacheMap[T any](ctx context.Context) *CacheMap[T] { 27 | ctx, cancel := context.WithCancel(ctx) 28 | return &CacheMap[T]{ 29 | ctx: ctx, 30 | cancel: cancel, 31 | m: make(map[string]*Item[T]), 32 | closeDone: make(chan struct{}, 1), 33 | } 34 | } 35 | 36 | func (m *CacheMap[T]) Start() { 37 | go m.loopHandle() 38 | } 39 | 40 | func (m *CacheMap[T]) Close() { 41 | m.cancel() 42 | <-m.closeDone 43 | close(m.closeDone) 44 | } 45 | 46 | func (m *CacheMap[T]) loopHandle() { 47 | defer func() { 48 | select { 49 | case m.closeDone <- struct{}{}: 50 | default: 51 | } 52 | }() 53 | ticker := time.NewTicker(10 * time.Second) 54 | defer ticker.Stop() 55 | for { 56 | select { 57 | case <-m.ctx.Done(): 58 | return 59 | case <-ticker.C: 60 | m.lock.Lock() 61 | for k, v := range m.m { 62 | if time.Now().After(v.Deadline) { 63 | delete(m.m, k) 64 | } 65 | } 66 | m.lock.Unlock() 67 | } 68 | } 69 | } 70 | 71 | func (m *CacheMap[T]) Get(key string) (T, bool) { 72 | m.lock.RLock() 73 | defer m.lock.RUnlock() 74 | var v T 75 | item, ok := m.m[key] 76 | if ok { 77 | v = item.Value 78 | } 79 | return v, ok 80 | } 81 | 82 | func (m *CacheMap[T]) Set(key string, value T, ttl time.Duration) { 83 | m.lock.Lock() 84 | defer m.lock.Unlock() 85 | m.m[key] = &Item[T]{ 86 | Value: value, 87 | TTL: utils.Duration(ttl), 88 | Deadline: time.Now().Add(ttl), 89 | } 90 | } 91 | 92 | func (m *CacheMap[T]) Delete(key string) { 93 | m.lock.Lock() 94 | defer m.lock.Unlock() 95 | delete(m.m, key) 96 | } 97 | 98 | func (m *CacheMap[T]) FlushAll() { 99 | m.lock.Lock() 100 | defer m.lock.Unlock() 101 | m.m = make(map[string]*Item[T]) 102 | } 103 | 104 | func (m *CacheMap[T]) Encode() ([]byte, error) { 105 | m.lock.RLock() 106 | defer m.lock.RUnlock() 107 | return json.Marshal(m.m) 108 | } 109 | 110 | func Decode[T any](ctx context.Context, raw []byte) (*CacheMap[T], error) { 111 | var mm map[string]*Item[T] 112 | err := json.Unmarshal(raw, &mm) 113 | if err != nil { 114 | return nil, err 115 | } 116 | for _, item := range mm { 117 | item.Deadline = time.Now().Add(time.Duration(item.TTL)) 118 | } 119 | ctx, cancel := context.WithCancel(ctx) 120 | m := &CacheMap[T]{ 121 | ctx: ctx, 122 | cancel: cancel, 123 | m: mm, 124 | closeDone: make(chan struct{}, 1), 125 | } 126 | return m, nil 127 | } 128 | -------------------------------------------------------------------------------- /plugin/matcher.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | ) 11 | 12 | type PluginMatcherOptions struct { 13 | Tag string `yaml:"tag"` 14 | Type string `yaml:"type"` 15 | Args any `yaml:"args,omitempty"` 16 | } 17 | 18 | var matcherMap sync.Map 19 | 20 | type PluginMatcherFactory func(ctx context.Context, core adapter.Core, logger log.Logger, tag string, args any) (adapter.PluginMatcher, error) 21 | 22 | func RegisterPluginMatcher(_type string, factory PluginMatcherFactory) { 23 | matcherMap.Store(_type, factory) 24 | } 25 | 26 | func NewPluginMatcher(ctx context.Context, core adapter.Core, logger log.Logger, tag string, _type string, args any) (adapter.PluginMatcher, error) { 27 | v, ok := matcherMap.Load(_type) 28 | if !ok { 29 | return nil, fmt.Errorf("unknown plugin matcher type: %s", _type) 30 | } 31 | f := v.(PluginMatcherFactory) 32 | return f(ctx, core, logger, tag, args) 33 | } 34 | 35 | func PluginMatcherTypes() []string { 36 | var types []string 37 | matcherMap.Range(func(key any, value any) bool { 38 | types = append(types, key.(string)) 39 | return true 40 | }) 41 | return types 42 | } 43 | -------------------------------------------------------------------------------- /plugin/matcher/geosite/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rnetx/cdns/utils/domain" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type metaRule struct { 14 | Payload []string `yaml:"payload"` 15 | } 16 | 17 | func ReadFile(path string) (*domain.DomainSet, int, error) { 18 | f, err := os.OpenFile(path, os.O_RDONLY, 0) 19 | if err != nil { 20 | return nil, 0, err 21 | } 22 | defer f.Close() 23 | 24 | decoder := yaml.NewDecoder(f) 25 | var rules metaRule 26 | err = decoder.Decode(&rules) 27 | if err != nil { 28 | return nil, 0, err 29 | } 30 | if len(rules.Payload) == 0 { 31 | return nil, 0, fmt.Errorf("missing payload") 32 | } 33 | builder := domain.NewDomainSetBuilder() 34 | for _, rule := range rules.Payload { 35 | rr := strings.SplitN(rule, ",", 2) 36 | if len(rr) == 1 { 37 | switch { 38 | case strings.HasPrefix(rule, "+"): 39 | builder.AddSuffix(rule[1:]) 40 | case strings.HasPrefix(rule, "."): 41 | builder.AddSuffix(rule) 42 | default: 43 | builder.AddFull(rule) 44 | } 45 | } else { 46 | switch rr[0] { 47 | case "DOMAIN": 48 | builder.AddFull(rr[1]) 49 | case "DOMAIN-SUFFIX": 50 | switch { 51 | case strings.HasPrefix(rr[1], "+"): 52 | builder.AddSuffix(rr[1][1:]) 53 | case strings.HasPrefix(rr[1], "*"): 54 | builder.AddSuffix(rr[1][1:]) 55 | case strings.Contains(rr[1], "*"): 56 | return nil, 0, fmt.Errorf("invalid rule: %s", rule) 57 | default: 58 | builder.AddSuffix(rr[1]) 59 | } 60 | case "DOMAIN-KEYWORD": 61 | builder.AddKeyword(rr[1]) 62 | default: 63 | return nil, 0, fmt.Errorf("unknown rule type: %s", rr[0]) 64 | } 65 | } 66 | } 67 | ss, err := builder.Build() 68 | if err != nil { 69 | return nil, 0, err 70 | } 71 | return ss, builder.Len(), nil 72 | } 73 | -------------------------------------------------------------------------------- /plugin/matcher/geosite/meta_stub/stub.go: -------------------------------------------------------------------------------- 1 | package metastub 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rnetx/cdns/utils/domain" 7 | ) 8 | 9 | func ReadFile(path string) (*domain.DomainSet, int, error) { 10 | return nil, 0, fmt.Errorf("clash.meta geosite type is not supported") 11 | } 12 | -------------------------------------------------------------------------------- /plugin/matcher/geosite/sing/rule.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/rnetx/cdns/utils/domain" 7 | ) 8 | 9 | type itemType = uint8 10 | 11 | const ( 12 | RuleTypeDomain itemType = iota 13 | RuleTypeDomainSuffix 14 | RuleTypeDomainKeyword 15 | RuleTypeDomainRegex 16 | ) 17 | 18 | type item struct { 19 | Type itemType 20 | Value string 21 | } 22 | 23 | func compile(code []item) (*domain.DomainSet, error) { 24 | var domainLength int 25 | var domainSuffixLength int 26 | var domainKeywordLength int 27 | var domainRegexLength int 28 | for _, item := range code { 29 | switch item.Type { 30 | case RuleTypeDomain: 31 | domainLength++ 32 | case RuleTypeDomainSuffix: 33 | domainSuffixLength++ 34 | case RuleTypeDomainKeyword: 35 | domainKeywordLength++ 36 | case RuleTypeDomainRegex: 37 | domainRegexLength++ 38 | } 39 | } 40 | var ( 41 | full = 0 42 | suffix = 0 43 | keyword = 0 44 | regexp = 0 45 | ) 46 | if domainLength > 0 { 47 | full = domainLength 48 | } 49 | if domainSuffixLength > 0 { 50 | suffix = domainSuffixLength 51 | } 52 | if domainKeywordLength > 0 { 53 | keyword = domainKeywordLength 54 | } 55 | if domainRegexLength > 0 { 56 | regexp = domainRegexLength 57 | } 58 | builder := domain.NewDomainSetBuilderWithSize(full, suffix, keyword, regexp) 59 | for _, item := range code { 60 | switch item.Type { 61 | case RuleTypeDomain: 62 | builder.AddFull(item.Value) 63 | case RuleTypeDomainSuffix: 64 | value := item.Value 65 | if !strings.HasPrefix(value, ".") { 66 | value = "." + value 67 | } 68 | builder.AddSuffix(value) 69 | case RuleTypeDomainKeyword: 70 | builder.AddKeyword(item.Value) 71 | case RuleTypeDomainRegex: 72 | builder.AddRegexp(item.Value) 73 | } 74 | } 75 | return builder.Build() 76 | } 77 | -------------------------------------------------------------------------------- /plugin/matcher/geosite/sing_stub/stub.go: -------------------------------------------------------------------------------- 1 | package singstub 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rnetx/cdns/utils/domain" 7 | ) 8 | 9 | type Reader struct{} 10 | 11 | func (r *Reader) Close() error { 12 | return nil 13 | } 14 | 15 | func (r *Reader) Read(code string) (*domain.DomainSet, error) { 16 | return nil, fmt.Errorf("sing geosite type is not supported") 17 | } 18 | 19 | func OpenReader(path string) (*Reader, []string, error) { 20 | return nil, nil, fmt.Errorf("sing geosite type is not supported") 21 | } 22 | -------------------------------------------------------------------------------- /plugin/matcher/geosite/v2xray_stub/stub.go: -------------------------------------------------------------------------------- 1 | package v2xraystub 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rnetx/cdns/utils/domain" 7 | ) 8 | 9 | func ReadRule(path string, codes []string) (map[string]*domain.DomainSet, error) { 10 | return nil, fmt.Errorf("v2xray geosite type is not supported") 11 | } 12 | -------------------------------------------------------------------------------- /plugin/matcher/init.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | import ( 4 | _ "github.com/rnetx/cdns/plugin/matcher/domain" 5 | _ "github.com/rnetx/cdns/plugin/matcher/geosite" 6 | _ "github.com/rnetx/cdns/plugin/matcher/ip" 7 | _ "github.com/rnetx/cdns/plugin/matcher/maxminddb" 8 | _ "github.com/rnetx/cdns/plugin/matcher/script" 9 | ) 10 | 11 | func Do() {} 12 | -------------------------------------------------------------------------------- /test/server-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2DCCAsCgAwIBAgIQa5QC3v1oT9uGWNuSfo6oljANBgkqhkiG9w0BAQsFADBe 3 | MQswCQYDVQQGEwJDTjEOMAwGA1UEChMFTXlTU0wxKzApBgNVBAsTIk15U1NMIFRl 4 | c3QgUlNBIC0gRm9yIHRlc3QgdXNlIG9ubHkxEjAQBgNVBAMTCU15U1NMLmNvbTAe 5 | Fw0yMzEwMjQwNTI2MzdaFw0yODEwMjIwNTI2MzdaMCExCzAJBgNVBAYTAkNOMRIw 6 | EAYDVQQDEwkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 7 | AQDiHLkAkCJfGbxQUEtvcqe/Hdetl6msMd0OFq2suAuilfOr6GXOVw/CWPIXy4n4 8 | 2dbxqk58ynoXYDTH6FFSuT5boOHgudbdfH0l+rxP5FQbp2Fodbt1KOqUxuRaysMQ 9 | iBLxXJxJtY4EY7osEcer2VCptdqSnW2WHwYGVnHKxJqgmogG13xN9IGuYD3anus2 10 | 3b5z4+uLAPNFLsyhxSeDPEnOYNXvZ4at7BjDN5HmrOfL/RrYEC7jUe0NKxsvSi1u 11 | FBMJAT/eZ7pGJtdZM/38JgVEYRH9soUYmYOpAWLwNPHb1TB9mFaDGtf/shYcCBcH 12 | DDaLUCYjdvCl/mw3SGCX3uNdAgMBAAGjgc4wgcswDgYDVR0PAQH/BAQDAgWgMB0G 13 | A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAfBgNVHSMEGDAWgBQogSYF0TQa 14 | P8FzD7uTzxUcPwO/fzBjBggrBgEFBQcBAQRXMFUwIQYIKwYBBQUHMAGGFWh0dHA6 15 | Ly9vY3NwLm15c3NsLmNvbTAwBggrBgEFBQcwAoYkaHR0cDovL2NhLm15c3NsLmNv 16 | bS9teXNzbHRlc3Ryc2EuY3J0MBQGA1UdEQQNMAuCCTEyNy4wLjAuMTANBgkqhkiG 17 | 9w0BAQsFAAOCAQEAn7YZMgVYeT8DOvQ6CIQRA3jOuRluEm+mU44TKlJeLIlMU6GP 18 | J/fSH8XQynxIxPv5h2amzr6pR6TreHC6JSYAe2RBVWsFWl1bKLxRUBxus2eMuAq4 19 | YMcQz3IANS2e3D11U8iLq7PQnEA8eIvEkzBRoZ4TdoUXx0fk9M9Lit9jAhyZn+Lp 20 | lPUmEbrlY/F5m/eSMlsvJkbXpIo59QNqHORWm5Ewhw+zym0yS2PVeEF074e5zsOy 21 | KtNmueI9/FSylEP8IsStqtIbSHlMUk1zvLLYsZu7Biq1weCBvVwS2ihMAOZwy+1z 22 | JRfDpSOLX1yeWuc9hZ8AnAAw6AbwiNEesQeeVA== 23 | -----END CERTIFICATE----- 24 | -----BEGIN CERTIFICATE----- 25 | MIIDuzCCAqOgAwIBAgIQSEIWDPfWTDKZcWNyL2O+fjANBgkqhkiG9w0BAQsFADBf 26 | MQswCQYDVQQGEwJDTjEOMAwGA1UEChMFTXlTU0wxLDAqBgNVBAsTI015U1NMIFRl 27 | c3QgUm9vdCAtIEZvciB0ZXN0IHVzZSBvbmx5MRIwEAYDVQQDEwlNeVNTTC5jb20w 28 | HhcNMTcxMTE2MDUzNTM1WhcNMjcxMTE2MDUzNTM1WjBeMQswCQYDVQQGEwJDTjEO 29 | MAwGA1UEChMFTXlTU0wxKzApBgNVBAsTIk15U1NMIFRlc3QgUlNBIC0gRm9yIHRl 30 | c3QgdXNlIG9ubHkxEjAQBgNVBAMTCU15U1NMLmNvbTCCASIwDQYJKoZIhvcNAQEB 31 | BQADggEPADCCAQoCggEBAMBOtZk0uzdG4dcIIdcAdSSYDbua0Bdd6N6s4hZaCOup 32 | q7G7lwXkCyViTYAFa3wZ0BMQ4Bl9Q4j82R5IaoqG7WRIklwYnQh4gZ14uRde6Mr8 33 | yzvPRbAXKVoVh4NPqpE6jWMTP38mh94bKc+ITAE5QBRhCTQ0ah2Hq846ZiDAj6sY 34 | hMJuhUWegVGd0vh0rvtzvYNx7NGyxzoj6MxkDiYfFiuBhF2R9Tmq2UW9KCZkEBVL 35 | Q/YKQuvZZKFqR7WUU8GpCwzUm1FZbKtaCyRRvzLa5otghU2teKS5SKVI+Tpxvasp 36 | fu4eXBvveMgyWwDpKlzLCLgvoC9YNpbmdiVxNNkjwNsCAwEAAaN0MHIwDgYDVR0P 37 | AQH/BAQDAgGGMA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAfBgNV 38 | HSMEGDAWgBSa8Z+5JRISiexzGLmXvMX4oAp+UzAdBgNVHQ4EFgQUKIEmBdE0Gj/B 39 | cw+7k88VHD8Dv38wDQYJKoZIhvcNAQELBQADggEBAEl01ufit9rUeL5kZ31ox2vq 40 | 648azH/r/GR1S+mXci0Mg6RrDdLzUO7VSf0JULJf98oEPr9fpIZuRTyWcxiP4yh0 41 | wVd35OIQBTToLrMOWYWuApU4/YLKvg4A86h577kuYeSsWyf5kk0ngXsL1AFMqjOk 42 | Tc7p8PuW68S5/88Pe+Bq3sAaG3U5rousiTIpoN/osq+GyXisgv5jd2M4YBtl/NlD 43 | ppZs5LAOjct+Aaofhc5rNysonKjkd44K2cgBkbpOMj0dbVNKyL2/2I0zyY1FU2Mk 44 | URUHyMW5Qd5Q9g6Y4sDOIm6It9TF7EjpwMs42R30agcRYzuUsN72ZFBYFJwnBX8= 45 | -----END CERTIFICATE----- 46 | -------------------------------------------------------------------------------- /test/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA4hy5AJAiXxm8UFBLb3Knvx3XrZeprDHdDhatrLgLopXzq+hl 3 | zlcPwljyF8uJ+NnW8apOfMp6F2A0x+hRUrk+W6Dh4LnW3Xx9Jfq8T+RUG6dhaHW7 4 | dSjqlMbkWsrDEIgS8VycSbWOBGO6LBHHq9lQqbXakp1tlh8GBlZxysSaoJqIBtd8 5 | TfSBrmA92p7rNt2+c+PriwDzRS7MocUngzxJzmDV72eGrewYwzeR5qzny/0a2BAu 6 | 41HtDSsbL0otbhQTCQE/3me6RibXWTP9/CYFRGER/bKFGJmDqQFi8DTx29UwfZhW 7 | gxrX/7IWHAgXBww2i1AmI3bwpf5sN0hgl97jXQIDAQABAoIBAQCF6tsLYVJqHtTw 8 | gE3dSPve7m7R6nRcfv/cm0PreumxNrymATNivR+hTIq92xfxYhuy79oigM1E0P8R 9 | sx+PLhWnsSZ2sbp3XYbFmrYzXxkNc9n2Q1xuLHia+x0+RL65KM6HUwFhinz9To9y 10 | EGnA9ymWChXVJhZEhkVMNaCZpk2qdh5otAWt1dYyt6txCOdDEyotBrvB+T9q8ctD 11 | 2zlR9I8BWIfHDlIXPIT7f0onR0RZPWuY00IvjC+GVqcVxBHogmqEPcMDzY0kXcqR 12 | 0wTpvYUr5C0xcwAm3+dWeHVXjeqUw8PB4X+lp4LJh5y/XOSc821QuIRaLbcZt2Og 13 | RnGnuodFAoGBAOTwrcSNGpKNYatbpygmDSxHSD7DZ0VUvLiAO0W0mdW0Pal6Nb4T 14 | cPE/8+SUk887yVL/B7l+aB6IG1y99a/1pTBblMMzjJn7xWqOggBpTLNMAaV1D5TM 15 | fPgQVCw/0i1oldp08q1UFn1SuUGa/+nabMbO+0T71KRSxDCDNeJMEKEfAoGBAPzW 16 | eZye7zXp3UwmyQMMNVVsQRNTo+FDZqkfhharA4FCsgx6v0RUSfbpRgA78qx2JuoE 17 | UcyZhbORS3Zdk2mKvWPj08nyzSM+7nD+3hUZ2NiNkJJ+H0VdjlIpSdeXC2x7pcxv 18 | wEohQgtnylL9lOVU7v0AVnSQUnc5qsgAsm6VswADAoGAShF/9q48IZvyNHFjpD6j 19 | Vmb9fajUeX7Py9VY19V1S8mThYhPaN57VOH+8z4KkCpkmSB/jEjUQMSCLcAbg8Ey 20 | n3GRsJG19H+bQD043A81THDTu4ci8l4yNEN8KBDB3AURLmtVtDQXTpZ77zjJgQw0 21 | 0yFV89yR0FQiuxtITJ2VZ7sCgYAG69AUa31c2nQGW6FxBeqH1hjJ8KYxymiLBKPl 22 | BvVnmm3JTarisfP8YFJcO0ffVLSn/0pF8YXpbnbEXLdmUjfw/hGUG6Nl7ZkVWsS1 23 | iymWc/mbKPyuJ2t38E1nK2lSpOfa+swmu1ZVfZfaQbrLtFF+d33mXvUC8n8sTmiU 24 | AmEOBQKBgE/IzbpLeMBaRwSpG8R4dj+yq3NuwIlyeNKdpRS/TQrHbS0C/dBYcHVp 25 | a60ouclsyCv/mXmff1wSWjO/SuOD8pN5Xg2pZj/LE9euelcbIZfGWOy3eBpiTNtr 26 | ufFkruvVRfeY+zT2OadaVcQj5xwqaK/EtcSKXUzoW9WCtsnwY7dp 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /upstream/common.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | "github.com/rnetx/cdns/log" 12 | "github.com/rnetx/cdns/utils" 13 | 14 | "github.com/miekg/dns" 15 | ) 16 | 17 | const ( 18 | DefaultConnectTimeout = 30 * time.Second 19 | DefaultIdleTimeout = 60 * time.Second 20 | DefaultQueryTimeout = 15 * time.Second 21 | ) 22 | 23 | func reqMessageInfo(req *dns.Msg) string { 24 | return fmt.Sprintf("%s %s %s", dns.ClassToString[req.Question[0].Qclass], dns.TypeToString[req.Question[0].Qtype], req.Question[0].Name) 25 | } 26 | 27 | func Exchange(ctx context.Context, req *dns.Msg, logger log.Logger, exchangeFunc func(ctx context.Context, req *dns.Msg) (*dns.Msg, error)) (resp *dns.Msg, err error) { 28 | messageInfo := reqMessageInfo(req) 29 | logger.InfoContext(ctx, "exchange: ", messageInfo) 30 | defer func() { 31 | if err != nil { 32 | logger.ErrorfContext(ctx, "exchange failed: %s, error: %s", messageInfo, err) 33 | } else { 34 | logger.InfoContext(ctx, "exchange success: ", messageInfo) 35 | } 36 | }() 37 | resp, err = exchangeFunc(ctx, req) 38 | return 39 | } 40 | 41 | type TLSOptions struct { 42 | Servername string `yaml:"servername,omitempty"` 43 | Insecure bool `yaml:"insecure,omitempty"` 44 | ServerCAFile utils.Listable[string] `yaml:"server-ca-file,omitempty"` 45 | ClientCertFile string `yaml:"client-cert-file,omitempty"` 46 | ClientKeyFile string `yaml:"client-key-file,omitempty"` 47 | } 48 | 49 | func newTLSConfig(options TLSOptions) (*tls.Config, error) { 50 | tlsConfig := &tls.Config{ 51 | ServerName: options.Servername, 52 | InsecureSkipVerify: options.Insecure, 53 | } 54 | if len(options.ServerCAFile) > 0 { 55 | caPool := x509.NewCertPool() 56 | for _, caFile := range options.ServerCAFile { 57 | ca, err := os.ReadFile(caFile) 58 | if err != nil { 59 | return nil, fmt.Errorf("read server-ca-file failed: %s, error: %s", caFile, err) 60 | } 61 | if !caPool.AppendCertsFromPEM(ca) { 62 | return nil, fmt.Errorf("append server-ca-file failed: %s", caFile) 63 | } 64 | } 65 | tlsConfig.RootCAs = caPool 66 | } 67 | if (options.ClientCertFile == "" && options.ClientKeyFile != "") || (options.ClientCertFile != "" && options.ClientKeyFile == "") { 68 | return nil, fmt.Errorf("invalid client-cert-file or client-key-file") 69 | } 70 | if options.ClientCertFile != "" && options.ClientKeyFile != "" { 71 | certPair, err := tls.LoadX509KeyPair(options.ClientCertFile, options.ClientKeyFile) 72 | if err != nil { 73 | return nil, fmt.Errorf("load client-cert-file and client-key-file failed: %s", err) 74 | } 75 | tlsConfig.Certificates = []tls.Certificate{certPair} 76 | } 77 | return tlsConfig, nil 78 | } 79 | -------------------------------------------------------------------------------- /upstream/parallel.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync/atomic" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | type ParallelUpstreamOptions struct { 16 | Upstreams []string `yaml:"upstreams"` 17 | } 18 | 19 | const ParallelUpstreamType = "parallel" 20 | 21 | var ( 22 | _ adapter.Upstream = (*ParallelUpstream)(nil) 23 | _ adapter.Starter = (*ParallelUpstream)(nil) 24 | ) 25 | 26 | type ParallelUpstream struct { 27 | tag string 28 | core adapter.Core 29 | logger log.Logger 30 | 31 | upstreamTags []string 32 | upstreams []adapter.Upstream 33 | 34 | reqTotal atomic.Uint64 35 | reqSuccess atomic.Uint64 36 | } 37 | 38 | func NewParallelUpstream(_ context.Context, core adapter.Core, logger log.Logger, tag string, options ParallelUpstreamOptions) (adapter.Upstream, error) { 39 | u := &ParallelUpstream{ 40 | tag: tag, 41 | core: core, 42 | logger: logger, 43 | upstreamTags: options.Upstreams, 44 | } 45 | if len(u.upstreamTags) == 0 { 46 | return nil, fmt.Errorf("create parallel upstream failed: missing upstreams") 47 | } 48 | return u, nil 49 | } 50 | 51 | func (u *ParallelUpstream) Tag() string { 52 | return u.tag 53 | } 54 | 55 | func (u *ParallelUpstream) Type() string { 56 | return ParallelUpstreamType 57 | } 58 | 59 | func (u *ParallelUpstream) Dependencies() []string { 60 | return u.upstreamTags 61 | } 62 | 63 | func (u *ParallelUpstream) Start() error { 64 | u.upstreams = make([]adapter.Upstream, 0, len(u.upstreamTags)) 65 | for _, tag := range u.upstreamTags { 66 | uu := u.core.GetUpstream(tag) 67 | if uu == nil { 68 | return fmt.Errorf("upstream [%s] not found", tag) 69 | } 70 | u.upstreams = append(u.upstreams, uu) 71 | } 72 | return nil 73 | } 74 | 75 | func (u *ParallelUpstream) exchange(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { 76 | ctx, cancel := context.WithCancel(ctx) 77 | defer cancel() 78 | ch := utils.NewSafeChan[utils.Result[*dns.Msg]](len(u.upstreams)) 79 | defer ch.Close() 80 | for _, uu := range u.upstreams { 81 | go func(uu adapter.Upstream, ch *utils.SafeChan[utils.Result[*dns.Msg]]) { 82 | defer ch.Close() 83 | resp, err := uu.Exchange(ctx, req) 84 | if err != nil { 85 | select { 86 | case ch.SendChan() <- utils.Result[*dns.Msg]{Error: err}: 87 | case <-ctx.Done(): 88 | } 89 | } else { 90 | select { 91 | case ch.SendChan() <- utils.Result[*dns.Msg]{Value: resp}: 92 | case <-ctx.Done(): 93 | } 94 | } 95 | }(uu, ch.Clone()) 96 | } 97 | var lastErr error 98 | for i := 0; i < len(u.upstreams); i++ { 99 | select { 100 | case <-ctx.Done(): 101 | return nil, ctx.Err() 102 | case result := <-ch.ReceiveChan(): 103 | if result.Error != nil { 104 | lastErr = result.Error 105 | continue 106 | } 107 | return result.Value, nil 108 | } 109 | } 110 | return nil, lastErr 111 | } 112 | 113 | func (u *ParallelUpstream) Exchange(ctx context.Context, req *dns.Msg) (resp *dns.Msg, err error) { 114 | resp, err = u.exchange(ctx, req) 115 | u.reqTotal.Add(1) 116 | if err == nil { 117 | u.reqSuccess.Add(1) 118 | } 119 | return 120 | } 121 | 122 | func (u *ParallelUpstream) StatisticalData() map[string]any { 123 | total := u.reqTotal.Load() 124 | success := u.reqSuccess.Load() 125 | return map[string]any{ 126 | "total": total, 127 | "success": success, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /upstream/pipeline/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | type DNSPipelineConn struct { 16 | conn dns.Conn 17 | lastUse *atomic.Int64 18 | n *atomic.Int32 19 | chMap *sync.Map 20 | ctx context.Context 21 | cancel context.CancelFunc 22 | closeDone chan struct{} 23 | isClosed bool 24 | closeFunc func() 25 | } 26 | 27 | func NewDNSPipelineConn(ctx context.Context, udpSize uint16, conn net.Conn, closeFunc func()) *DNSPipelineConn { 28 | ctx, cancel := context.WithCancel(ctx) 29 | c := &DNSPipelineConn{ 30 | conn: dns.Conn{Conn: conn}, 31 | lastUse: &atomic.Int64{}, 32 | n: &atomic.Int32{}, 33 | chMap: &sync.Map{}, 34 | ctx: ctx, 35 | cancel: cancel, 36 | closeDone: make(chan struct{}, 1), 37 | closeFunc: closeFunc, 38 | } 39 | if udpSize > 0 { 40 | c.conn.UDPSize = udpSize 41 | } 42 | c.n.Add(1) 43 | c.flushLastUse() 44 | go c.loopReadHandle() 45 | return c 46 | } 47 | 48 | func (c *DNSPipelineConn) loopReadHandle() { 49 | defer func() { 50 | select { 51 | case c.closeDone <- struct{}{}: 52 | default: 53 | } 54 | }() 55 | defer c.cancel() 56 | for { 57 | msg, err := c.conn.ReadMsg() 58 | if err != nil { 59 | return 60 | } 61 | v, ok := c.chMap.LoadAndDelete(msg.Id) 62 | if ok { 63 | ch := v.(*utils.SafeChan[*dns.Msg]) 64 | select { 65 | case ch.SendChan() <- msg: 66 | ch.Close() 67 | case <-c.ctx.Done(): 68 | ch.Close() 69 | return 70 | } 71 | } 72 | } 73 | } 74 | 75 | func (c *DNSPipelineConn) LastUseUnix() int64 { 76 | return c.lastUse.Load() 77 | } 78 | 79 | func (c *DNSPipelineConn) flushLastUse() { 80 | c.lastUse.Store(time.Now().UnixNano()) 81 | } 82 | 83 | func (c *DNSPipelineConn) Clone() *DNSPipelineConn { 84 | c.n.Add(1) 85 | c.flushLastUse() 86 | return c 87 | } 88 | 89 | func (c *DNSPipelineConn) IsClosed() bool { 90 | return utils.IsContextCancelled(c.ctx) 91 | } 92 | 93 | func (c *DNSPipelineConn) close() { 94 | if c.isClosed { 95 | return 96 | } 97 | c.isClosed = true 98 | c.cancel() 99 | c.conn.Close() 100 | <-c.closeDone 101 | close(c.closeDone) 102 | if c.closeFunc != nil { 103 | c.closeFunc() 104 | } 105 | c.chMap.Range(func(key, value any) bool { 106 | ch := value.(*utils.SafeChan[*dns.Msg]) 107 | ch.Close() 108 | return true 109 | }) 110 | } 111 | 112 | func (c *DNSPipelineConn) Close() { 113 | if c.n.Add(-1) == 0 { 114 | c.close() 115 | } 116 | } 117 | 118 | func (c *DNSPipelineConn) Exchange(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { 119 | if c.isClosed { 120 | return nil, context.Canceled 121 | } 122 | ch := utils.NewSafeChan[*dns.Msg](1) 123 | defer ch.Close() 124 | c.chMap.Store(req.Id, ch.Clone()) 125 | defer c.chMap.Delete(req.Id) 126 | err := c.conn.WriteMsg(req) 127 | if err != nil { 128 | return nil, err 129 | } 130 | select { 131 | case <-ctx.Done(): 132 | return nil, ctx.Err() 133 | case <-c.ctx.Done(): 134 | return nil, context.Canceled 135 | case resp := <-ch.ReceiveChan(): 136 | return resp, nil 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /upstream/random.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/rnetx/cdns/adapter" 11 | "github.com/rnetx/cdns/log" 12 | 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | type RandomUpstreamOptions struct { 17 | Upstreams []string `yaml:"upstreams"` 18 | } 19 | 20 | const RandomUpstreamType = "random" 21 | 22 | var ( 23 | _ adapter.Upstream = (*RandomUpstream)(nil) 24 | _ adapter.Starter = (*RandomUpstream)(nil) 25 | ) 26 | 27 | type RandomUpstream struct { 28 | tag string 29 | core adapter.Core 30 | logger log.Logger 31 | 32 | upstreamTags []string 33 | upstreams []adapter.Upstream 34 | 35 | reqTotal atomic.Uint64 36 | reqSuccess atomic.Uint64 37 | } 38 | 39 | func NewRandomUpstream(_ context.Context, core adapter.Core, logger log.Logger, tag string, options RandomUpstreamOptions) (adapter.Upstream, error) { 40 | u := &RandomUpstream{ 41 | tag: tag, 42 | core: core, 43 | logger: logger, 44 | upstreamTags: options.Upstreams, 45 | } 46 | if len(u.upstreamTags) == 0 { 47 | return nil, fmt.Errorf("create random upstream failed: missing upstreams") 48 | } 49 | return u, nil 50 | } 51 | 52 | func (u *RandomUpstream) Tag() string { 53 | return u.tag 54 | } 55 | 56 | func (u *RandomUpstream) Type() string { 57 | return RandomUpstreamType 58 | } 59 | 60 | func (u *RandomUpstream) Dependencies() []string { 61 | return u.upstreamTags 62 | } 63 | 64 | func (u *RandomUpstream) Start() error { 65 | u.upstreams = make([]adapter.Upstream, 0, len(u.upstreamTags)) 66 | for _, tag := range u.upstreamTags { 67 | uu := u.core.GetUpstream(tag) 68 | if uu == nil { 69 | return fmt.Errorf("upstream [%s] not found", tag) 70 | } 71 | u.upstreams = append(u.upstreams, uu) 72 | } 73 | return nil 74 | } 75 | 76 | func (u *RandomUpstream) exchange(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { 77 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 78 | index := r.Intn(len(u.upstreams)) 79 | uu := u.upstreams[index] 80 | u.logger.DebugfContext(ctx, "random upstream [%s] selected", uu.Tag()) 81 | return uu.Exchange(ctx, req) 82 | } 83 | 84 | func (u *RandomUpstream) Exchange(ctx context.Context, req *dns.Msg) (resp *dns.Msg, err error) { 85 | resp, err = u.exchange(ctx, req) 86 | u.reqTotal.Add(1) 87 | if err == nil { 88 | u.reqSuccess.Add(1) 89 | } 90 | return 91 | } 92 | 93 | func (u *RandomUpstream) StatisticalData() map[string]any { 94 | total := u.reqTotal.Load() 95 | success := u.reqSuccess.Load() 96 | return map[string]any{ 97 | "total": total, 98 | "success": success, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /utils/chan.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sync/atomic" 4 | 5 | type SafeChan[T any] struct { 6 | ch chan T 7 | n *atomic.Int32 8 | } 9 | 10 | func NewSafeChan[T any](size int) *SafeChan[T] { 11 | c := &SafeChan[T]{ 12 | n: &atomic.Int32{}, 13 | } 14 | c.n.Add(1) 15 | if size == 0 { 16 | c.ch = make(chan T) 17 | } else { 18 | c.ch = make(chan T, size) 19 | } 20 | return c 21 | } 22 | 23 | func (c *SafeChan[T]) Counter() int { 24 | return int(c.n.Load()) 25 | } 26 | 27 | func (c *SafeChan[T]) Clone() *SafeChan[T] { 28 | c.n.Add(1) 29 | return c 30 | } 31 | 32 | func (c *SafeChan[T]) ReceiveChan() <-chan T { 33 | return c.ch 34 | } 35 | 36 | func (c *SafeChan[T]) SendChan() chan<- T { 37 | return c.ch 38 | } 39 | 40 | func (c *SafeChan[T]) Close() { 41 | if c.n.Add(-1) == 0 { 42 | close(c.ch) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /utils/chi.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type ChiRouterBuilderItem struct { 11 | Path string `json:"-"` 12 | Methods []string `json:"method"` 13 | Description any `json:"description"` 14 | Handler http.Handler `json:"-"` 15 | } 16 | 17 | type ChiRouterBuilder struct { 18 | m map[string]*ChiRouterBuilderItem 19 | } 20 | 21 | func NewChiRouterBuilder() *ChiRouterBuilder { 22 | return &ChiRouterBuilder{ 23 | m: make(map[string]*ChiRouterBuilderItem), 24 | } 25 | } 26 | 27 | func (c *ChiRouterBuilder) Add(item *ChiRouterBuilderItem) { 28 | c.m[item.Path] = item 29 | } 30 | 31 | func (c *ChiRouterBuilder) helpHTTPHandler() http.HandlerFunc { 32 | return func(w http.ResponseWriter, r *http.Request) { 33 | raw, err := json.Marshal(c.m) 34 | if err != nil { 35 | w.WriteHeader(http.StatusInternalServerError) 36 | } else { 37 | w.WriteHeader(http.StatusOK) 38 | w.Header().Set("Content-Type", "application/json") 39 | w.Write(raw) 40 | } 41 | } 42 | } 43 | 44 | func (c *ChiRouterBuilder) Build() chi.Router { 45 | router := chi.NewRouter() 46 | for path, item := range c.m { 47 | for _, method := range item.Methods { 48 | router.Method(method, path, item.Handler) 49 | } 50 | } 51 | router.Get("/help", c.helpHTTPHandler()) 52 | return router 53 | } 54 | -------------------------------------------------------------------------------- /utils/compare.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type compareItem struct { 4 | old bool 5 | new bool 6 | } 7 | 8 | func Compare[T comparable](old, new []T) ([]T, []T) { 9 | m := make(map[T]compareItem) 10 | for _, v := range old { 11 | m[v] = compareItem{old: true} 12 | } 13 | for _, v := range new { 14 | item, ok := m[v] 15 | if ok { 16 | item.new = true 17 | m[v] = item 18 | } else { 19 | m[v] = compareItem{new: true} 20 | } 21 | } 22 | added := make([]T, 0, len(new)) 23 | removed := make([]T, 0, len(old)) 24 | for k, v := range m { 25 | if v.old && !v.new { 26 | removed = append(removed, k) 27 | } else if !v.old && v.new { 28 | added = append(added, k) 29 | } 30 | } 31 | return added, removed 32 | } 33 | -------------------------------------------------------------------------------- /utils/context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "context" 4 | 5 | func IsContextCancelled(ctx context.Context) bool { 6 | select { 7 | case <-ctx.Done(): 8 | return true 9 | default: 10 | return false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /utils/decode.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/json" 4 | 5 | func JsonDecode(in any, out any) error { 6 | raw, err := json.Marshal(in) 7 | if err != nil { 8 | return err 9 | } 10 | return json.Unmarshal(raw, out) 11 | } 12 | -------------------------------------------------------------------------------- /utils/dns.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/miekg/dns" 4 | 5 | // from mosdns(https://github.com/IrineSistiana/mosdns), thank for @IrineSistiana 6 | func FakeSOA(name string) *dns.SOA { 7 | if name == "" { 8 | name = "." 9 | } 10 | return &dns.SOA{ 11 | Hdr: dns.RR_Header{ 12 | Name: name, 13 | Rrtype: dns.TypeSOA, 14 | Class: dns.ClassINET, 15 | Ttl: 10, 16 | }, 17 | Ns: "fake-ns.cdns.", 18 | Mbox: "fake-mbox.cdns.", 19 | Serial: 2023060700, 20 | Refresh: 1800, 21 | Retry: 900, 22 | Expire: 604800, 23 | Minttl: 86400, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/domain/domain.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | type DomainSetBuilder struct { 12 | fulls []string // full 13 | suffixs []string // suffix 14 | keywords []string // keyword 15 | regexps []string // regexp 16 | } 17 | 18 | func NewDomainSetBuilder() *DomainSetBuilder { 19 | return &DomainSetBuilder{} 20 | } 21 | 22 | func NewDomainSetBuilderWithSize(full, suffix, keyword, regexp int) *DomainSetBuilder { 23 | return &DomainSetBuilder{ 24 | fulls: make([]string, 0, full), 25 | suffixs: make([]string, 0, suffix), 26 | keywords: make([]string, 0, keyword), 27 | regexps: make([]string, 0, regexp), 28 | } 29 | } 30 | 31 | func (b *DomainSetBuilder) Len() int { 32 | return len(b.fulls) + len(b.suffixs) + len(b.keywords) + len(b.regexps) 33 | } 34 | 35 | func (b *DomainSetBuilder) AddFull(full string) { 36 | b.fulls = append(b.fulls, full) 37 | } 38 | 39 | func (b *DomainSetBuilder) AddSuffix(suffix string) { 40 | b.suffixs = append(b.suffixs, suffix) 41 | } 42 | 43 | func (b *DomainSetBuilder) AddKeyword(keyword string) { 44 | b.keywords = append(b.keywords, keyword) 45 | } 46 | 47 | func (b *DomainSetBuilder) AddRegexp(regexp string) { 48 | b.regexps = append(b.regexps, regexp) 49 | } 50 | 51 | type DomainSet struct { 52 | trie *succinctSet // full && suffix 53 | keywords []string // keyword 54 | regexps []*regexp.Regexp // regex 55 | } 56 | 57 | func (b *DomainSetBuilder) Build() (*DomainSet, error) { 58 | s := &DomainSet{} 59 | if len(b.keywords) > 0 { 60 | s.keywords = b.keywords 61 | } 62 | if len(b.regexps) > 0 { 63 | s.regexps = make([]*regexp.Regexp, len(b.regexps)) 64 | for i, r := range b.regexps { 65 | regex, err := regexp.Compile(r) 66 | if err != nil { 67 | return nil, fmt.Errorf("compile regexp %s failed: %v", r, err) 68 | } 69 | s.regexps[i] = regex 70 | } 71 | } 72 | if len(b.fulls) > 0 || len(b.suffixs) > 0 { 73 | domainList := make([]string, 0, len(b.fulls)+len(b.suffixs)) 74 | seen := make(map[string]bool, len(domainList)) 75 | for _, domain := range b.suffixs { 76 | if seen[domain] { 77 | continue 78 | } 79 | seen[domain] = true 80 | domainList = append(domainList, reverseDomainSuffix(domain)) 81 | } 82 | for _, domain := range b.fulls { 83 | if seen[domain] { 84 | continue 85 | } 86 | seen[domain] = true 87 | domainList = append(domainList, reverseDomain(domain)) 88 | } 89 | sort.Strings(domainList) 90 | s.trie = newSuccinctSet(domainList) 91 | } 92 | return s, nil 93 | } 94 | 95 | func reverseDomain(domain string) string { 96 | l := len(domain) 97 | b := make([]byte, l) 98 | for i := 0; i < l; { 99 | r, n := utf8.DecodeRuneInString(domain[i:]) 100 | i += n 101 | utf8.EncodeRune(b[l-i:], r) 102 | } 103 | return string(b) 104 | } 105 | 106 | func reverseDomainSuffix(domain string) string { 107 | l := len(domain) 108 | b := make([]byte, l+1) 109 | for i := 0; i < l; { 110 | r, n := utf8.DecodeRuneInString(domain[i:]) 111 | i += n 112 | utf8.EncodeRune(b[l-i:], r) 113 | } 114 | b[l] = prefixLabel 115 | return string(b) 116 | } 117 | 118 | func (d *DomainSet) Match(domain string) bool { 119 | domain = strings.TrimSuffix(domain, ".") 120 | if d.trie != nil { 121 | if d.trie.Has(reverseDomain(domain)) { 122 | return true 123 | } 124 | } 125 | if d.keywords != nil { 126 | for _, keyword := range d.keywords { 127 | if strings.Contains(domain, keyword) { 128 | return true 129 | } 130 | } 131 | } 132 | if d.regexps != nil { 133 | for _, regex := range d.regexps { 134 | if regex.MatchString(domain) { 135 | return true 136 | } 137 | } 138 | } 139 | return false 140 | } 141 | -------------------------------------------------------------------------------- /utils/duration.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type Duration time.Duration 9 | 10 | func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { 11 | var s string 12 | err := unmarshal(&s) 13 | if err != nil { 14 | return err 15 | } 16 | duration, err := time.ParseDuration(s) 17 | if err != nil { 18 | return err 19 | } 20 | *d = Duration(duration) 21 | return nil 22 | } 23 | 24 | func (d Duration) MarshalYAML() (interface{}, error) { 25 | return time.Duration(d).String(), nil 26 | } 27 | 28 | func (d *Duration) UnmarshalJSON(data []byte) error { 29 | var s string 30 | err := json.Unmarshal(data, &s) 31 | if err != nil { 32 | return err 33 | } 34 | duration, err := time.ParseDuration(s) 35 | if err != nil { 36 | return err 37 | } 38 | *d = Duration(duration) 39 | return nil 40 | } 41 | 42 | func (d Duration) MarshalJSON() ([]byte, error) { 43 | return json.Marshal(time.Duration(d).String()) 44 | } 45 | -------------------------------------------------------------------------------- /utils/graph.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type GraphNode[T comparable] struct { 4 | data T 5 | prev map[T]*GraphNode[T] 6 | next map[T]*GraphNode[T] 7 | } 8 | 9 | func NewGraphNode[T comparable](data T) *GraphNode[T] { 10 | return &GraphNode[T]{ 11 | data: data, 12 | prev: make(map[T]*GraphNode[T]), 13 | next: make(map[T]*GraphNode[T]), 14 | } 15 | } 16 | 17 | func (n *GraphNode[T]) Data() T { 18 | return n.data 19 | } 20 | 21 | func (n *GraphNode[T]) Prev() []*GraphNode[T] { 22 | nodes := make([]*GraphNode[T], 0, len(n.prev)) 23 | for _, node := range n.prev { 24 | nodes = append(nodes, node) 25 | } 26 | return nodes 27 | } 28 | 29 | func (n *GraphNode[T]) Next() []*GraphNode[T] { 30 | nodes := make([]*GraphNode[T], 0, len(n.next)) 31 | for _, node := range n.next { 32 | nodes = append(nodes, node) 33 | } 34 | return nodes 35 | } 36 | 37 | func (n *GraphNode[T]) AddPrev(node *GraphNode[T]) { 38 | n.prev[node.data] = node 39 | } 40 | 41 | func (n *GraphNode[T]) AddNext(node *GraphNode[T]) { 42 | n.next[node.data] = node 43 | } 44 | 45 | func (n *GraphNode[T]) RemovePrev(node *GraphNode[T]) { 46 | delete(n.prev, node.data) 47 | } 48 | 49 | func (n *GraphNode[T]) RemoveNext(node *GraphNode[T]) { 50 | delete(n.next, node.data) 51 | } 52 | 53 | func (n *GraphNode[T]) PrevMap() map[T]*GraphNode[T] { 54 | return n.prev 55 | } 56 | 57 | func (n *GraphNode[T]) NextMap() map[T]*GraphNode[T] { 58 | return n.next 59 | } 60 | 61 | func (n *GraphNode[T]) HasPrev() bool { 62 | return len(n.prev) > 0 63 | } 64 | 65 | func (n *GraphNode[T]) HasNext() bool { 66 | return len(n.next) > 0 67 | } 68 | -------------------------------------------------------------------------------- /utils/limit.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "context" 4 | 5 | type Limiter struct { 6 | ch chan *struct{} 7 | } 8 | 9 | func NewLimiter(n int) *Limiter { 10 | l := &Limiter{ 11 | ch: make(chan *struct{}, n), 12 | } 13 | for i := 0; i < n; i++ { 14 | l.ch <- (*struct{})(nil) 15 | } 16 | return l 17 | } 18 | 19 | func (l *Limiter) Get(ctx context.Context) bool { 20 | select { 21 | case <-ctx.Done(): 22 | return false 23 | case <-l.ch: 24 | return true 25 | } 26 | } 27 | 28 | func (l *Limiter) PutBack() { 29 | select { 30 | case l.ch <- (*struct{})(nil): 31 | default: 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utils/listable.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/json" 4 | 5 | type Listable[T any] []T 6 | 7 | func (l *Listable[T]) UnmarshalYAML(unmarshal func(interface{}) error) error { 8 | var list []T 9 | err := unmarshal(&list) 10 | if err == nil { 11 | *l = list 12 | return nil 13 | } 14 | var single T 15 | err2 := unmarshal(&single) 16 | if err2 == nil { 17 | *l = []T{single} 18 | return nil 19 | } 20 | return err 21 | } 22 | 23 | func (l *Listable[T]) MarshalYAML() (interface{}, error) { 24 | if len(*l) == 1 { 25 | return (*l)[0], nil 26 | } else { 27 | return ([]T)(*l), nil 28 | } 29 | } 30 | 31 | func (l *Listable[T]) UnmarshalJSON(data []byte) error { 32 | var list []T 33 | err := json.Unmarshal(data, &list) 34 | if err == nil { 35 | *l = list 36 | return nil 37 | } 38 | var single T 39 | err2 := json.Unmarshal(data, &single) 40 | if err2 == nil { 41 | *l = []T{single} 42 | return nil 43 | } 44 | return err 45 | } 46 | 47 | func (l *Listable[T]) MarshalJSON() ([]byte, error) { 48 | if len(*l) == 1 { 49 | return json.Marshal((*l)[0]) 50 | } else { 51 | return json.Marshal([]T(*l)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /utils/netip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/big" 5 | "math/rand" 6 | "net/netip" 7 | "time" 8 | ) 9 | 10 | func RandomAddrFromPrefix(prefix netip.Prefix) netip.Addr { 11 | ip := prefix.Addr() 12 | startN := big.NewInt(0).SetBytes(ip.AsSlice()) 13 | var bits int 14 | if ip.Is4() { 15 | bits = 5 16 | } else { 17 | bits = 7 18 | } 19 | bt := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(1< 0 { 21 | return errors.New(strings.Join(errs, ", and ")) 22 | } 23 | } 24 | return nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utils/network/basic/control/mark_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package control 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | func SetMark(mark uint32) func(network string, address string, c syscall.RawConn) error { 11 | return func(network string, address string, c syscall.RawConn) error { 12 | var inErr error 13 | err := c.Control(func(fd uintptr) { 14 | inErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)) 15 | }) 16 | if inErr != nil { 17 | if err == nil { 18 | err = inErr 19 | } 20 | return fmt.Errorf("errors: %s, and %s", err, inErr) 21 | } 22 | return err 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/network/basic/control/mark_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package control 4 | 5 | import "syscall" 6 | 7 | func SetMark(mark uint32) func(network string, address string, c syscall.RawConn) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /utils/network/basic/control/reuse_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows 2 | 3 | package control 4 | 5 | import "syscall" 6 | 7 | func ReuseAddr() func(network, address string, conn syscall.RawConn) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /utils/network/basic/control/reuse_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | 3 | package control 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func ReuseAddr() func(network, address string, conn syscall.RawConn) error { 13 | return func(network, address string, conn syscall.RawConn) error { 14 | var innerErr error 15 | err := conn.Control(func(fd uintptr) { 16 | innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) 17 | if innerErr != nil { 18 | return 19 | } 20 | innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) 21 | }) 22 | if innerErr != nil { 23 | if err != nil { 24 | return fmt.Errorf("%w | %w", err, innerErr) 25 | } 26 | return innerErr 27 | } 28 | return err 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/network/basic/control/reuse_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package control 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | func ReuseAddr() func(network, address string, conn syscall.RawConn) error { 11 | return func(network, address string, conn syscall.RawConn) error { 12 | var innerErr error 13 | err := conn.Control(func(fd uintptr) { 14 | innerErr = syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) 15 | }) 16 | if innerErr != nil { 17 | if err != nil { 18 | return fmt.Errorf("%w | %w", err, innerErr) 19 | } 20 | return innerErr 21 | } 22 | return err 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/network/common/dialer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type Dialer interface { 9 | DialContext(ctx context.Context, network string, address SocksAddr) (net.Conn, error) 10 | ListenPacket(ctx context.Context, address SocksAddr) (net.PacketConn, error) 11 | } 12 | -------------------------------------------------------------------------------- /utils/network/common/socksaddr.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/netip" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type SocksAddr struct { 12 | domain string 13 | ip netip.Addr 14 | port uint16 15 | } 16 | 17 | func NewSocksIPPort(ip netip.Addr, port uint16) *SocksAddr { 18 | return &SocksAddr{ 19 | ip: ip, 20 | port: port, 21 | } 22 | } 23 | 24 | func NewSocksDomainPort(domain string, port uint16) *SocksAddr { 25 | return &SocksAddr{ 26 | domain: domain, 27 | port: port, 28 | } 29 | } 30 | 31 | func NewSocksAddrFromAddrPort(addr netip.AddrPort) *SocksAddr { 32 | return &SocksAddr{ 33 | ip: addr.Addr(), 34 | port: addr.Port(), 35 | } 36 | } 37 | 38 | func NewSocksAddrFromString(address string) (*SocksAddr, error) { 39 | addr, err := netip.ParseAddrPort(address) 40 | if err == nil { 41 | return NewSocksAddrFromAddrPort(addr), nil 42 | } 43 | host, port, err := net.SplitHostPort(address) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if host == "" { 48 | return nil, errors.New("invalid address") 49 | } 50 | portUint16, err := strconv.ParseUint(port, 10, 16) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return &SocksAddr{ 55 | domain: host, 56 | port: uint16(portUint16), 57 | }, nil 58 | } 59 | 60 | func NewSocksAddrFromStringWithDefaultPort(address string, defaultPort uint16) (*SocksAddr, error) { 61 | addr, err := netip.ParseAddrPort(address) 62 | if err == nil { 63 | return NewSocksAddrFromAddrPort(addr), nil 64 | } 65 | ip, err := netip.ParseAddr(address) 66 | if err == nil { 67 | return NewSocksAddrFromAddrPort(netip.AddrPortFrom(ip, defaultPort)), nil 68 | } 69 | host, port, err := net.SplitHostPort(address) 70 | if err != nil { 71 | if strings.Contains(err.Error(), "missing port in address") { 72 | address = strings.Trim(address, "[]") 73 | ip, err := netip.ParseAddr(address) 74 | if err == nil { 75 | return &SocksAddr{ 76 | ip: ip, 77 | port: defaultPort, 78 | }, nil 79 | } 80 | return &SocksAddr{ 81 | domain: address, 82 | port: defaultPort, 83 | }, nil 84 | } 85 | return nil, err 86 | } 87 | if host == "" { 88 | return nil, errors.New("invalid address") 89 | } 90 | portUint16, err := strconv.ParseUint(port, 10, 16) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return &SocksAddr{ 95 | domain: host, 96 | port: uint16(portUint16), 97 | }, nil 98 | } 99 | 100 | func (s *SocksAddr) String() string { 101 | if s.domain != "" { 102 | return net.JoinHostPort(s.domain, strconv.Itoa(int(s.port))) 103 | } 104 | return netip.AddrPortFrom(s.ip, s.port).String() 105 | } 106 | 107 | func (s *SocksAddr) Domain() string { 108 | return s.domain 109 | } 110 | 111 | func (s *SocksAddr) IP() netip.Addr { 112 | return s.ip 113 | } 114 | 115 | func (s *SocksAddr) Port() uint16 { 116 | return s.port 117 | } 118 | 119 | func (s *SocksAddr) IsDomain() bool { 120 | return s.domain != "" 121 | } 122 | 123 | func (s *SocksAddr) IsIP() bool { 124 | return s.ip.IsValid() 125 | } 126 | 127 | func (s *SocksAddr) IsIPv4() bool { 128 | return s.IsIP() && s.ip.Is4() 129 | } 130 | 131 | func (s *SocksAddr) IsIPv6() bool { 132 | return s.IsIP() && s.ip.Is6() 133 | } 134 | 135 | func (s *SocksAddr) UDPAddr() net.Addr { 136 | if s.IsIP() { 137 | return &net.UDPAddr{ 138 | IP: s.ip.AsSlice(), 139 | Port: int(s.port), 140 | Zone: s.ip.Zone(), 141 | } 142 | } else { 143 | return &DomainUDPAddr{ 144 | domain: s.domain, 145 | port: s.port, 146 | } 147 | } 148 | } 149 | 150 | type DomainUDPAddr struct { 151 | domain string 152 | port uint16 153 | } 154 | 155 | func (d *DomainUDPAddr) Network() string { 156 | return "udp" 157 | } 158 | 159 | func (d *DomainUDPAddr) String() string { 160 | return net.JoinHostPort(d.domain, strconv.Itoa(int(d.port))) 161 | } 162 | -------------------------------------------------------------------------------- /utils/network/netinterface/interface.go: -------------------------------------------------------------------------------- 1 | package netinterface 2 | 3 | import "errors" 4 | 5 | // netinterface is from https://github.com/SagerNet, thanks for nekohasekai's work. 6 | 7 | var ErrNoRoute = errors.New("no route found") 8 | -------------------------------------------------------------------------------- /utils/network/netinterface/interface_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package netinterface 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "net/netip" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/net/route" 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | func GetDefaultInterfaceName() (*net.Interface, error) { 17 | ribMessage, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0) 18 | if err != nil { 19 | return nil, err 20 | } 21 | routeMessages, err := route.ParseRIB(route.RIBTypeRoute, ribMessage) 22 | if err != nil { 23 | return nil, err 24 | } 25 | var defaultInterface *net.Interface 26 | for _, rawRouteMessage := range routeMessages { 27 | routeMessage := rawRouteMessage.(*route.RouteMessage) 28 | if len(routeMessage.Addrs) <= unix.RTAX_NETMASK { 29 | continue 30 | } 31 | destination, isIPv4Destination := routeMessage.Addrs[unix.RTAX_DST].(*route.Inet4Addr) 32 | if !isIPv4Destination { 33 | continue 34 | } 35 | if destination.IP != netip.IPv4Unspecified().As4() { 36 | continue 37 | } 38 | mask, isIPv4Mask := routeMessage.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr) 39 | if !isIPv4Mask { 40 | continue 41 | } 42 | ones, _ := net.IPMask(mask.IP[:]).Size() 43 | if ones != 0 { 44 | continue 45 | } 46 | routeInterface, err := net.InterfaceByIndex(routeMessage.Index) 47 | if err != nil { 48 | return nil, err 49 | } 50 | if routeMessage.Flags&unix.RTF_UP == 0 { 51 | continue 52 | } 53 | if routeMessage.Flags&unix.RTF_GATEWAY == 0 { 54 | continue 55 | } 56 | // if routeMessage.Flags&unix.RTF_IFSCOPE != 0 { 57 | // continue 58 | // } 59 | defaultInterface = routeInterface 60 | break 61 | } 62 | if defaultInterface == nil { 63 | defaultInterface, err = getDefaultInterfaceBySocket() 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | if defaultInterface == nil { 69 | return nil, ErrNoRoute 70 | } 71 | return defaultInterface, nil 72 | } 73 | 74 | func getDefaultInterfaceBySocket() (*net.Interface, error) { 75 | socketFd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0) 76 | if err != nil { 77 | return nil, fmt.Errorf("create file descriptor: %w", err) 78 | } 79 | defer unix.Close(socketFd) 80 | go unix.Connect(socketFd, &unix.SockaddrInet4{ 81 | Addr: [4]byte{10, 255, 255, 255}, 82 | Port: 80, 83 | }) 84 | result := make(chan netip.Addr, 1) 85 | go func() { 86 | for { 87 | sockname, sockErr := unix.Getsockname(socketFd) 88 | if sockErr != nil { 89 | break 90 | } 91 | sockaddr, isInet4Sockaddr := sockname.(*unix.SockaddrInet4) 92 | if !isInet4Sockaddr { 93 | break 94 | } 95 | addr := netip.AddrFrom4(sockaddr.Addr) 96 | if addr.IsUnspecified() { 97 | time.Sleep(time.Millisecond) 98 | continue 99 | } 100 | result <- addr 101 | break 102 | } 103 | }() 104 | var selectedAddr netip.Addr 105 | select { 106 | case selectedAddr = <-result: 107 | case <-time.After(time.Second): 108 | return nil, os.ErrDeadlineExceeded 109 | } 110 | interfaces, err := net.Interfaces() 111 | if err != nil { 112 | return nil, err 113 | } 114 | for _, netInterface := range interfaces { 115 | interfaceAddrs, err := netInterface.Addrs() 116 | if err != nil { 117 | return nil, err 118 | } 119 | for _, interfaceAddr := range interfaceAddrs { 120 | ipNet, isIPNet := interfaceAddr.(*net.IPNet) 121 | if !isIPNet { 122 | continue 123 | } 124 | if ipNet.Contains(selectedAddr.AsSlice()) { 125 | return &netInterface, nil 126 | } 127 | } 128 | } 129 | return nil, fmt.Errorf("no interface found for address %s", selectedAddr) 130 | } 131 | -------------------------------------------------------------------------------- /utils/network/netinterface/interface_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !android 2 | 3 | package netinterface 4 | 5 | import ( 6 | "net" 7 | 8 | "github.com/sagernet/netlink" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func GetDefaultInterfaceName() (*net.Interface, error) { 13 | routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: unix.RT_TABLE_MAIN}, netlink.RT_FILTER_TABLE) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | for _, route := range routes { 19 | if route.Dst != nil { 20 | continue 21 | } 22 | 23 | var link netlink.Link 24 | link, err = netlink.LinkByIndex(route.LinkIndex) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return net.InterfaceByName(link.Attrs().Name) 30 | } 31 | 32 | return nil, ErrNoRoute 33 | } 34 | -------------------------------------------------------------------------------- /utils/network/netinterface/interface_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !windows && (!linux || android) 2 | 3 | package netinterface 4 | 5 | import ( 6 | "net" 7 | "os" 8 | ) 9 | 10 | func GetDefaultInterfaceName() (*net.Interface, error) { 11 | return nil, os.ErrInvalid 12 | } 13 | -------------------------------------------------------------------------------- /utils/network/netinterface/interface_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package netinterface 4 | 5 | import ( 6 | "net" 7 | 8 | "github.com/rnetx/cdns/utils/network/netinterface/internal/winipcfg" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | func GetDefaultInterfaceName() (*net.Interface, error) { 14 | rows, err := winipcfg.GetIPForwardTable2(windows.AF_INET) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | lowestMetric := ^uint32(0) 20 | alias := "" 21 | 22 | for _, row := range rows { 23 | if row.DestinationPrefix.PrefixLength != 0 { 24 | continue 25 | } 26 | 27 | ifrow, err := row.InterfaceLUID.Interface() 28 | if err != nil || ifrow.OperStatus != winipcfg.IfOperStatusUp { 29 | continue 30 | } 31 | 32 | iface, err := row.InterfaceLUID.IPInterface(windows.AF_INET) 33 | if err != nil { 34 | continue 35 | } 36 | 37 | if ifrow.Type == winipcfg.IfTypePropVirtual || ifrow.Type == winipcfg.IfTypeSoftwareLoopback { 38 | continue 39 | } 40 | 41 | metric := row.Metric + iface.Metric 42 | if metric < lowestMetric { 43 | lowestMetric = metric 44 | alias = ifrow.Alias() 45 | } 46 | } 47 | 48 | if alias == "" { 49 | return nil, ErrNoRoute 50 | } 51 | 52 | return net.InterfaceByName(alias) 53 | } 54 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/interface_change_handler.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package winipcfg 7 | 8 | import ( 9 | "sync" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | // InterfaceChangeCallback structure allows interface change callback handling. 15 | type InterfaceChangeCallback struct { 16 | cb func(notificationType MibNotificationType, iface *MibIPInterfaceRow) 17 | wait sync.WaitGroup 18 | } 19 | 20 | var ( 21 | interfaceChangeAddRemoveMutex = sync.Mutex{} 22 | interfaceChangeMutex = sync.Mutex{} 23 | interfaceChangeCallbacks = make(map[*InterfaceChangeCallback]bool) 24 | interfaceChangeHandle = windows.Handle(0) 25 | ) 26 | 27 | // RegisterInterfaceChangeCallback registers a new InterfaceChangeCallback. If this particular callback is already 28 | // registered, the function will silently return. Returned InterfaceChangeCallback.Unregister method should be used 29 | // to unregister. 30 | func RegisterInterfaceChangeCallback(callback func(notificationType MibNotificationType, iface *MibIPInterfaceRow)) (*InterfaceChangeCallback, error) { 31 | s := &InterfaceChangeCallback{cb: callback} 32 | 33 | interfaceChangeAddRemoveMutex.Lock() 34 | defer interfaceChangeAddRemoveMutex.Unlock() 35 | 36 | interfaceChangeMutex.Lock() 37 | defer interfaceChangeMutex.Unlock() 38 | 39 | interfaceChangeCallbacks[s] = true 40 | 41 | if interfaceChangeHandle == 0 { 42 | err := notifyIPInterfaceChange(windows.AF_UNSPEC, windows.NewCallback(interfaceChanged), 0, false, &interfaceChangeHandle) 43 | if err != nil { 44 | delete(interfaceChangeCallbacks, s) 45 | interfaceChangeHandle = 0 46 | return nil, err 47 | } 48 | } 49 | 50 | return s, nil 51 | } 52 | 53 | // Unregister unregisters the callback. 54 | func (callback *InterfaceChangeCallback) Unregister() error { 55 | interfaceChangeAddRemoveMutex.Lock() 56 | defer interfaceChangeAddRemoveMutex.Unlock() 57 | 58 | interfaceChangeMutex.Lock() 59 | delete(interfaceChangeCallbacks, callback) 60 | removeIt := len(interfaceChangeCallbacks) == 0 && interfaceChangeHandle != 0 61 | interfaceChangeMutex.Unlock() 62 | 63 | callback.wait.Wait() 64 | 65 | if removeIt { 66 | err := cancelMibChangeNotify2(interfaceChangeHandle) 67 | if err != nil { 68 | return err 69 | } 70 | interfaceChangeHandle = 0 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func interfaceChanged(callerContext uintptr, row *MibIPInterfaceRow, notificationType MibNotificationType) uintptr { 77 | rowCopy := *row 78 | interfaceChangeMutex.Lock() 79 | for cb := range interfaceChangeCallbacks { 80 | cb.wait.Add(1) 81 | go func(cb *InterfaceChangeCallback) { 82 | cb.cb(notificationType, &rowCopy) 83 | cb.wait.Done() 84 | }(cb) 85 | } 86 | interfaceChangeMutex.Unlock() 87 | return 0 88 | } 89 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/mksyscall.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package winipcfg 7 | 8 | //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zwinipcfg_windows.go winipcfg.go 9 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/route_change_handler.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package winipcfg 7 | 8 | import ( 9 | "sync" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | // RouteChangeCallback structure allows route change callback handling. 15 | type RouteChangeCallback struct { 16 | cb func(notificationType MibNotificationType, route *MibIPforwardRow2) 17 | wait sync.WaitGroup 18 | } 19 | 20 | var ( 21 | routeChangeAddRemoveMutex = sync.Mutex{} 22 | routeChangeMutex = sync.Mutex{} 23 | routeChangeCallbacks = make(map[*RouteChangeCallback]bool) 24 | routeChangeHandle = windows.Handle(0) 25 | ) 26 | 27 | // RegisterRouteChangeCallback registers a new RouteChangeCallback. If this particular callback is already 28 | // registered, the function will silently return. Returned RouteChangeCallback.Unregister method should be used 29 | // to unregister. 30 | func RegisterRouteChangeCallback(callback func(notificationType MibNotificationType, route *MibIPforwardRow2)) (*RouteChangeCallback, error) { 31 | s := &RouteChangeCallback{cb: callback} 32 | 33 | routeChangeAddRemoveMutex.Lock() 34 | defer routeChangeAddRemoveMutex.Unlock() 35 | 36 | routeChangeMutex.Lock() 37 | defer routeChangeMutex.Unlock() 38 | 39 | routeChangeCallbacks[s] = true 40 | 41 | if routeChangeHandle == 0 { 42 | err := notifyRouteChange2(windows.AF_UNSPEC, windows.NewCallback(routeChanged), 0, false, &routeChangeHandle) 43 | if err != nil { 44 | delete(routeChangeCallbacks, s) 45 | routeChangeHandle = 0 46 | return nil, err 47 | } 48 | } 49 | 50 | return s, nil 51 | } 52 | 53 | // Unregister unregisters the callback. 54 | func (callback *RouteChangeCallback) Unregister() error { 55 | routeChangeAddRemoveMutex.Lock() 56 | defer routeChangeAddRemoveMutex.Unlock() 57 | 58 | routeChangeMutex.Lock() 59 | delete(routeChangeCallbacks, callback) 60 | removeIt := len(routeChangeCallbacks) == 0 && routeChangeHandle != 0 61 | routeChangeMutex.Unlock() 62 | 63 | callback.wait.Wait() 64 | 65 | if removeIt { 66 | err := cancelMibChangeNotify2(routeChangeHandle) 67 | if err != nil { 68 | return err 69 | } 70 | routeChangeHandle = 0 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func routeChanged(callerContext uintptr, row *MibIPforwardRow2, notificationType MibNotificationType) uintptr { 77 | rowCopy := *row 78 | routeChangeMutex.Lock() 79 | for cb := range routeChangeCallbacks { 80 | cb.wait.Add(1) 81 | go func(cb *RouteChangeCallback) { 82 | cb.cb(notificationType, &rowCopy) 83 | cb.wait.Done() 84 | }(cb) 85 | } 86 | routeChangeMutex.Unlock() 87 | return 0 88 | } 89 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/types_test_32.go: -------------------------------------------------------------------------------- 1 | //go:build 386 || arm 2 | 3 | /* SPDX-License-Identifier: MIT 4 | * 5 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 6 | */ 7 | 8 | package winipcfg 9 | 10 | const ( 11 | ipAdapterWINSServerAddressSize = 24 12 | ipAdapterWINSServerAddressNextOffset = 8 13 | ipAdapterWINSServerAddressAddressOffset = 12 14 | 15 | ipAdapterGatewayAddressSize = 24 16 | ipAdapterGatewayAddressNextOffset = 8 17 | ipAdapterGatewayAddressAddressOffset = 12 18 | 19 | ipAdapterDNSSuffixSize = 516 20 | ipAdapterDNSSuffixStringOffset = 4 21 | 22 | ipAdapterAddressesSize = 376 23 | ipAdapterAddressesIfIndexOffset = 4 24 | ipAdapterAddressesNextOffset = 8 25 | ipAdapterAddressesAdapterNameOffset = 12 26 | ipAdapterAddressesFirstUnicastAddressOffset = 16 27 | ipAdapterAddressesFirstAnycastAddressOffset = 20 28 | ipAdapterAddressesFirstMulticastAddressOffset = 24 29 | ipAdapterAddressesFirstDNSServerAddressOffset = 28 30 | ipAdapterAddressesDNSSuffixOffset = 32 31 | ipAdapterAddressesDescriptionOffset = 36 32 | ipAdapterAddressesFriendlyNameOffset = 40 33 | ipAdapterAddressesPhysicalAddressOffset = 44 34 | ipAdapterAddressesPhysicalAddressLengthOffset = 52 35 | ipAdapterAddressesFlagsOffset = 56 36 | ipAdapterAddressesMTUOffset = 60 37 | ipAdapterAddressesIfTypeOffset = 64 38 | ipAdapterAddressesOperStatusOffset = 68 39 | ipAdapterAddressesIPv6IfIndexOffset = 72 40 | ipAdapterAddressesZoneIndicesOffset = 76 41 | ipAdapterAddressesFirstPrefixOffset = 140 42 | ipAdapterAddressesTransmitLinkSpeedOffset = 144 43 | ipAdapterAddressesReceiveLinkSpeedOffset = 152 44 | ipAdapterAddressesFirstWINSServerAddressOffset = 160 45 | ipAdapterAddressesFirstGatewayAddressOffset = 164 46 | ipAdapterAddressesIPv4MetricOffset = 168 47 | ipAdapterAddressesIPv6MetricOffset = 172 48 | ipAdapterAddressesLUIDOffset = 176 49 | ipAdapterAddressesDHCPv4ServerOffset = 184 50 | ipAdapterAddressesCompartmentIDOffset = 192 51 | ipAdapterAddressesNetworkGUIDOffset = 196 52 | ipAdapterAddressesConnectionTypeOffset = 212 53 | ipAdapterAddressesTunnelTypeOffset = 216 54 | ipAdapterAddressesDHCPv6ServerOffset = 220 55 | ipAdapterAddressesDHCPv6ClientDUIDOffset = 228 56 | ipAdapterAddressesDHCPv6ClientDUIDLengthOffset = 360 57 | ipAdapterAddressesDHCPv6IAIDOffset = 364 58 | ipAdapterAddressesFirstDNSSuffixOffset = 368 59 | ) 60 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/types_test_64.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 2 | 3 | /* SPDX-License-Identifier: MIT 4 | * 5 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 6 | */ 7 | 8 | package winipcfg 9 | 10 | const ( 11 | ipAdapterWINSServerAddressSize = 32 12 | ipAdapterWINSServerAddressNextOffset = 8 13 | ipAdapterWINSServerAddressAddressOffset = 16 14 | 15 | ipAdapterGatewayAddressSize = 32 16 | ipAdapterGatewayAddressNextOffset = 8 17 | ipAdapterGatewayAddressAddressOffset = 16 18 | 19 | ipAdapterDNSSuffixSize = 520 20 | ipAdapterDNSSuffixStringOffset = 8 21 | 22 | ipAdapterAddressesSize = 448 23 | ipAdapterAddressesIfIndexOffset = 4 24 | ipAdapterAddressesNextOffset = 8 25 | ipAdapterAddressesAdapterNameOffset = 16 26 | ipAdapterAddressesFirstUnicastAddressOffset = 24 27 | ipAdapterAddressesFirstAnycastAddressOffset = 32 28 | ipAdapterAddressesFirstMulticastAddressOffset = 40 29 | ipAdapterAddressesFirstDNSServerAddressOffset = 48 30 | ipAdapterAddressesDNSSuffixOffset = 56 31 | ipAdapterAddressesDescriptionOffset = 64 32 | ipAdapterAddressesFriendlyNameOffset = 72 33 | ipAdapterAddressesPhysicalAddressOffset = 80 34 | ipAdapterAddressesPhysicalAddressLengthOffset = 88 35 | ipAdapterAddressesFlagsOffset = 92 36 | ipAdapterAddressesMTUOffset = 96 37 | ipAdapterAddressesIfTypeOffset = 100 38 | ipAdapterAddressesOperStatusOffset = 104 39 | ipAdapterAddressesIPv6IfIndexOffset = 108 40 | ipAdapterAddressesZoneIndicesOffset = 112 41 | ipAdapterAddressesFirstPrefixOffset = 176 42 | ipAdapterAddressesTransmitLinkSpeedOffset = 184 43 | ipAdapterAddressesReceiveLinkSpeedOffset = 192 44 | ipAdapterAddressesFirstWINSServerAddressOffset = 200 45 | ipAdapterAddressesFirstGatewayAddressOffset = 208 46 | ipAdapterAddressesIPv4MetricOffset = 216 47 | ipAdapterAddressesIPv6MetricOffset = 220 48 | ipAdapterAddressesLUIDOffset = 224 49 | ipAdapterAddressesDHCPv4ServerOffset = 232 50 | ipAdapterAddressesCompartmentIDOffset = 248 51 | ipAdapterAddressesNetworkGUIDOffset = 252 52 | ipAdapterAddressesConnectionTypeOffset = 268 53 | ipAdapterAddressesTunnelTypeOffset = 272 54 | ipAdapterAddressesDHCPv6ServerOffset = 280 55 | ipAdapterAddressesDHCPv6ClientDUIDOffset = 296 56 | ipAdapterAddressesDHCPv6ClientDUIDLengthOffset = 428 57 | ipAdapterAddressesDHCPv6IAIDOffset = 432 58 | ipAdapterAddressesFirstDNSSuffixOffset = 440 59 | ) 60 | -------------------------------------------------------------------------------- /utils/network/netinterface/internal/winipcfg/unicast_address_change_handler.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package winipcfg 7 | 8 | import ( 9 | "sync" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | // UnicastAddressChangeCallback structure allows unicast address change callback handling. 15 | type UnicastAddressChangeCallback struct { 16 | cb func(notificationType MibNotificationType, unicastAddress *MibUnicastIPAddressRow) 17 | wait sync.WaitGroup 18 | } 19 | 20 | var ( 21 | unicastAddressChangeAddRemoveMutex = sync.Mutex{} 22 | unicastAddressChangeMutex = sync.Mutex{} 23 | unicastAddressChangeCallbacks = make(map[*UnicastAddressChangeCallback]bool) 24 | unicastAddressChangeHandle = windows.Handle(0) 25 | ) 26 | 27 | // RegisterUnicastAddressChangeCallback registers a new UnicastAddressChangeCallback. If this particular callback is already 28 | // registered, the function will silently return. Returned UnicastAddressChangeCallback.Unregister method should be used 29 | // to unregister. 30 | func RegisterUnicastAddressChangeCallback(callback func(notificationType MibNotificationType, unicastAddress *MibUnicastIPAddressRow)) (*UnicastAddressChangeCallback, error) { 31 | s := &UnicastAddressChangeCallback{cb: callback} 32 | 33 | unicastAddressChangeAddRemoveMutex.Lock() 34 | defer unicastAddressChangeAddRemoveMutex.Unlock() 35 | 36 | unicastAddressChangeMutex.Lock() 37 | defer unicastAddressChangeMutex.Unlock() 38 | 39 | unicastAddressChangeCallbacks[s] = true 40 | 41 | if unicastAddressChangeHandle == 0 { 42 | err := notifyUnicastIPAddressChange(windows.AF_UNSPEC, windows.NewCallback(unicastAddressChanged), 0, false, &unicastAddressChangeHandle) 43 | if err != nil { 44 | delete(unicastAddressChangeCallbacks, s) 45 | unicastAddressChangeHandle = 0 46 | return nil, err 47 | } 48 | } 49 | 50 | return s, nil 51 | } 52 | 53 | // Unregister unregisters the callback. 54 | func (callback *UnicastAddressChangeCallback) Unregister() error { 55 | unicastAddressChangeAddRemoveMutex.Lock() 56 | defer unicastAddressChangeAddRemoveMutex.Unlock() 57 | 58 | unicastAddressChangeMutex.Lock() 59 | delete(unicastAddressChangeCallbacks, callback) 60 | removeIt := len(unicastAddressChangeCallbacks) == 0 && unicastAddressChangeHandle != 0 61 | unicastAddressChangeMutex.Unlock() 62 | 63 | callback.wait.Wait() 64 | 65 | if removeIt { 66 | err := cancelMibChangeNotify2(unicastAddressChangeHandle) 67 | if err != nil { 68 | return err 69 | } 70 | unicastAddressChangeHandle = 0 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func unicastAddressChanged(callerContext uintptr, row *MibUnicastIPAddressRow, notificationType MibNotificationType) uintptr { 77 | rowCopy := *row 78 | unicastAddressChangeMutex.Lock() 79 | for cb := range unicastAddressChangeCallbacks { 80 | cb.wait.Add(1) 81 | go func(cb *UnicastAddressChangeCallback) { 82 | cb.cb(notificationType, &rowCopy) 83 | cb.wait.Done() 84 | }(cb) 85 | } 86 | unicastAddressChangeMutex.Unlock() 87 | return 0 88 | } 89 | -------------------------------------------------------------------------------- /utils/network/socks5/udp.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "syscall" 8 | 9 | "github.com/rnetx/cdns/utils/network/common" 10 | ) 11 | 12 | // +----+------+------+----------+----------+----------+ 13 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 14 | // +----+------+------+----------+----------+----------+ 15 | // | 2 | 1 | 1 | Variable | 2 | Variable | 16 | // +----+------+------+----------+----------+----------+ 17 | 18 | var ( 19 | _ net.Conn = (*AssociatePacketConn)(nil) 20 | _ net.PacketConn = (*AssociatePacketConn)(nil) 21 | ) 22 | 23 | type AssociatePacketConn struct { 24 | net.Conn 25 | tcpConn net.Conn 26 | udpRealRemoteAddr common.SocksAddr 27 | } 28 | 29 | func (c *AssociatePacketConn) RemoteAddr() net.Addr { 30 | return c.udpRealRemoteAddr.UDPAddr() 31 | } 32 | 33 | func (c *AssociatePacketConn) Close() error { 34 | defer c.tcpConn.Close() 35 | return c.Conn.Close() 36 | } 37 | 38 | func (c *AssociatePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { 39 | n, err = c.Conn.Read(p) 40 | if err != nil { 41 | return 42 | } 43 | reader := bytes.NewReader(p[:n]) 44 | if reader.Len() < 4 { 45 | n = 0 46 | err = fmt.Errorf("invalid udp packet") 47 | return 48 | } 49 | header := make([]byte, 3) 50 | _, err = reader.Read(header) 51 | if err != nil { 52 | n = 0 53 | return 54 | } 55 | if header[0] != 0x00 || header[1] != 0x00 || header[2] != 0x00 { 56 | n = 0 57 | err = fmt.Errorf("invalid udp packet") 58 | return 59 | } 60 | socksAddr, err := readSocksAddr(reader) 61 | if err != nil { 62 | n = 0 63 | err = fmt.Errorf("invalid udp packet") 64 | return 65 | } 66 | index := 3 + socksAddrLen(socksAddr) 67 | n = copy(p, p[index:n]) 68 | addr = socksAddr.UDPAddr() 69 | return 70 | } 71 | 72 | func (c *AssociatePacketConn) Read(p []byte) (n int, err error) { 73 | n, _, err = c.ReadFrom(p) 74 | return 75 | } 76 | 77 | func (c *AssociatePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { 78 | socksAddr, err := common.NewSocksAddrFromString(addr.String()) 79 | if err != nil { 80 | err = fmt.Errorf("invalid udp address: %s", err) 81 | return 82 | } 83 | buffer := bytes.NewBuffer(make([]byte, 0, len(p)+3+socksAddrLen(socksAddr))) 84 | buffer.Write([]byte{0x00, 0x00, 0x00}) 85 | err = writeSocksAddr(buffer, socksAddr) 86 | if err != nil { 87 | return 88 | } 89 | buffer.Write(p) 90 | _, err = c.Conn.Write(buffer.Bytes()) 91 | if err != nil { 92 | return 93 | } 94 | n = len(p) 95 | return 96 | } 97 | 98 | func (c *AssociatePacketConn) Write(p []byte) (n int, err error) { 99 | buffer := bytes.NewBuffer(make([]byte, 0, len(p)+3+socksAddrLen(&c.udpRealRemoteAddr))) 100 | buffer.Write([]byte{0x00, 0x00, 0x00}) 101 | err = writeSocksAddr(buffer, &c.udpRealRemoteAddr) 102 | if err != nil { 103 | return 104 | } 105 | buffer.Write(p) 106 | _, err = c.Conn.Write(buffer.Bytes()) 107 | if err != nil { 108 | return 109 | } 110 | n = len(p) 111 | return 112 | } 113 | 114 | // QUIC 115 | func (c *AssociatePacketConn) SetReadBuffer(n int) error { 116 | udpConn := c.Conn.(*net.UDPConn) 117 | return udpConn.SetReadBuffer(n + 261) 118 | } 119 | 120 | // QUIC 121 | func (c *AssociatePacketConn) SetWriteBuffer(n int) error { 122 | udpConn := c.Conn.(*net.UDPConn) 123 | return udpConn.SetWriteBuffer(n + 261) 124 | } 125 | 126 | // QUIC 127 | func (c *AssociatePacketConn) SyscallConn() (syscall.RawConn, error) { 128 | conn, ok := c.Conn.(interface { 129 | SyscallConn() (syscall.RawConn, error) 130 | }) 131 | if !ok { 132 | return nil, fmt.Errorf("not support syscall conn") 133 | } 134 | return conn.SyscallConn() 135 | } 136 | -------------------------------------------------------------------------------- /utils/queue.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Queue[T any] struct { 4 | data []T 5 | } 6 | 7 | func NewQueue[T any]() *Queue[T] { 8 | return &Queue[T]{} 9 | } 10 | 11 | func (q *Queue[T]) Push(data T) { 12 | q.data = append(q.data, data) 13 | } 14 | 15 | func (q *Queue[T]) Pop() T { 16 | if len(q.data) == 0 { 17 | var v T 18 | return v 19 | } 20 | data := q.data[0] 21 | q.data = q.data[1:] 22 | return data 23 | } 24 | 25 | func (q *Queue[T]) Len() int { 26 | return len(q.data) 27 | } 28 | -------------------------------------------------------------------------------- /utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | ) 7 | 8 | func RandomIDUint16() uint16 { 9 | var output uint16 10 | err := binary.Read(rand.Reader, binary.BigEndian, &output) 11 | if err != nil { 12 | panic("reading random id failed: " + err.Error()) 13 | } 14 | return output 15 | } 16 | -------------------------------------------------------------------------------- /utils/result.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Result[T any] struct { 4 | Value T 5 | Error error 6 | } 7 | -------------------------------------------------------------------------------- /utils/stack.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Stack[T any] struct { 4 | data []T 5 | } 6 | 7 | func NewStack[T any](size int) *Stack[T] { 8 | s := &Stack[T]{} 9 | if size > 0 { 10 | s.data = make([]T, 0, size) 11 | } 12 | return s 13 | } 14 | 15 | func (s *Stack[T]) Push(v T) { 16 | s.data = append(s.data, v) 17 | } 18 | 19 | func (s *Stack[T]) Pop() T { 20 | if len(s.data) == 0 { 21 | var v T 22 | return v 23 | } 24 | v := s.data[len(s.data)-1] 25 | s.data = s.data[:len(s.data)-1] 26 | return v 27 | } 28 | 29 | func (s *Stack[T]) Peek() T { 30 | if len(s.data) == 0 { 31 | var v T 32 | return v 33 | } 34 | return s.data[len(s.data)-1] 35 | } 36 | 37 | func (s *Stack[T]) Len() int { 38 | return len(s.data) 39 | } 40 | -------------------------------------------------------------------------------- /utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unsafe" 7 | ) 8 | 9 | func Join[T fmt.Stringer](arr []T, seq string) string { 10 | s := make([]string, 0, len(arr)) 11 | for _, v := range arr { 12 | s = append(s, v.String()) 13 | } 14 | return strings.Join(s, seq) 15 | } 16 | 17 | // from mosdns(https://github.com/IrineSistiana/mosdns), thank for @IrineSistiana 18 | // BytesToStringUnsafe converts bytes to string. 19 | func BytesToStringUnsafe(b []byte) string { 20 | return unsafe.String(unsafe.SliceData(b), len(b)) 21 | } 22 | -------------------------------------------------------------------------------- /utils/task.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | ) 7 | 8 | type TaskGroup struct { 9 | ctx context.Context 10 | n *atomic.Int32 11 | doneCtx context.Context 12 | doneFunc context.CancelFunc 13 | } 14 | 15 | func NewTaskGroupWithContext(ctx context.Context) *TaskGroup { 16 | g := &TaskGroup{ 17 | ctx: ctx, 18 | n: &atomic.Int32{}, 19 | } 20 | g.doneCtx, g.doneFunc = context.WithCancel(g.ctx) 21 | g.n.Add(1) 22 | return g 23 | } 24 | 25 | func NewTaskGroup() *TaskGroup { 26 | return NewTaskGroupWithContext(context.Background()) 27 | } 28 | 29 | func (g *TaskGroup) Wait() <-chan struct{} { 30 | if g.n.Add(-1) == 0 { 31 | g.doneFunc() 32 | } 33 | return g.doneCtx.Done() 34 | } 35 | 36 | type Task TaskGroup 37 | 38 | func (g *TaskGroup) AddTask() *Task { 39 | g.n.Add(1) 40 | return (*Task)(g) 41 | } 42 | 43 | func (t *Task) Done() { 44 | g := (*TaskGroup)(t) 45 | if g.n.Add(-1) == 0 { 46 | g.doneFunc() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_clean.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorCleanRule)(nil) 14 | 15 | type itemExecutorCleanRule struct { 16 | clean bool 17 | } 18 | 19 | func (r *itemExecutorCleanRule) UnmarshalYAML(value *yaml.Node) error { 20 | var c bool 21 | err := value.Decode(&c) 22 | if err != nil { 23 | return fmt.Errorf("clean: %w", err) 24 | } 25 | r.clean = c 26 | return nil 27 | } 28 | 29 | func (r *itemExecutorCleanRule) check(_ context.Context, _ adapter.Core) error { 30 | return nil 31 | } 32 | 33 | func (r *itemExecutorCleanRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 34 | if r.clean { 35 | dnsCtx.SetRespMsg(nil) 36 | logger.DebugContext(ctx, "clean: clean response message") 37 | } 38 | return adapter.ReturnModeContinue, nil 39 | } 40 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_go_to.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorGoToRule)(nil) 14 | 15 | type itemExecutorGoToRule struct { 16 | goToTag string 17 | goTo adapter.Workflow 18 | } 19 | 20 | func (r *itemExecutorGoToRule) UnmarshalYAML(value *yaml.Node) error { 21 | var g string 22 | err := value.Decode(&g) 23 | if err != nil { 24 | return fmt.Errorf("go-to: %w", err) 25 | } 26 | if g == "" { 27 | return fmt.Errorf("go-to: missing go-to") 28 | } 29 | r.goToTag = g 30 | return nil 31 | } 32 | 33 | func (r *itemExecutorGoToRule) check(_ context.Context, core adapter.Core) error { 34 | w := core.GetWorkflow(r.goToTag) 35 | if w == nil { 36 | return fmt.Errorf("go-to: workflow [%s] not found", r.goToTag) 37 | } 38 | r.goTo = w 39 | r.goToTag = "" // clean 40 | return nil 41 | } 42 | 43 | func (r *itemExecutorGoToRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 44 | logger.DebugfContext(ctx, "go-to: go to workflow [%s]", r.goTo.Tag()) 45 | return r.goTo.Exec(ctx, dnsCtx) 46 | } 47 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_jump_to.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemExecutorRule = (*itemExecutorJumpToRule)(nil) 15 | 16 | type itemExecutorJumpToRule struct { 17 | jumpToTag []string 18 | jumpTo []adapter.Workflow 19 | } 20 | 21 | func (r *itemExecutorJumpToRule) UnmarshalYAML(value *yaml.Node) error { 22 | var j utils.Listable[string] 23 | err := value.Decode(&j) 24 | if err != nil { 25 | return fmt.Errorf("jump-to: %w", err) 26 | } 27 | if len(j) == 0 { 28 | return fmt.Errorf("jump-to: missing jump-to") 29 | } 30 | r.jumpToTag = j 31 | return nil 32 | } 33 | 34 | func (r *itemExecutorJumpToRule) check(_ context.Context, core adapter.Core) error { 35 | r.jumpTo = make([]adapter.Workflow, 0, len(r.jumpToTag)) 36 | for _, tag := range r.jumpToTag { 37 | w := core.GetWorkflow(tag) 38 | if w == nil { 39 | return fmt.Errorf("jump-to: workflow [%s] not found", tag) 40 | } 41 | r.jumpTo = append(r.jumpTo, w) 42 | } 43 | r.jumpToTag = nil // clean 44 | return nil 45 | } 46 | 47 | func (r *itemExecutorJumpToRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 48 | for _, w := range r.jumpTo { 49 | logger.DebugfContext(ctx, "jump-to: jump to workflow [%s]", w.Tag()) 50 | returnMode, err := w.Exec(ctx, dnsCtx) 51 | if err != nil { 52 | logger.ErrorfContext(ctx, "jump-to: workflow [%s] exec failed: %v", w.Tag(), err) 53 | return adapter.ReturnModeUnknown, err 54 | } 55 | logger.DebugfContext(ctx, "jump-to: workflow [%s]: %s", w.Tag(), returnMode.String()) 56 | if returnMode != adapter.ReturnModeContinue { 57 | return returnMode, nil 58 | } 59 | } 60 | return adapter.ReturnModeContinue, nil 61 | } 62 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_mark.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorMarkRule)(nil) 14 | 15 | type itemExecutorMarkRule struct { 16 | mark uint64 17 | } 18 | 19 | func (r *itemExecutorMarkRule) UnmarshalYAML(value *yaml.Node) error { 20 | var m uint64 21 | err := value.Decode(&m) 22 | if err != nil { 23 | return fmt.Errorf("mark: %w", err) 24 | } 25 | r.mark = m 26 | return nil 27 | } 28 | 29 | func (r *itemExecutorMarkRule) check(_ context.Context, _ adapter.Core) error { 30 | return nil 31 | } 32 | 33 | func (r *itemExecutorMarkRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 34 | mark := dnsCtx.Mark() 35 | dnsCtx.SetMark(r.mark) 36 | logger.DebugfContext(ctx, "mark: set mark: %d => %d", mark, r.mark) 37 | return adapter.ReturnModeContinue, nil 38 | } 39 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_metadata.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorMetadataRule)(nil) 14 | 15 | type itemExecutorMetadataRule struct { 16 | metadata map[string]string 17 | } 18 | 19 | func (r *itemExecutorMetadataRule) UnmarshalYAML(value *yaml.Node) error { 20 | var m map[string]string 21 | err := value.Decode(&m) 22 | if err != nil { 23 | return fmt.Errorf("metadata: %w", err) 24 | } 25 | r.metadata = m 26 | return nil 27 | } 28 | 29 | func (r *itemExecutorMetadataRule) check(_ context.Context, _ adapter.Core) error { 30 | return nil 31 | } 32 | 33 | func (r *itemExecutorMetadataRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 34 | metadata := dnsCtx.Metadata() 35 | for k, v := range r.metadata { 36 | if v == "" { 37 | logger.DebugfContext(ctx, "metadata: delete metadata: %s", k) 38 | delete(metadata, k) 39 | } else { 40 | vv, ok := metadata[k] 41 | if ok { 42 | logger.DebugfContext(ctx, "metadata: set metadata: key: %s, value: %s => %s", k, vv, v) 43 | } else { 44 | logger.DebugfContext(ctx, "metadata: set metadata: key: %s, value: %s", k, v) 45 | } 46 | metadata[k] = v 47 | } 48 | } 49 | return adapter.ReturnModeContinue, nil 50 | } 51 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_parallel.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemExecutorRule = (*itemExecutorParallelRule)(nil) 15 | 16 | type itemExecutorParallelRule struct { 17 | workflowTags []string 18 | workflows []adapter.Workflow 19 | } 20 | 21 | type itemExecutorParallelRuleOptions struct { 22 | Workflows utils.Listable[string] `yaml:"workflows,omitempty"` 23 | } 24 | 25 | func (r *itemExecutorParallelRule) UnmarshalYAML(value *yaml.Node) error { 26 | var o itemExecutorParallelRuleOptions 27 | err := value.Decode(&o) 28 | if err != nil { 29 | return fmt.Errorf("parallel: %w", err) 30 | } 31 | r.workflowTags = o.Workflows 32 | return nil 33 | } 34 | 35 | func (r *itemExecutorParallelRule) check(ctx context.Context, core adapter.Core) error { 36 | r.workflows = make([]adapter.Workflow, 0, len(r.workflowTags)) 37 | for _, tag := range r.workflowTags { 38 | w := core.GetWorkflow(tag) 39 | if w == nil { 40 | return fmt.Errorf("parallel: workflow [%s] not found", tag) 41 | } 42 | r.workflows = append(r.workflows, w) 43 | } 44 | r.workflowTags = nil // clean 45 | return nil 46 | } 47 | 48 | func (r *itemExecutorParallelRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 49 | ch := utils.NewSafeChan[parallelResult](1) 50 | defer ch.Close() 51 | for i, w := range r.workflows { 52 | iDNSCtx := dnsCtx.Clone() 53 | iDNSCtx.SetID(iDNSCtx.ID() + uint32(i) + 1) 54 | iDNSCtx.FlushColor() 55 | logger.DebugfContext(ctx, "parallel: workflow [%s] exec, id: %d", w.Tag(), iDNSCtx.ID()) 56 | go func( 57 | ctx context.Context, 58 | dnsCtx *adapter.DNSContext, 59 | ch *utils.SafeChan[parallelResult], 60 | w adapter.Workflow, 61 | ) { 62 | defer ch.Close() 63 | returnMode, err := w.Exec(adapter.SaveLogContext(ctx, dnsCtx), dnsCtx) 64 | if err == nil { 65 | select { 66 | case ch.SendChan() <- parallelResult{ 67 | w: w, 68 | dnsCtx: dnsCtx, 69 | returnMode: returnMode, 70 | }: 71 | default: 72 | } 73 | } else { 74 | select { 75 | case ch.SendChan() <- parallelResult{ 76 | w: w, 77 | err: err, 78 | }: 79 | default: 80 | } 81 | } 82 | }( 83 | ctx, 84 | iDNSCtx, 85 | ch.Clone(), 86 | w, 87 | ) 88 | } 89 | for i := 0; i < len(r.workflows); i++ { 90 | select { 91 | case result := <-ch.ReceiveChan(): 92 | if result.err == nil { 93 | logger.DebugfContext(ctx, "parallel: workflow [%s] exec success: %s", result.w.Tag(), result.returnMode.String()) 94 | oldID := dnsCtx.ID() 95 | *dnsCtx = *result.dnsCtx 96 | dnsCtx.SetID(oldID) 97 | dnsCtx.FlushColor() 98 | return result.returnMode, nil 99 | } 100 | case <-ctx.Done(): 101 | logger.ErrorContext(ctx, "parallel: timeout") 102 | return adapter.ReturnModeUnknown, ctx.Err() 103 | } 104 | } 105 | err := fmt.Errorf("parallel: all workflow exec failed") 106 | logger.ErrorContext(ctx, err) 107 | return adapter.ReturnModeUnknown, err 108 | } 109 | 110 | type parallelResult struct { 111 | w adapter.Workflow 112 | dnsCtx *adapter.DNSContext 113 | returnMode adapter.ReturnMode 114 | err error 115 | } 116 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_plugin_executor.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorPluginExecutorRule)(nil) 14 | 15 | type itemExecutorPluginExecutorRule struct { 16 | tag string 17 | args any 18 | argsID uint16 19 | executor adapter.PluginExecutor 20 | } 21 | 22 | type itemExecutorPluginExecutorRuleOptions struct { 23 | Tag string `yaml:"tag,omitempty"` 24 | Args any `yaml:"args,omitempty"` 25 | } 26 | 27 | func (r *itemExecutorPluginExecutorRule) UnmarshalYAML(value *yaml.Node) error { 28 | var o itemExecutorPluginExecutorRuleOptions 29 | err := value.Decode(&o) 30 | if err != nil { 31 | return fmt.Errorf("plugin: %w", err) 32 | } 33 | if o.Tag == "" { 34 | return fmt.Errorf("plugin: missing tag") 35 | } 36 | r.tag = o.Tag 37 | r.args = o.Args 38 | return nil 39 | } 40 | 41 | func (r *itemExecutorPluginExecutorRule) check(ctx context.Context, core adapter.Core) error { 42 | p := core.GetPluginExecutor(r.tag) 43 | if p == nil { 44 | return fmt.Errorf("plugin: plugin executor [%s] not found", r.tag) 45 | } 46 | id, err := p.LoadRunningArgs(ctx, r.args) 47 | if err != nil { 48 | return fmt.Errorf("plugin: plugin executor [%s] load running args failed: %v", r.tag, err) 49 | } 50 | r.argsID = id 51 | r.executor = p 52 | r.tag = "" // clean 53 | r.args = nil // clean 54 | return nil 55 | } 56 | 57 | func (r *itemExecutorPluginExecutorRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 58 | returnMode, err := r.executor.Exec(ctx, dnsCtx, r.argsID) 59 | if err != nil { 60 | logger.DebugfContext(ctx, "plugin: plugin executor [%s] exec failed: %v", r.executor.Tag(), err) 61 | return adapter.ReturnModeUnknown, err 62 | } 63 | logger.DebugfContext(ctx, "plugin: plugin executor [%s]: %s", r.executor.Tag(), returnMode.String()) 64 | return returnMode, nil 65 | } 66 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_return.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "github.com/miekg/dns" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | var _ itemExecutorRule = (*itemExecutorReturnRule)(nil) 17 | 18 | type itemExecutorReturnRule struct { 19 | _return string 20 | } 21 | 22 | func (r *itemExecutorReturnRule) UnmarshalYAML(value *yaml.Node) error { 23 | var re any 24 | err := value.Decode(&re) 25 | if err != nil { 26 | return fmt.Errorf("return: %w", err) 27 | } 28 | switch rr := re.(type) { 29 | case string: 30 | rr = strings.ToLower(rr) 31 | switch rr { 32 | case "all": 33 | r._return = "all" 34 | case "once": 35 | r._return = "once" 36 | case "success": 37 | r._return = "success" // all 38 | case "failure", "fail": 39 | r._return = "failure" // all 40 | case "nxdomain": 41 | r._return = "nxdomain" // all 42 | case "refused": 43 | r._return = "refused" // all 44 | default: 45 | return fmt.Errorf("return: invalid return: %s", rr) 46 | } 47 | case bool: 48 | if rr { 49 | r._return = "all" 50 | } 51 | default: 52 | return fmt.Errorf("return: invalid return: %v", rr) 53 | } 54 | return nil 55 | } 56 | 57 | func (r *itemExecutorReturnRule) check(_ context.Context, _ adapter.Core) error { 58 | return nil 59 | } 60 | 61 | func (r *itemExecutorReturnRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 62 | var rcode int 63 | switch r._return { 64 | case "all": 65 | logger.DebugContext(ctx, "return: return all") 66 | return adapter.ReturnModeReturnAll, nil 67 | case "once": 68 | logger.DebugContext(ctx, "return: return once") 69 | return adapter.ReturnModeReturnOnce, nil 70 | case "success": 71 | logger.DebugContext(ctx, "return: return success") 72 | rcode = dns.RcodeSuccess 73 | case "failure": 74 | logger.DebugContext(ctx, "return: return failure") 75 | rcode = dns.RcodeServerFailure 76 | case "nxdomain": 77 | logger.DebugContext(ctx, "return: return nxdomain") 78 | rcode = dns.RcodeNameError 79 | case "refused": 80 | logger.DebugContext(ctx, "return: return refused") 81 | rcode = dns.RcodeRefused 82 | case "": 83 | return adapter.ReturnModeContinue, nil 84 | } 85 | name := dnsCtx.ReqMsg().Question[0].Name 86 | newRespMsg := &dns.Msg{} 87 | newRespMsg.SetRcode(dnsCtx.ReqMsg(), rcode) 88 | newRespMsg.Ns = []dns.RR{utils.FakeSOA(name)} 89 | dnsCtx.SetRespMsg(newRespMsg) 90 | return adapter.ReturnModeReturnAll, nil 91 | } 92 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_set_resp_ip.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/netip" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "github.com/miekg/dns" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | var _ itemExecutorRule = (*itemExecutorSetRespIPRule)(nil) 17 | 18 | type itemExecutorSetRespIPRule struct { 19 | ipv4 bool 20 | ipv6 bool 21 | addr []netip.Prefix 22 | } 23 | 24 | func (r *itemExecutorSetRespIPRule) UnmarshalYAML(value *yaml.Node) error { 25 | var i utils.Listable[string] 26 | err := value.Decode(&i) 27 | if err != nil { 28 | return fmt.Errorf("set-resp-ip: %w", err) 29 | } 30 | if len(i) == 0 { 31 | return fmt.Errorf("set-resp-ip: ip is empty") 32 | } 33 | r.addr = make([]netip.Prefix, 0, len(i)) 34 | for _, s := range i { 35 | ip, err := netip.ParseAddr(s) 36 | if err == nil { 37 | bits := 0 38 | if ip.Is4() { 39 | bits = 32 40 | r.ipv4 = true 41 | } else { 42 | bits = 128 43 | r.ipv6 = true 44 | } 45 | r.addr = append(r.addr, netip.PrefixFrom(ip, bits)) 46 | continue 47 | } 48 | prefix, err := netip.ParsePrefix(s) 49 | if err == nil { 50 | if prefix.Addr().Is4() { 51 | r.ipv4 = true 52 | } else { 53 | r.ipv6 = true 54 | } 55 | r.addr = append(r.addr, prefix) 56 | continue 57 | } 58 | return fmt.Errorf("set-resp-ip: invalid ip: %s, error: %w", s, err) 59 | } 60 | return nil 61 | } 62 | 63 | func (r *itemExecutorSetRespIPRule) check(_ context.Context, _ adapter.Core) error { 64 | return nil 65 | } 66 | 67 | func (r *itemExecutorSetRespIPRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 68 | reqMsg := dnsCtx.ReqMsg() 69 | if reqMsg == nil { 70 | return adapter.ReturnModeContinue, fmt.Errorf("set-resp-ip: request message is nil") 71 | } 72 | question := reqMsg.Question[0] 73 | qName := question.Name 74 | qType := question.Qtype 75 | if qType == dns.TypeA && !r.ipv4 { 76 | return adapter.ReturnModeContinue, fmt.Errorf("set-resp-ip: request type is A, but no ipv4 ip") 77 | } 78 | if qType == dns.TypeAAAA && !r.ipv6 { 79 | return adapter.ReturnModeContinue, fmt.Errorf("set-resp-ip: request type is AAAA, but no ipv6 ip") 80 | } 81 | answers := make([]dns.RR, 0, len(r.addr)) 82 | for _, addr := range r.addr { 83 | ip := addr.Addr() 84 | if (addr.Bits() == 32 && ip.Is4()) || (addr.Bits() == 128 && ip.Is6()) { 85 | ip = utils.RandomAddrFromPrefix(addr) 86 | } 87 | if qType == dns.TypeA && ip.Is4() { 88 | answers = append(answers, &dns.A{ 89 | Hdr: dns.RR_Header{ 90 | Name: qName, 91 | Rrtype: dns.TypeA, 92 | Class: dns.ClassINET, 93 | Ttl: 600, 94 | }, 95 | A: ip.AsSlice(), 96 | }) 97 | } 98 | if qType == dns.TypeAAAA && ip.Is6() { 99 | answers = append(answers, &dns.AAAA{ 100 | Hdr: dns.RR_Header{ 101 | Name: qName, 102 | Rrtype: dns.TypeAAAA, 103 | Class: dns.ClassINET, 104 | Ttl: 600, 105 | }, 106 | AAAA: ip.AsSlice(), 107 | }) 108 | } 109 | } 110 | respMsg := &dns.Msg{} 111 | respMsg.SetReply(reqMsg) 112 | respMsg.Answer = answers 113 | dnsCtx.SetRespMsg(respMsg) 114 | return adapter.ReturnModeContinue, nil 115 | } 116 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_set_ttl.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemExecutorRule = (*itemExecutorSetTTLRule)(nil) 14 | 15 | type itemExecutorSetTTLRule struct { 16 | ttl uint32 17 | } 18 | 19 | func (r *itemExecutorSetTTLRule) UnmarshalYAML(value *yaml.Node) error { 20 | var s uint32 21 | err := value.Decode(&s) 22 | if err != nil { 23 | return fmt.Errorf("set-ttl: %w", err) 24 | } 25 | r.ttl = s 26 | return nil 27 | } 28 | 29 | func (r *itemExecutorSetTTLRule) check(_ context.Context, _ adapter.Core) error { 30 | return nil 31 | } 32 | 33 | func (r *itemExecutorSetTTLRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 34 | respMsg := dnsCtx.RespMsg() 35 | if respMsg == nil { 36 | logger.DebugfContext(ctx, "set-ttl: response message is nil") 37 | return adapter.ReturnModeContinue, nil 38 | } 39 | for i := range respMsg.Answer { 40 | respMsg.Answer[i].Header().Ttl = r.ttl 41 | } 42 | for i := range respMsg.Ns { 43 | respMsg.Ns[i].Header().Ttl = r.ttl 44 | } 45 | for i := range respMsg.Extra { 46 | respMsg.Extra[i].Header().Ttl = r.ttl 47 | } 48 | logger.DebugfContext(ctx, "set-ttl: %d", r.ttl) 49 | return adapter.ReturnModeContinue, nil 50 | } 51 | -------------------------------------------------------------------------------- /workflow/item_executor_rule_workflow_rules.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemExecutorRule = (*itemExecutorWorkflowRulesRule)(nil) 15 | 16 | type itemExecutorWorkflowRulesRule struct { 17 | rules []Rule 18 | } 19 | 20 | func (r *itemExecutorWorkflowRulesRule) UnmarshalYAML(value *yaml.Node) error { 21 | var w utils.Listable[RuleOptions] 22 | err := value.Decode(&w) 23 | if err != nil { 24 | return fmt.Errorf("workflow-rules: %w", err) 25 | } 26 | if len(w) == 0 { 27 | return fmt.Errorf("workflow-rules: missing workflow-rules") 28 | } 29 | r.rules = make([]Rule, 0, len(w)) 30 | for _, o := range w { 31 | r.rules = append(r.rules, o.rule) 32 | } 33 | return nil 34 | } 35 | 36 | func (r *itemExecutorWorkflowRulesRule) check(ctx context.Context, core adapter.Core) error { 37 | for i, w := range r.rules { 38 | err := w.Check(ctx, core) 39 | if err != nil { 40 | return fmt.Errorf("workflow-rules: workflow-rule[%d] check failed: %v", i, err) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (r *itemExecutorWorkflowRulesRule) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 47 | for i, w := range r.rules { 48 | logger.DebugfContext(ctx, "workflow-rules: workflow-rule[%d] exec", i) 49 | returnMode, err := w.Exec(ctx, core, logger, dnsCtx) 50 | if err != nil { 51 | logger.ErrorfContext(ctx, "workflow-rules: workflow-rule[%d] exec failed: %v", i, err) 52 | return adapter.ReturnModeUnknown, err 53 | } 54 | switch returnMode { 55 | case adapter.ReturnModeReturnAll: 56 | logger.DebugfContext(ctx, "workflow-rules: workflow-rule[%d]: %s", i, returnMode.String()) 57 | return returnMode, nil 58 | case adapter.ReturnModeReturnOnce: 59 | logger.DebugfContext(ctx, "workflow-rules: workflow-rule[%d]: %s", i, returnMode.String()) 60 | return adapter.ReturnModeContinue, nil 61 | } 62 | } 63 | return adapter.ReturnModeContinue, nil 64 | } 65 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_client_ip.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/netip" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | var _ itemMatcherRule = (*itemMatcherClientIPRule)(nil) 16 | 17 | type itemMatcherClientIPRule struct { 18 | clientIP []netip.Prefix 19 | } 20 | 21 | func (r *itemMatcherClientIPRule) UnmarshalYAML(value *yaml.Node) error { 22 | var c utils.Listable[string] 23 | err := value.Decode(&c) 24 | if err != nil { 25 | return fmt.Errorf("client-ip: %w", err) 26 | } 27 | if len(c) == 0 { 28 | return fmt.Errorf("client-ip: missing client-ip") 29 | } 30 | r.clientIP = make([]netip.Prefix, 0, len(c)) 31 | for _, s := range c { 32 | prefix, err := netip.ParsePrefix(s) 33 | if err == nil { 34 | r.clientIP = append(r.clientIP, prefix) 35 | continue 36 | } 37 | ip, err := netip.ParseAddr(s) 38 | if err == nil { 39 | bits := 0 40 | if ip.Is4() { 41 | bits = 32 42 | } else { 43 | bits = 128 44 | } 45 | r.clientIP = append(r.clientIP, netip.PrefixFrom(ip, bits)) 46 | continue 47 | } 48 | return fmt.Errorf("client-ip: invalid client-ip: %s", s) 49 | } 50 | return nil 51 | } 52 | 53 | func (r *itemMatcherClientIPRule) check(_ context.Context, _ adapter.Core) error { 54 | return nil 55 | } 56 | 57 | func (r *itemMatcherClientIPRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 58 | clientIP := dnsCtx.ClientIP() 59 | for _, prefix := range r.clientIP { 60 | if prefix.Contains(clientIP) { 61 | logger.DebugfContext(ctx, "client-ip: match client-ip: %s => %s", prefix.String(), clientIP.String()) 62 | return true, nil 63 | } 64 | } 65 | logger.DebugfContext(ctx, "client-ip: no match client-ip: %s", clientIP.String()) 66 | return false, nil 67 | } 68 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_env.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemMatcherRule = (*itemMatcherEnvRule)(nil) 15 | 16 | type itemMatcherEnvRule struct { 17 | env map[string]string 18 | } 19 | 20 | func (r *itemMatcherEnvRule) UnmarshalYAML(value *yaml.Node) error { 21 | var e map[string]string 22 | err := value.Decode(&e) 23 | if err != nil { 24 | return fmt.Errorf("env: %w", err) 25 | } 26 | if len(e) == 0 { 27 | return fmt.Errorf("env: missing env") 28 | } 29 | r.env = e 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherEnvRule) check(_ context.Context, _ adapter.Core) error { 34 | return nil 35 | } 36 | 37 | func (r *itemMatcherEnvRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 38 | for k, v := range r.env { 39 | if os.Getenv(k) == v { 40 | logger.DebugfContext(ctx, "env: match env: %s => %s", k, v) 41 | return true, nil 42 | } 43 | } 44 | logger.DebugContext(ctx, "env: no match env") 45 | return false, nil 46 | } 47 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_has_resp_msg.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemMatcherRule = (*itemMatcherHasRespMsgRule)(nil) 14 | 15 | type itemMatcherHasRespMsgRule struct { 16 | hasRespMsg bool 17 | } 18 | 19 | func (r *itemMatcherHasRespMsgRule) UnmarshalYAML(value *yaml.Node) error { 20 | var rr bool 21 | err := value.Decode(&rr) 22 | if err != nil { 23 | return fmt.Errorf("has-resp-msg: %w", err) 24 | } 25 | r.hasRespMsg = rr 26 | return nil 27 | } 28 | 29 | func (r *itemMatcherHasRespMsgRule) check(_ context.Context, _ adapter.Core) error { 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherHasRespMsgRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 34 | respMsg := dnsCtx.RespMsg() 35 | if r.hasRespMsg && respMsg != nil { 36 | logger.DebugfContext(ctx, "has-resp-msg: match has-resp-msg: true") 37 | return true, nil 38 | } 39 | if !r.hasRespMsg && respMsg == nil { 40 | logger.DebugfContext(ctx, "has-resp-msg: match has-resp-msg: false") 41 | return true, nil 42 | } 43 | logger.DebugfContext(ctx, "has-resp-msg: no match has-resp-msg: %t", respMsg != nil) 44 | return false, nil 45 | } 46 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_listener.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemMatcherRule = (*itemMatcherListenerRule)(nil) 15 | 16 | type itemMatcherListenerRule struct { 17 | listener []string 18 | } 19 | 20 | func (r *itemMatcherListenerRule) UnmarshalYAML(value *yaml.Node) error { 21 | var l utils.Listable[string] 22 | err := value.Decode(&l) 23 | if err != nil { 24 | return fmt.Errorf("listener: %w", err) 25 | } 26 | if len(l) == 0 { 27 | return fmt.Errorf("listener: missing listener") 28 | } 29 | r.listener = l 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherListenerRule) check(_ context.Context, core adapter.Core) error { 34 | for _, l := range r.listener { 35 | if core.GetListener(l) == nil { 36 | return fmt.Errorf("listener: listener [%s] not found", l) 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func (r *itemMatcherListenerRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 43 | ll := dnsCtx.Listener() 44 | for _, l := range r.listener { 45 | if ll == l { 46 | logger.DebugfContext(ctx, "listener: match listener: %s", l) 47 | return true, nil 48 | } 49 | } 50 | logger.DebugfContext(ctx, "listener: no match listener: %s", ll) 51 | return false, nil 52 | } 53 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_mark.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemMatcherRule = (*itemMatcherMarkRule)(nil) 15 | 16 | type itemMatcherMarkRule struct { 17 | mark []uint64 18 | } 19 | 20 | func (r *itemMatcherMarkRule) UnmarshalYAML(value *yaml.Node) error { 21 | var m utils.Listable[uint64] 22 | err := value.Decode(&m) 23 | if err != nil { 24 | return fmt.Errorf("mark: %w", err) 25 | } 26 | if len(m) == 0 { 27 | return fmt.Errorf("mark: missing mark") 28 | } 29 | r.mark = m 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherMarkRule) check(_ context.Context, _ adapter.Core) error { 34 | return nil 35 | } 36 | 37 | func (r *itemMatcherMarkRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 38 | mark := dnsCtx.Mark() 39 | for _, m := range r.mark { 40 | if m == mark { 41 | logger.DebugfContext(ctx, "mark: match mark: %d", m) 42 | return true, nil 43 | } 44 | } 45 | logger.DebugfContext(ctx, "mark: no match mark: %d", mark) 46 | return false, nil 47 | } 48 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_match_and.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemMatcherRule = (*itemMatcherMatchAndRule)(nil) 15 | 16 | type itemMatcherMatchAndRule struct { 17 | matchAnd []RuleItemMatch 18 | } 19 | 20 | func (r *itemMatcherMatchAndRule) UnmarshalYAML(value *yaml.Node) error { 21 | var m utils.Listable[RuleItemMatch] 22 | err := value.Decode(&m) 23 | if err != nil { 24 | return fmt.Errorf("match-and: %w", err) 25 | } 26 | if len(m) == 0 { 27 | return fmt.Errorf("match-and: missing match rule") 28 | } 29 | r.matchAnd = m 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherMatchAndRule) check(ctx context.Context, core adapter.Core) error { 34 | for i, m := range r.matchAnd { 35 | err := m.check(ctx, core) 36 | if err != nil { 37 | return fmt.Errorf("match-and: match-and[%d] check failed: %v", i, err) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (r *itemMatcherMatchAndRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 44 | match := true 45 | for i, m := range r.matchAnd { 46 | logger.DebugfContext(ctx, "match-and: match match-and[%d]", i) 47 | matched, err := m.match(ctx, core, logger, dnsCtx) 48 | if err != nil { 49 | logger.DebugfContext(ctx, "match-and: match match-and[%d] failed: %v", i, err) 50 | return false, err 51 | } 52 | if !matched { 53 | logger.DebugfContext(ctx, "match-and: match match-and[%d] => false", i) 54 | match = false 55 | break 56 | } 57 | logger.DebugfContext(ctx, "match-and: match match-and[%d] => true, continue", i) 58 | } 59 | if match { 60 | logger.DebugfContext(ctx, "match-and: match match-and: true") 61 | return true, nil 62 | } else { 63 | logger.DebugfContext(ctx, "match-and: no match match-and") 64 | return false, nil 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_match_or.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | var _ itemMatcherRule = (*itemMatcherMatchOrRule)(nil) 15 | 16 | type itemMatcherMatchOrRule struct { 17 | matchOr []RuleItemMatch 18 | } 19 | 20 | func (r *itemMatcherMatchOrRule) UnmarshalYAML(value *yaml.Node) error { 21 | var m utils.Listable[RuleItemMatch] 22 | err := value.Decode(&m) 23 | if err != nil { 24 | return fmt.Errorf("match-or: %w", err) 25 | } 26 | if len(m) == 0 { 27 | return fmt.Errorf("match-or: missing match rule") 28 | } 29 | r.matchOr = m 30 | return nil 31 | } 32 | 33 | func (r *itemMatcherMatchOrRule) check(ctx context.Context, core adapter.Core) error { 34 | for i, m := range r.matchOr { 35 | err := m.check(ctx, core) 36 | if err != nil { 37 | return fmt.Errorf("match-or: match-or[%d] check failed: %v", i, err) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (r *itemMatcherMatchOrRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 44 | match := false 45 | for i, m := range r.matchOr { 46 | logger.DebugfContext(ctx, "match-or: match match-or[%d]", i) 47 | matched, err := m.match(ctx, core, logger, dnsCtx) 48 | if err != nil { 49 | logger.DebugfContext(ctx, "match-or: match match-or[%d] failed: %v", i, err) 50 | return false, err 51 | } 52 | if matched { 53 | logger.DebugfContext(ctx, "match-or: match match-or[%d] => true", i) 54 | match = true 55 | break 56 | } 57 | logger.DebugfContext(ctx, "match-or: match match-or[%d] => false, continue", i) 58 | } 59 | if match { 60 | logger.DebugfContext(ctx, "match-or: match match-or: true") 61 | return true, nil 62 | } else { 63 | logger.DebugfContext(ctx, "match-or: no match match-or") 64 | return false, nil 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_metadata.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemMatcherRule = (*itemMatcherMetadataRule)(nil) 14 | 15 | type itemMatcherMetadataRule struct { 16 | metadata map[string]string 17 | } 18 | 19 | func (r *itemMatcherMetadataRule) UnmarshalYAML(value *yaml.Node) error { 20 | var m map[string]string 21 | err := value.Decode(&m) 22 | if err != nil { 23 | return fmt.Errorf("metadata: %w", err) 24 | } 25 | if len(m) == 0 { 26 | return fmt.Errorf("metadata: missing metadata") 27 | } 28 | r.metadata = m 29 | return nil 30 | } 31 | 32 | func (r *itemMatcherMetadataRule) check(_ context.Context, _ adapter.Core) error { 33 | return nil 34 | } 35 | 36 | func (r *itemMatcherMetadataRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 37 | metadata := dnsCtx.Metadata() 38 | for k1, v1 := range r.metadata { 39 | v2, ok := metadata[k1] 40 | if ok || v2 == v1 { 41 | logger.DebugfContext(ctx, "metadata: match metadata: %s => %s", k1, v1) 42 | return true, nil 43 | } 44 | } 45 | logger.DebugContext(ctx, "metadata: no match metadata") 46 | return false, nil 47 | } 48 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_plugin_matcher.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ itemMatcherRule = (*itemMatcherPluginMatcherRule)(nil) 14 | 15 | type itemMatcherPluginMatcherRule struct { 16 | tag string 17 | args any 18 | argsID uint16 19 | matcher adapter.PluginMatcher 20 | } 21 | 22 | type itemMatcherPluginMatcherRuleOptions struct { 23 | Tag string `yaml:"tag,omitempty"` 24 | Args any `yaml:"args,omitempty"` 25 | } 26 | 27 | func (r *itemMatcherPluginMatcherRule) UnmarshalYAML(value *yaml.Node) error { 28 | var o itemMatcherPluginMatcherRuleOptions 29 | err := value.Decode(&o) 30 | if err != nil { 31 | return fmt.Errorf("plugin: %w", err) 32 | } 33 | if o.Tag == "" { 34 | return fmt.Errorf("plugin: missing tag") 35 | } 36 | r.tag = o.Tag 37 | r.args = o.Args 38 | return nil 39 | } 40 | 41 | func (r *itemMatcherPluginMatcherRule) check(ctx context.Context, core adapter.Core) error { 42 | p := core.GetPluginMatcher(r.tag) 43 | if p == nil { 44 | return fmt.Errorf("plugin: plugin matcher [%s] not found", r.tag) 45 | } 46 | id, err := p.LoadRunningArgs(ctx, r.args) 47 | if err != nil { 48 | return fmt.Errorf("plugin: plugin matcher [%s] load running args failed: %v", r.tag, err) 49 | } 50 | r.argsID = id 51 | r.matcher = p 52 | r.tag = "" // clean 53 | r.args = nil // clean 54 | return nil 55 | } 56 | 57 | func (r *itemMatcherPluginMatcherRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 58 | matched, err := r.matcher.Match(ctx, dnsCtx, r.argsID) 59 | if err != nil { 60 | logger.DebugfContext(ctx, "plugin: plugin matcher [%s] match failed: %v", r.matcher.Tag(), err) 61 | return false, err 62 | } 63 | logger.DebugfContext(ctx, "plugin: plugin matcher [%s] match: %t", r.matcher.Tag(), matched) 64 | return matched, nil 65 | } 66 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_qname.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "github.com/miekg/dns" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | var _ itemMatcherRule = (*itemMatcherQNameRule)(nil) 16 | 17 | type itemMatcherQNameRule struct { 18 | qName []string 19 | } 20 | 21 | func (r *itemMatcherQNameRule) UnmarshalYAML(value *yaml.Node) error { 22 | var q utils.Listable[string] 23 | err := value.Decode(&q) 24 | if err != nil { 25 | return fmt.Errorf("qname: %w", err) 26 | } 27 | if len(q) == 0 { 28 | return fmt.Errorf("qname: missing qname") 29 | } 30 | r.qName = make([]string, 0, len(q)) 31 | for _, v := range q { 32 | r.qName = append(r.qName, dns.Fqdn(v)) 33 | } 34 | return nil 35 | } 36 | 37 | func (r *itemMatcherQNameRule) check(_ context.Context, _ adapter.Core) error { 38 | return nil 39 | } 40 | 41 | func (r *itemMatcherQNameRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 42 | question := dnsCtx.ReqMsg().Question[0] 43 | qName := question.Name 44 | for _, n := range r.qName { 45 | if n == qName { 46 | logger.DebugfContext(ctx, "qname: match qname: %s", qName) 47 | return true, nil 48 | } 49 | } 50 | logger.DebugfContext(ctx, "qname: no match qname: %s", qName) 51 | return false, nil 52 | } 53 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_qtype.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | 11 | "github.com/miekg/dns" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | var _ itemMatcherRule = (*itemMatcherQTypeRule)(nil) 16 | 17 | type itemMatcherQTypeRule struct { 18 | qType []uint16 19 | } 20 | 21 | func (r *itemMatcherQTypeRule) UnmarshalYAML(value *yaml.Node) error { 22 | var q utils.Listable[any] 23 | err := value.Decode(&q) 24 | if err != nil { 25 | return fmt.Errorf("qtype: %w", err) 26 | } 27 | if len(q) == 0 { 28 | return fmt.Errorf("qtype: missing qtype") 29 | } 30 | r.qType = make([]uint16, 0, len(r.qType)) 31 | for _, v := range q { 32 | switch vv := v.(type) { 33 | case string: 34 | t, ok := dns.StringToType[vv] 35 | if !ok { 36 | return fmt.Errorf("qtype: invalid qtype: %s", vv) 37 | } 38 | r.qType = append(r.qType, t) 39 | case int: 40 | r.qType = append(r.qType, uint16(vv)) 41 | default: 42 | return fmt.Errorf("qtype: invalid qtype: %v", v) 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | func (r *itemMatcherQTypeRule) check(_ context.Context, _ adapter.Core) error { 49 | return nil 50 | } 51 | 52 | func (r *itemMatcherQTypeRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 53 | question := dnsCtx.ReqMsg().Question[0] 54 | qType := question.Qtype 55 | for _, t := range r.qType { 56 | if t == qType { 57 | logger.DebugfContext(ctx, "qtype: match qtype: %s", dns.TypeToString[t]) 58 | return true, nil 59 | } 60 | } 61 | logger.DebugfContext(ctx, "qtype: no match qtype: %s", dns.TypeToString[qType]) 62 | return false, nil 63 | } 64 | -------------------------------------------------------------------------------- /workflow/item_matcher_rule_resp_ip.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/netip" 7 | 8 | "github.com/rnetx/cdns/adapter" 9 | "github.com/rnetx/cdns/log" 10 | "github.com/rnetx/cdns/utils" 11 | 12 | "github.com/miekg/dns" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | var _ itemMatcherRule = (*itemMatcherRespIPRule)(nil) 17 | 18 | type itemMatcherRespIPRule struct { 19 | respIP []netip.Prefix 20 | } 21 | 22 | func (r *itemMatcherRespIPRule) UnmarshalYAML(value *yaml.Node) error { 23 | var rr utils.Listable[string] 24 | err := value.Decode(&rr) 25 | if err != nil { 26 | return fmt.Errorf("resp-ip: %w", err) 27 | } 28 | if len(rr) == 0 { 29 | return fmt.Errorf("resp-ip: missing resp-ip") 30 | } 31 | r.respIP = make([]netip.Prefix, 0, len(rr)) 32 | for _, s := range rr { 33 | prefix, err := netip.ParsePrefix(s) 34 | if err == nil { 35 | r.respIP = append(r.respIP, prefix) 36 | continue 37 | } 38 | ip, err := netip.ParseAddr(s) 39 | if err == nil { 40 | bits := 0 41 | if ip.Is4() { 42 | bits = 32 43 | } else { 44 | bits = 128 45 | } 46 | r.respIP = append(r.respIP, netip.PrefixFrom(ip, bits)) 47 | continue 48 | } 49 | return fmt.Errorf("resp-ip: invalid resp-ip: %s", s) 50 | } 51 | return nil 52 | } 53 | 54 | func (r *itemMatcherRespIPRule) check(_ context.Context, _ adapter.Core) error { 55 | return nil 56 | } 57 | 58 | func (r *itemMatcherRespIPRule) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 59 | answer := dnsCtx.RespMsg().Answer 60 | ips := make([]netip.Addr, 0, len(answer)) 61 | for _, rr := range answer { 62 | switch a := rr.(type) { 63 | case *dns.A: 64 | ip, ok := netip.AddrFromSlice(a.A) 65 | if ok { 66 | ips = append(ips, ip) 67 | } 68 | case *dns.AAAA: 69 | ip, ok := netip.AddrFromSlice(a.AAAA) 70 | if ok { 71 | ips = append(ips, ip) 72 | } 73 | } 74 | } 75 | if len(ips) == 0 { 76 | logger.DebugfContext(ctx, "resp-ip: no match resp-ip: no ips found") 77 | return false, nil 78 | } 79 | for _, ip := range ips { 80 | for _, p := range r.respIP { 81 | if p.Contains(ip) { 82 | logger.DebugfContext(ctx, "resp-ip: match resp-ip: %s => %s", p.String(), ip.String()) 83 | return true, nil 84 | } 85 | } 86 | } 87 | logger.DebugfContext(ctx, "resp-ip: no match resp-ip: %s", utils.Join(ips, ", ")) 88 | return false, nil 89 | } 90 | -------------------------------------------------------------------------------- /workflow/rule.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | ) 10 | 11 | type Rule interface { 12 | Check(ctx context.Context, core adapter.Core) error 13 | Exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) 14 | } 15 | 16 | type RuleOptions struct { 17 | rule Rule 18 | } 19 | 20 | type _RuleOptions struct { 21 | MatchOr any `yaml:"match-or,omitempty"` 22 | MatchAnd any `yaml:"match-and,omitempty"` 23 | ElseExec any `yaml:"else-exec,omitempty"` 24 | Exec any `yaml:"exec,omitempty"` 25 | } 26 | 27 | func (r *RuleOptions) UnmarshalYAML(unmarshal func(interface{}) error) error { 28 | var o _RuleOptions 29 | err := unmarshal(&o) 30 | if err != nil { 31 | return err 32 | } 33 | var ( 34 | isMatchOr = o.MatchOr != nil 35 | isMatchAnd = o.MatchAnd != nil 36 | isExec = o.Exec != nil 37 | isElseExec = o.ElseExec != nil 38 | ) 39 | switch { 40 | case isMatchOr && (isExec || isElseExec): 41 | r.rule = &RuleMatchOr{} 42 | case isMatchAnd && (isExec || isElseExec): 43 | r.rule = &RuleMatchAnd{} 44 | case isExec: 45 | r.rule = &RuleExec{} 46 | default: 47 | return fmt.Errorf("invalid workflow rule") 48 | } 49 | return unmarshal(r.rule) 50 | } 51 | -------------------------------------------------------------------------------- /workflow/rule_exec.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ Rule = (*RuleExec)(nil) 14 | 15 | type RuleExec struct { 16 | Execs []*RuleItemExec 17 | } 18 | 19 | type RuleExecOptions struct { 20 | Execs []yaml.Node `yaml:"exec,omitempty"` 21 | } 22 | 23 | func (r *RuleExec) UnmarshalYAML(unmarshal func(any) error) error { 24 | var o RuleExecOptions 25 | err := unmarshal(&o) 26 | if err != nil { 27 | return err 28 | } 29 | if len(o.Execs) == 0 { 30 | return fmt.Errorf("missing exec") 31 | } 32 | execs := make([]*RuleItemExec, 0, len(o.Execs)) 33 | for i, node := range o.Execs { 34 | if node.IsZero() { 35 | return fmt.Errorf("invalid exec[%d]: empty", i) 36 | } 37 | var e RuleItemExec 38 | err := node.Decode(&e) 39 | if err != nil { 40 | return fmt.Errorf("invalid exec[%d]: %w", i, err) 41 | } 42 | execs = append(execs, &e) 43 | } 44 | r.Execs = execs 45 | return nil 46 | } 47 | 48 | func (r *RuleExec) Check(ctx context.Context, core adapter.Core) error { 49 | var err error 50 | for i, e := range r.Execs { 51 | err = e.check(ctx, core) 52 | if err != nil { 53 | return fmt.Errorf("exec[%d]: %w", i, err) 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (r *RuleExec) Exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 60 | for i, e := range r.Execs { 61 | logger.DebugfContext(ctx, "run exec[%d]", i) 62 | returnMode, err := e.exec(ctx, core, logger, dnsCtx) 63 | if err != nil { 64 | logger.ErrorfContext(ctx, "run exec[%d]: run failed: %s", i, err) 65 | return adapter.ReturnModeUnknown, err 66 | } 67 | if returnMode != adapter.ReturnModeContinue { 68 | logger.DebugfContext(ctx, "run exec[%d]: %s", i, returnMode.String()) 69 | return returnMode, nil 70 | } 71 | } 72 | logger.DebugfContext(ctx, "run exec finish") 73 | return adapter.ReturnModeContinue, nil 74 | } 75 | -------------------------------------------------------------------------------- /workflow/rule_item_exec.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type itemExecutorRule interface { 14 | yaml.Unmarshaler 15 | check(ctx context.Context, core adapter.Core) error 16 | exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) 17 | } 18 | 19 | type RuleItemExec struct { 20 | rule itemExecutorRule 21 | } 22 | 23 | type RuleItemExecOptions struct { 24 | Mark yaml.Node `yaml:"mark,omitempty"` 25 | Metadata yaml.Node `yaml:"metadata,omitempty"` 26 | Plugin yaml.Node `yaml:"plugin,omitempty"` 27 | Upstream yaml.Node `yaml:"upstream,omitempty"` 28 | JumpTo yaml.Node `yaml:"jump-to,omitempty"` 29 | GoTo yaml.Node `yaml:"go-to,omitempty"` 30 | WorkflowRules yaml.Node `yaml:"workflow-rules,omitempty"` 31 | Fallback yaml.Node `yaml:"fallback,omitempty"` 32 | Parallel yaml.Node `yaml:"parallel,omitempty"` 33 | SetTTL yaml.Node `yaml:"set-ttl,omitempty"` 34 | SetRespIP yaml.Node `yaml:"set-resp-ip,omitempty"` 35 | Clean yaml.Node `yaml:"clean,omitempty"` 36 | Return yaml.Node `yaml:"return,omitempty"` 37 | } 38 | 39 | func (r *RuleItemExec) UnmarshalYAML(unmarshal func(interface{}) error) error { 40 | var o RuleItemExecOptions 41 | err := unmarshal(&o) 42 | if err != nil { 43 | // String 44 | var s string 45 | err2 := unmarshal(&s) 46 | if err2 == nil { 47 | switch s { 48 | case "clean": 49 | r.rule = &itemExecutorCleanRule{ 50 | clean: true, 51 | } 52 | case "return": 53 | r.rule = &itemExecutorReturnRule{ 54 | _return: "all", 55 | } 56 | } 57 | return nil 58 | } 59 | return err 60 | } 61 | var item itemExecutorRule 62 | switch { 63 | case !o.Mark.IsZero(): 64 | item = &itemExecutorMarkRule{} 65 | err = o.Mark.Decode(item) 66 | case !o.Metadata.IsZero(): 67 | item = &itemExecutorMetadataRule{} 68 | err = o.Metadata.Decode(item) 69 | case !o.Plugin.IsZero(): 70 | item = &itemExecutorPluginExecutorRule{} 71 | err = o.Plugin.Decode(item) 72 | case !o.Upstream.IsZero(): 73 | item = &itemExecutorUpstreamRule{} 74 | err = o.Upstream.Decode(item) 75 | case !o.JumpTo.IsZero(): 76 | item = &itemExecutorJumpToRule{} 77 | err = o.JumpTo.Decode(item) 78 | case !o.GoTo.IsZero(): 79 | item = &itemExecutorGoToRule{} 80 | err = o.GoTo.Decode(item) 81 | case !o.WorkflowRules.IsZero(): 82 | item = &itemExecutorWorkflowRulesRule{} 83 | err = o.WorkflowRules.Decode(item) 84 | case !o.Fallback.IsZero(): 85 | item = &itemExecutorFallbackRule{} 86 | err = o.Fallback.Decode(item) 87 | case !o.Parallel.IsZero(): 88 | item = &itemExecutorParallelRule{} 89 | err = o.Parallel.Decode(item) 90 | case !o.SetTTL.IsZero(): 91 | item = &itemExecutorSetTTLRule{} 92 | err = o.SetTTL.Decode(item) 93 | case !o.SetRespIP.IsZero(): 94 | item = &itemExecutorSetRespIPRule{} 95 | err = o.SetRespIP.Decode(item) 96 | case !o.Clean.IsZero(): 97 | item = &itemExecutorCleanRule{} 98 | err = o.Clean.Decode(item) 99 | case !o.Return.IsZero(): 100 | item = &itemExecutorReturnRule{} 101 | err = o.Return.Decode(item) 102 | default: 103 | return fmt.Errorf("exec rule: unknown rule") 104 | } 105 | if err != nil { 106 | return err 107 | } 108 | r.rule = item 109 | return nil 110 | } 111 | 112 | func (r *RuleItemExec) check(ctx context.Context, core adapter.Core) error { 113 | return r.rule.check(ctx, core) 114 | } 115 | 116 | func (r *RuleItemExec) exec(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 117 | return r.rule.exec(ctx, core, logger, dnsCtx) 118 | } 119 | -------------------------------------------------------------------------------- /workflow/rule_item_match.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type itemMatcherRule interface { 14 | yaml.Unmarshaler 15 | check(ctx context.Context, core adapter.Core) error 16 | match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) 17 | } 18 | 19 | type RuleItemMatch struct { 20 | rule itemMatcherRule 21 | invert bool 22 | } 23 | 24 | type RuleItemMatchOptions struct { 25 | Listener yaml.Node `yaml:"listener,omitempty"` 26 | ClientIP yaml.Node `yaml:"client-ip,omitempty"` 27 | QType yaml.Node `yaml:"qtype,omitempty"` 28 | QName yaml.Node `yaml:"qname,omitempty"` 29 | HasRespMsg yaml.Node `yaml:"has-resp-msg,omitempty"` 30 | RespIP yaml.Node `yaml:"resp-ip,omitempty"` 31 | Mark yaml.Node `yaml:"mark,omitempty"` 32 | Env yaml.Node `yaml:"env,omitempty"` 33 | Metadata yaml.Node `yaml:"metadata,omitempty"` 34 | Plugin yaml.Node `yaml:"plugin,omitempty"` 35 | MatchOr yaml.Node `yaml:"match-or,omitempty"` 36 | MatchAnd yaml.Node `yaml:"match-and,omitempty"` 37 | // 38 | Invert bool `yaml:"invert,omitempty"` 39 | } 40 | 41 | func (r *RuleItemMatch) UnmarshalYAML(unmarshal func(interface{}) error) error { 42 | var o RuleItemMatchOptions 43 | err := unmarshal(&o) 44 | if err != nil { 45 | return err 46 | } 47 | var item itemMatcherRule 48 | switch { 49 | case !o.Listener.IsZero(): 50 | item = &itemMatcherListenerRule{} 51 | err = o.Listener.Decode(item) 52 | case !o.ClientIP.IsZero(): 53 | item = &itemMatcherClientIPRule{} 54 | err = o.ClientIP.Decode(item) 55 | case !o.QType.IsZero(): 56 | item = &itemMatcherQTypeRule{} 57 | err = o.QType.Decode(item) 58 | case !o.QName.IsZero(): 59 | item = &itemMatcherQNameRule{} 60 | err = o.QName.Decode(item) 61 | case !o.HasRespMsg.IsZero(): 62 | item = &itemMatcherHasRespMsgRule{} 63 | err = o.HasRespMsg.Decode(item) 64 | case !o.RespIP.IsZero(): 65 | item = &itemMatcherRespIPRule{} 66 | err = o.RespIP.Decode(item) 67 | case !o.Mark.IsZero(): 68 | item = &itemMatcherMarkRule{} 69 | err = o.Mark.Decode(item) 70 | case !o.Env.IsZero(): 71 | item = &itemMatcherEnvRule{} 72 | err = o.Env.Decode(item) 73 | case !o.Metadata.IsZero(): 74 | item = &itemMatcherMetadataRule{} 75 | err = o.Metadata.Decode(item) 76 | case !o.Plugin.IsZero(): 77 | item = &itemMatcherPluginMatcherRule{} 78 | err = o.Plugin.Decode(item) 79 | case !o.MatchOr.IsZero(): 80 | item = &itemMatcherMatchOrRule{} 81 | err = o.MatchOr.Decode(item) 82 | case !o.MatchAnd.IsZero(): 83 | item = &itemMatcherMatchAndRule{} 84 | err = o.MatchAnd.Decode(item) 85 | default: 86 | return fmt.Errorf("match rule: unknown rule") 87 | } 88 | if err != nil { 89 | return err 90 | } 91 | r.rule = item 92 | r.invert = o.Invert 93 | return nil 94 | } 95 | 96 | func (r *RuleItemMatch) check(ctx context.Context, core adapter.Core) error { 97 | return r.rule.check(ctx, core) 98 | } 99 | 100 | func (r *RuleItemMatch) match(ctx context.Context, core adapter.Core, logger log.Logger, dnsCtx *adapter.DNSContext) (bool, error) { 101 | matched, err := r.rule.match(ctx, core, logger, dnsCtx) 102 | if err != nil { 103 | return matched, err 104 | } 105 | if r.invert { 106 | logger.DebugfContext(ctx, "invert match: %t => %t", matched, !matched) 107 | return !matched, nil 108 | } 109 | return matched, nil 110 | } 111 | -------------------------------------------------------------------------------- /workflow/workflow.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rnetx/cdns/adapter" 8 | "github.com/rnetx/cdns/log" 9 | "github.com/rnetx/cdns/utils" 10 | ) 11 | 12 | type WorkflowOptions struct { 13 | Tag string `yaml:"tag"` 14 | Rules utils.Listable[RuleOptions] `yaml:"rules"` 15 | } 16 | 17 | type Workflow struct { 18 | ctx context.Context 19 | tag string 20 | core adapter.Core 21 | logger log.Logger 22 | 23 | rules []Rule 24 | } 25 | 26 | func NewWorkflow(ctx context.Context, core adapter.Core, logger log.Logger, tag string, options WorkflowOptions) (adapter.Workflow, error) { 27 | w := &Workflow{ 28 | ctx: ctx, 29 | tag: tag, 30 | core: core, 31 | logger: logger, 32 | } 33 | if len(options.Rules) == 0 { 34 | return nil, fmt.Errorf("missing rules") 35 | } 36 | w.rules = make([]Rule, 0, len(options.Rules)) 37 | for _, o := range options.Rules { 38 | w.rules = append(w.rules, o.rule) 39 | } 40 | return w, nil 41 | } 42 | 43 | func (w *Workflow) Tag() string { 44 | return w.tag 45 | } 46 | 47 | func (w *Workflow) Check() error { 48 | var err error 49 | for i, rule := range w.rules { 50 | err = rule.Check(w.ctx, w.core) 51 | if err != nil { 52 | return fmt.Errorf("rule [%d] check failed: %v", i, err) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func (w *Workflow) Exec(ctx context.Context, dnsCtx *adapter.DNSContext) (adapter.ReturnMode, error) { 59 | for i, rule := range w.rules { 60 | w.logger.DebugfContext(ctx, "rule[%d] exec", i) 61 | returnMode, err := rule.Exec(ctx, w.core, w.logger, dnsCtx) 62 | if err != nil { 63 | w.logger.ErrorfContext(ctx, "rule[%d] exec failed: %v", i, err) 64 | return adapter.ReturnModeUnknown, err 65 | } 66 | switch returnMode { 67 | case adapter.ReturnModeReturnAll: 68 | w.logger.DebugfContext(ctx, "rule[%d]: %s", i, returnMode.String()) 69 | return returnMode, nil 70 | case adapter.ReturnModeReturnOnce: 71 | w.logger.DebugfContext(ctx, "rule[%d]: %s", i, returnMode.String()) 72 | return adapter.ReturnModeContinue, nil 73 | } 74 | } 75 | return adapter.ReturnModeContinue, nil 76 | } 77 | --------------------------------------------------------------------------------