├── .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 | [![Go Report Card](https://goreportcard.com/badge/github.com/esrrhs/yellowdns)](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 | --------------------------------------------------------------------------------