├── .github
└── workflows
│ └── go.yml
├── .gitignore
├── Dockerfile
├── GeoLite2-Country.mmdb
├── LICENSE
├── README.md
├── go.mod
├── main.go
└── pack.sh
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 |
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: 1.19
23 |
24 | - name: Build
25 | run: |
26 | go mod tidy
27 | go build -v ./...
28 |
29 | - name: Test
30 | run: go test -v ./...
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang AS build-env
2 |
3 | RUN GO111MODULE=off go get -u github.com/esrrhs/yellowdns
4 | RUN GO111MODULE=off go get -u github.com/esrrhs/yellowdns/...
5 | RUN GO111MODULE=off go install github.com/esrrhs/yellowdns
6 |
7 | FROM debian
8 | COPY --from=build-env /go/bin/yellowdns .
9 | COPY GeoLite2-Country.mmdb .
10 | WORKDIR ./
11 |
--------------------------------------------------------------------------------
/GeoLite2-Country.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/yellowdns/3979096c3ceccded6033b0bfd42aa8d9487b4926/GeoLite2-Country.mmdb
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 zhao xin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # yellowdns
2 |
3 | [
](https://github.com/esrrhs/yellowdns)
4 | [
](https://github.com/esrrhs/yellowdns)
5 | [](https://goreportcard.com/report/github.com/esrrhs/yellowdns)
6 | [
](https://github.com/esrrhs/yellowdns/releases)
7 | [
](https://github.com/esrrhs/yellowdns/releases)
8 | [
](https://hub.docker.com/repository/docker/esrrhs/yellowdns)
9 | [
](https://github.com/esrrhs/yellowdns/actions)
10 |
11 | 简单的dns proxy,根据地域转发到不同的dns server,解决访问境外dns的问题
12 |
13 | # 使用
14 | 直接启动
15 | ```
16 | ./yellowdns
17 | ```
18 | 等价于
19 | ```
20 | ./yellowdns -l :53 -los 114.114.114.114:53 -exs 8.8.8.8:53 -lor CN -lof GeoLite2-Country.mmdb
21 | ```
22 | 或者使用docker
23 | ```
24 | docker run --name yellowdns -d --net=host --restart=always -p 55353:55353/udp esrrhs/yellowdns ./yellowdns -l :55353 -exs 127.0.0.1:55354
25 | ```
26 | 如果提示53端口被占用,看看是不是其他网卡被占了,那么修改成127.0.0.1:53即可
27 |
28 | # 参数说明
29 | * -l:监听的udp地址,默认53
30 |
31 | * -los: 境内的dns server,默认114.114.114.114:53,域名解析时,先走境内dns server,发现如果是境外ip,则再重新走境外的dns server
32 |
33 | * -exs:境外的dns server,默认8.8.8.8:53,境外的ip都用这个dns server做解析
34 |
35 | * -lor: 境内的定义,默认CN
36 |
37 | * -lof: ip查询国家的数据库文件
38 |
39 | * 其他的选项,参考-h
40 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/esrrhs/yellowdns
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/esrrhs/gohome v0.0.0-20230318083541-913b3bf95271
7 | github.com/miekg/dns v1.1.52
8 | )
9 |
10 | require (
11 | github.com/OneOfOne/xxhash v1.2.8 // indirect
12 | github.com/google/uuid v1.3.0 // indirect
13 | github.com/oschwald/geoip2-golang v1.8.0 // indirect
14 | github.com/oschwald/maxminddb-golang v1.10.0 // indirect
15 | golang.org/x/mod v0.9.0 // indirect
16 | golang.org/x/net v0.23.0 // indirect
17 | golang.org/x/sys v0.18.0 // indirect
18 | golang.org/x/tools v0.7.0 // indirect
19 | google.golang.org/protobuf v1.33.0 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "github.com/esrrhs/gohome/common"
6 | "github.com/esrrhs/gohome/geoip"
7 | "github.com/esrrhs/gohome/loggo"
8 | "github.com/miekg/dns"
9 | "net"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type dnscache struct {
15 | host string
16 | ip string
17 | externip string
18 | extern bool
19 | fromextern bool
20 | time time.Time
21 | }
22 |
23 | type dnscachestatus struct {
24 | LocalDNS int
25 | Local_local int
26 | Local_extern int
27 | ExternDNS int
28 | Extern_same int
29 | Extern_diff int
30 | }
31 |
32 | type dnsserverstatus struct {
33 | Reqnum int
34 | ResNum int
35 | Packerror int
36 | Anum int
37 | ARetnum int
38 | ACachenum int
39 | Localnum int
40 | Externnum int
41 | ExternFailnum int
42 | LocalFailnum int
43 | LocalRetnum int
44 | ExternRetnum int
45 | }
46 |
47 | type dnsserver struct {
48 | listener *net.UDPConn
49 | localregion string
50 | timeout int
51 | expire int
52 | status dnsserverstatus
53 |
54 | cache sync.Map
55 | localsereraddr *net.UDPAddr
56 | externalserveraddr *net.UDPAddr
57 | }
58 |
59 | var gds dnsserver
60 |
61 | func main() {
62 | defer common.CrashLog()
63 |
64 | listen := flag.String("l", ":53", "listen addr")
65 | localserer := flag.String("los", "114.114.114.114:53", "local dns server")
66 | externalserver := flag.String("exs", "8.8.8.8:53", "external dns server")
67 | localregion := flag.String("lor", "CN", "local region")
68 | localregionfile := flag.String("lof", "GeoLite2-Country.mmdb", "local region file")
69 | timeout := flag.Int("timeout", 5000, "wait response timeout in ms")
70 | expire := flag.Int("expire", 24, "host region cache expire time in hour")
71 | nolog := flag.Int("nolog", 0, "write log file")
72 | noprint := flag.Int("noprint", 0, "print stdout")
73 | loglevel := flag.String("loglevel", "info", "log level")
74 |
75 | flag.Parse()
76 |
77 | if *listen == "" || *localserer == "" ||
78 | *externalserver == "" || *localregion == "" ||
79 | *localregionfile == "" {
80 | flag.Usage()
81 | return
82 | }
83 |
84 | level := loggo.LEVEL_INFO
85 | if loggo.NameToLevel(*loglevel) >= 0 {
86 | level = loggo.NameToLevel(*loglevel)
87 | }
88 | loggo.Ini(loggo.Config{
89 | Level: level,
90 | Prefix: "yellowdns",
91 | MaxDay: 3,
92 | NoLogFile: *nolog > 0,
93 | NoPrint: *noprint > 0,
94 | })
95 | loggo.Info("start...")
96 |
97 | gds.timeout = *timeout
98 | gds.expire = *expire
99 |
100 | listenaddr, err := net.ResolveUDPAddr("udp", *listen)
101 | if err != nil {
102 | loggo.Error("listen addr fail %v", err)
103 | return
104 | }
105 | loggo.Info("listen addr %v", listenaddr)
106 |
107 | listener, err := net.ListenUDP("udp", listenaddr)
108 | if err != nil {
109 | loggo.Error("listening fail %v", err)
110 | return
111 | }
112 | gds.listener = listener
113 | loggo.Info("listen ok %v", listener.LocalAddr())
114 |
115 | localsereraddr, err := net.ResolveUDPAddr("udp", *localserer)
116 | if err != nil {
117 | loggo.Error("local dns server fail %v", err)
118 | return
119 | }
120 | gds.localsereraddr = localsereraddr
121 | loggo.Info("local dns server is %v", localsereraddr)
122 |
123 | externalserveraddr, err := net.ResolveUDPAddr("udp", *externalserver)
124 | if err != nil {
125 | loggo.Error("external dns server fail %v", err)
126 | return
127 | }
128 | gds.externalserveraddr = externalserveraddr
129 | loggo.Info("external dns server is %v", externalserveraddr)
130 |
131 | err = geoip.Load(*localregionfile)
132 | if err != nil {
133 | loggo.Error("load local region ip file ERROR: %v", err)
134 | return
135 | }
136 |
137 | gds.localregion = *localregion
138 |
139 | go updateCache()
140 |
141 | for {
142 | bytes := make([]byte, 4096)
143 |
144 | loggo.Info("wait for udp in")
145 | n, srcaddr, err := gds.listener.ReadFromUDP(bytes)
146 | if err != nil {
147 | continue
148 | }
149 | if n <= 0 {
150 | continue
151 | }
152 |
153 | loggo.Info("recv udp %v from %v", n, srcaddr)
154 |
155 | gds.status.Reqnum++
156 |
157 | go forward(srcaddr, bytes[0:n])
158 | }
159 | }
160 |
161 | func updateCache() {
162 | defer common.CrashLog()
163 |
164 | for {
165 | dcs := dnscachestatus{}
166 |
167 | tmpdelete := make([]string, 0)
168 |
169 | gds.cache.Range(func(key, value interface{}) bool {
170 | host := key.(string)
171 | dc := value.(*dnscache)
172 |
173 | if dc.fromextern {
174 | dcs.ExternDNS++
175 | if dc.externip != dc.ip {
176 | dcs.Extern_diff++
177 | } else {
178 | dcs.Extern_same++
179 | }
180 | } else {
181 | if dc.extern {
182 | dcs.Local_extern++
183 | } else {
184 | dcs.Local_local++
185 | }
186 | dcs.LocalDNS++
187 | }
188 |
189 | if time.Now().Sub(dc.time) > time.Hour*time.Duration(gds.expire) {
190 | tmpdelete = append(tmpdelete, host)
191 | }
192 |
193 | return true
194 | })
195 |
196 | loggo.Warn("\n%s%s", common.StructToTable(&dcs),
197 | common.StructToTable(&(gds.status)))
198 |
199 | for _, host := range tmpdelete {
200 | gds.cache.Delete(host)
201 | loggo.Warn("delete expire cache %s", host)
202 | }
203 |
204 | gds.status = dnsserverstatus{}
205 |
206 | time.Sleep(time.Minute)
207 | }
208 |
209 | }
210 |
211 | func forward(srcaddr *net.UDPAddr, srcreq []byte) {
212 | defer common.CrashLog()
213 |
214 | msg := dns.Msg{}
215 | err := msg.Unpack(srcreq)
216 | if err != nil {
217 | gds.status.Packerror++
218 | loggo.Error("dns Msg Unpack fail %v", err)
219 | return
220 | }
221 | loggo.Info("dns Msg: \n%v", msg.String())
222 |
223 | extern := false
224 | for _, q := range msg.Question {
225 | if q.Qtype == dns.TypeA {
226 | gds.status.Anum++
227 | v, ok := gds.cache.Load(q.Name)
228 | if !ok {
229 | continue
230 | }
231 | gds.status.ACachenum++
232 | dc := v.(*dnscache)
233 | if dc.extern {
234 | extern = true
235 | }
236 | }
237 | }
238 |
239 | if extern {
240 | go forwardextern(srcaddr, srcreq)
241 | } else {
242 | go forwardlocal(srcaddr, srcreq)
243 | }
244 | }
245 |
246 | func forwardlocal(srcaddr *net.UDPAddr, srcreq []byte) {
247 | defer common.CrashLog()
248 |
249 | gds.status.Localnum++
250 |
251 | loggo.Info("forward local start %v %v", srcaddr, gds.localsereraddr)
252 | c, err := net.DialUDP("udp", nil, gds.localsereraddr)
253 | if err != nil {
254 | gds.status.LocalFailnum++
255 | loggo.Error("DialUDP local fail %v", err)
256 | return
257 | }
258 | loggo.Info("forward local dail ok %v %v", srcaddr, gds.localsereraddr)
259 |
260 | _, err = c.Write(srcreq)
261 | if err != nil {
262 | gds.status.LocalFailnum++
263 | loggo.Error("Write local fail %v", err)
264 | return
265 | }
266 | loggo.Info("forward local write ok, wait ret %v %v", srcaddr, gds.localsereraddr)
267 |
268 | bytes := make([]byte, 4096)
269 | c.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(gds.timeout)))
270 | n, err := c.Read(bytes)
271 | if err != nil {
272 | gds.status.LocalFailnum++
273 | loggo.Info("ReadFromUDP local fail %v", err)
274 | return
275 | }
276 |
277 | loggo.Info("forward local ret %v %v", srcaddr, gds.externalserveraddr)
278 |
279 | gds.status.LocalRetnum++
280 |
281 | go processret(false, srcaddr, srcreq, bytes[0:n])
282 | }
283 |
284 | func forwardextern(srcaddr *net.UDPAddr, srcreq []byte) {
285 | defer common.CrashLog()
286 |
287 | gds.status.Externnum++
288 |
289 | loggo.Info("forward extern start %v %v", srcaddr, gds.externalserveraddr)
290 | c, err := net.DialUDP("udp", nil, gds.externalserveraddr)
291 | if err != nil {
292 | gds.status.ExternFailnum++
293 | loggo.Error("DialUDP extern fail %v", err)
294 | return
295 | }
296 | loggo.Info("forward extern dail ok %v %v", srcaddr, gds.externalserveraddr)
297 |
298 | _, err = c.Write(srcreq)
299 | if err != nil {
300 | gds.status.ExternFailnum++
301 | loggo.Error("Write extern fail %v", err)
302 | return
303 | }
304 | loggo.Info("forward extern write ok, wait ret %v %v", srcaddr, gds.externalserveraddr)
305 |
306 | bytes := make([]byte, 4096)
307 | c.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(gds.timeout)))
308 | n, err := c.Read(bytes)
309 | if err != nil {
310 | gds.status.ExternFailnum++
311 | loggo.Info("ReadFromUDP extern fail %v", err)
312 | return
313 | }
314 |
315 | loggo.Info("forward extern ret %v %v", srcaddr, gds.externalserveraddr)
316 |
317 | gds.status.ExternRetnum++
318 |
319 | go processret(true, srcaddr, srcreq, bytes[0:n])
320 | }
321 |
322 | func processret(extern bool, srcaddr *net.UDPAddr, srcreq []byte, retdata []byte) {
323 | defer common.CrashLog()
324 |
325 | name := ""
326 | if extern {
327 | name = "extern"
328 | } else {
329 | name = "local"
330 | }
331 |
332 | loggo.Info("%v %v process ret start", name, srcaddr)
333 |
334 | msg := dns.Msg{}
335 | err := msg.Unpack(retdata)
336 | if err != nil {
337 | loggo.Error("%v %v Msg Unpack fail %v", name, srcaddr, err)
338 | return
339 | }
340 | loggo.Info("%v %v return dns Msg: \n%v", name, srcaddr, msg.String())
341 |
342 | hasextern := false
343 | if msg.Rcode == dns.RcodeSuccess {
344 | for _, a := range msg.Answer {
345 | if a.Header().Rrtype == dns.TypeA {
346 | gds.status.ARetnum++
347 | aa := a.(*dns.A)
348 | ip := aa.A.String()
349 | host := aa.Hdr.Name
350 |
351 | v, _ := gds.cache.LoadOrStore(host, &dnscache{})
352 | dc := v.(*dnscache)
353 | dc.host = host
354 | if extern {
355 | dc.externip = ip
356 | } else {
357 | dc.ip = ip
358 | }
359 | dc.time = time.Now()
360 | dc.fromextern = extern
361 |
362 | region, _ := geoip.GetCountryIsoCode(ip)
363 | if len(region) <= 0 {
364 | dc.extern = false
365 | } else if gds.localregion == region {
366 | dc.extern = false
367 | } else {
368 | dc.extern = true
369 | hasextern = true
370 | }
371 |
372 | if dc.extern {
373 | loggo.Info("%v %v save extern dns cache: %v %v", name, srcaddr, host, ip)
374 | } else {
375 | loggo.Info("%v %v save local dns cache: %v %v", name, srcaddr, host, ip)
376 | }
377 | }
378 | }
379 | }
380 |
381 | if !extern && hasextern {
382 | loggo.Info("%v %v retry forward extern", name, srcaddr)
383 | go forwardextern(srcaddr, srcreq)
384 | return
385 | }
386 |
387 | _, err = gds.listener.WriteToUDP(retdata, srcaddr)
388 | if err != nil {
389 | loggo.Error("%v %v WriteToUDP fail %v", name, srcaddr, err)
390 | return
391 | }
392 |
393 | loggo.Info("%v %v process ret ok", name, srcaddr)
394 |
395 | gds.status.ResNum++
396 | }
397 |
--------------------------------------------------------------------------------
/pack.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | #set -x
3 | NAME="yellowdns"
4 |
5 | export GO111MODULE=on
6 |
7 | #go tool dist list
8 | build_list=$(go tool dist list)
9 |
10 | rm pack -rf
11 | rm pack.zip -f
12 | mkdir pack
13 |
14 | for line in $build_list; do
15 | os=$(echo "$line" | awk -F"/" '{print $1}')
16 | arch=$(echo "$line" | awk -F"/" '{print $2}')
17 | echo "os="$os" arch="$arch" start build"
18 | if [ $os == "android" ]; then
19 | continue
20 | fi
21 | if [ $os == "ios" ]; then
22 | continue
23 | fi
24 | if [ $arch == "wasm" ]; then
25 | continue
26 | fi
27 | CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags="-s -w"
28 | if [ $? -ne 0 ]; then
29 | echo "os="$os" arch="$arch" build fail"
30 | exit 1
31 | fi
32 | if [ $os = "windows" ]; then
33 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME".exe"
34 | if [ $? -ne 0 ]; then
35 | echo "os="$os" arch="$arch" zip fail"
36 | exit 1
37 | fi
38 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/
39 | rm $NAME".exe" -f
40 | else
41 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME
42 | if [ $? -ne 0 ]; then
43 | echo "os="$os" arch="$arch" zip fail"
44 | exit 1
45 | fi
46 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/
47 | rm $NAME -f
48 | fi
49 | echo "os="$os" arch="$arch" done build"
50 | done
51 |
52 | zip pack.zip pack/ -r
53 |
54 | echo "all done"
55 |
--------------------------------------------------------------------------------