From 818e3dc466bc0f2e9b89ae1759de1cd053f3d6b9 Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Sun, 1 Mar 2026 17:35:34 +0300 Subject: [PATCH 1/6] update opennicZones --- proxy/dns.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proxy/dns.go b/proxy/dns.go index c27115c3..fb6429cb 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -21,6 +21,7 @@ var ( "chan", "cyb", "dyn", + "epic", "geek", "gopher", "indy", @@ -43,6 +44,8 @@ var ( "te", "ti", "uu", + "ko", + "rm", } commonResolver = &DoH{} From e5473f21c0af2b39726570325d29e9ccf0bc32f8 Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Thu, 5 Mar 2026 01:35:45 +0300 Subject: [PATCH 2/6] WIP 1 --- AGENTS.md | 43 +++++++ config/config.go | 23 ++-- go.mod | 23 ++-- go.sum | 43 +++---- proxy/client.go | 1 + proxy/dns.go | 265 ++++++++++++++++++++++++++++++++------ proxy/resolver.go | 321 +++++++++++++++++++++++++++------------------- 7 files changed, 494 insertions(+), 225 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..821802a3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Repository Guidelines + +## Project Structure & Module Organization +`main.go` is the daemon entrypoint. Core Go packages live at the repository root: +- `api/` HTTP and route handlers. +- `bittorrent/` torrent session, player, and filesystem integration. +- `providers/` provider search and payload handling. +- `xbmc/` Kodi/XBMC integration types and RPC bindings. +- `util/` shared helpers (time, files, tracing, networking). + +Build outputs are written to `build/_/`. Operational fixtures and manual test assets are in `_test/`. Supporting runtime pieces are in `config/`, `cache/`, and `database/`. + +## Build, Test, and Development Commands +- `make pull PLATFORM=linux-x64` downloads a single cross-compiler image. +- `make pull-all` pulls images for all supported platforms. +- `make linux-x64` builds the Linux x64 binary into `build/linux_x64/`. +- `make linux-x64-shared` builds the shared-library variant. +- `make` builds all platform targets defined in `Makefile`. +- `make zip PLATFORM=linux-x64` packages a platform artifact zip. +- `./test_build.sh local restart-local` builds locally and copies binaries into local Kodi addon paths. +- `/bin/bash test_build.sh sanitize` builds with Address/Leak Sanitizers for memory debugging. + +## Coding Style & Naming Conventions +This is a Go codebase; follow standard Go formatting and naming. +- Run `gofmt -w` on changed files before review. +- Use lowercase package names and `CamelCase` for exported identifiers. +- Keep filenames descriptive and consistent with existing patterns (for example, `torrent_file.go`, `xbmcgui.go`). +- Keep platform-specific code split by suffix files (for example, `*_all.go`, `*_arm.go`). + +## Testing Guidelines +Automated unit tests are currently sparse (no committed `*_test.go` files yet). For new logic, prefer table-driven tests next to the changed package and run `go test ./...`. For integration-sensitive changes, run at least one target build (for example, `make linux-x64`) and include a local smoke check using `test_build.sh`. + +## Commit & Pull Request Guidelines +Recent history favors short, imperative subjects (for example, `Fix ...`, `Update ...`, `Add ...`) and issue references like `(#172)`. Keep commits focused and avoid leaving `WIP` commits in the final PR. + +PRs should include: +- What changed and why. +- Affected platforms (`linux-x64`, `windows-x86`, etc.). +- Verification steps/commands you ran. +- Linked issue(s) and logs/screenshots when behavior changes are user-visible. + +## Security & Configuration Tips +Do not commit secrets (for example, `GH_TOKEN`, FTP credentials, local paths). Use environment variables for local build/release configuration and keep machine-specific values out of tracked files. diff --git a/config/config.go b/config/config.go index 89282924..ed1b4456 100644 --- a/config/config.go +++ b/config/config.go @@ -239,10 +239,12 @@ type Configuration struct { CustomProviderTimeout int ProviderUseLowestReleaseDate bool - InternalDNSEnabled bool - InternalDNSSkipIPv6 bool - InternalDNSOrder int - InternalDNSOpenNic []string + InternalDNSEnabled bool + InternalDNSSkipIPv6 bool + InternalDNSServer string + InternalDNSServerCustom string + InternalDNSOpenNicUse bool + InternalDNSOpenNicOnlySpecialZones bool InternalProxyEnabled bool InternalProxyLogging bool @@ -782,8 +784,10 @@ func Reload() (ret *Configuration, err error) { CustomProviderTimeout: settings.ToInt("custom_provider_timeout"), ProviderUseLowestReleaseDate: settings.ToBool("provider_use_lowest_release_date"), - InternalDNSEnabled: settings.ToBool("internal_dns_enabled"), - InternalDNSSkipIPv6: settings.ToBool("internal_dns_skip_ipv6"), + InternalDNSEnabled: settings.ToBool("internal_dns_enabled"), + InternalDNSSkipIPv6: settings.ToBool("internal_dns_skip_ipv6"), + InternalDNSOpenNicUse: settings.ToBool("internal_dns_opennic_use"), + InternalDNSOpenNicOnlySpecialZones: settings.ToBool("internal_dns_opennic_only_special_zones"), InternalProxyEnabled: settings.ToBool("internal_proxy_enabled"), InternalProxyLogging: settings.ToBool("internal_proxy_logging"), @@ -830,8 +834,11 @@ func Reload() (ret *Configuration, err error) { newConfig.OutgoingInterfaces = strings.Join(Args.OutgoingInterfaces, ",") } - reDNS := regexp.MustCompile(`\s*,\s*`) - newConfig.InternalDNSOpenNic = reDNS.Split(settings.ToString("internal_dns_opennic"), -1) + newConfig.InternalDNSServer = settings.ToString("internal_dns_server") + newConfig.InternalDNSServerCustom = settings.ToString("internal_dns_server_custom") + if newConfig.InternalDNSServer == "custom" { + newConfig.InternalDNSServer = newConfig.InternalDNSServerCustom + } updateLoggingLevel(newConfig.LogLevel) diff --git a/go.mod b/go.mod index 34334a00..bd708457 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/elgatito/elementum -go 1.21 - -toolchain go1.21.1 +go 1.25.0 require ( + codeberg.org/miekg/dns v0.6.64 github.com/ElementumOrg/go-nat-pmp v0.0.0-20250224152958-33a79384df6a github.com/ElementumOrg/libtorrent-go v0.0.0-20230915150218-d8763f5e1783 github.com/RoaringBitmap/roaring v1.9.4 @@ -13,7 +12,6 @@ require ( github.com/anacrolix/sync v0.5.1 github.com/anacrolix/tagflag v1.4.0 github.com/asdine/storm v2.1.2+incompatible - github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6 github.com/c-robinson/iplib/v2 v2.0.5 github.com/cespare/xxhash v1.1.0 github.com/dustin/go-humanize v1.0.1 @@ -26,7 +24,6 @@ require ( github.com/jmcvetta/napping v3.2.0+incompatible github.com/karrick/godirwalk v1.17.0 github.com/klauspost/compress v1.17.4 - github.com/likexian/doh v0.7.1 github.com/likexian/gokit v0.25.15 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 @@ -62,12 +59,11 @@ require ( github.com/huandu/xstrings v1.3.2 // indirect github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.63 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect @@ -85,16 +81,13 @@ require ( github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.5 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect lukechampine.com/uint128 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 4950662b..56938dae 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +codeberg.org/miekg/dns v0.6.64 h1:xzvVDL4qhFvU+scxZ2TlV0zhPlCYIps8sUncz2RR7ok= +codeberg.org/miekg/dns v0.6.64/go.mod h1:58Y3ZTg6Z5ZEm/ZAAwHehbZfrD4u5mE4RByHoPEMyKk= crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -61,8 +63,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6 h1:oV1V+uwP+sjmdSkvMxsl/l+HE+N8wbL49wCXZPel25M= -github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6/go.mod h1:txOV61Nn+21z77KUMkNsp8lTHoOFTtqotltQAFenS9I= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= @@ -201,8 +201,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -215,8 +215,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/likexian/doh v0.7.1 h1:0p75orMxXrfGNAOnOEtl2xZ5T9Ctqi++i/WhN6P6hs8= -github.com/likexian/doh v0.7.1/go.mod h1:aIEOK197o6nC82iRo5+5L5ws+X7xRCmuer3M4In91WA= github.com/likexian/gokit v0.25.15 h1:QjospM1eXhdMMHwZRpMKKAHY/Wig9wgcREmLtf9NslY= github.com/likexian/gokit v0.25.15/go.mod h1:S2QisdsxLEHWeD/XI0QMVeggp+jbxYqUxMvSBil7MRg= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -227,8 +225,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -347,8 +343,6 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA= github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -357,15 +351,13 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -378,8 +370,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -387,8 +379,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -404,25 +396,22 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -443,8 +432,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/proxy/client.go b/proxy/client.go index 8897c2bb..0808db71 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -84,6 +84,7 @@ func CustomDialContext(ctx context.Context, network, addr string) (net.Conn, err if len(addrs) == 2 && len(addrs[0]) > 2 && strings.Contains(addrs[0], ".") { if ipTest := net.ParseIP(addrs[0]); ipTest == nil { if ips, err := resolveAddr(addrs[0]); err == nil && len(ips) > 0 { + log.Debugf("Resolved DNS %s to %+v", addrs[0], ips) //TODO: remove for _, i := range ips { if config.Get().InternalDNSSkipIPv6 { if ip := net.ParseIP(i); ip == nil || ip.To4() == nil { diff --git a/proxy/dns.go b/proxy/dns.go index fb6429cb..496c8e1f 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -2,12 +2,15 @@ package proxy import ( "context" + "fmt" + "io" + "net" + "net/http" "strings" + "time" "github.com/anacrolix/missinggo/perf" "github.com/anacrolix/sync" - "github.com/bogdanovich/dns_resolver" - "github.com/likexian/doh/dns" "github.com/likexian/gokit/xcache" "github.com/elgatito/elementum/config" @@ -48,8 +51,14 @@ var ( "rm", } - commonResolver = &DoH{} - opennicResolver = &dns_resolver.DnsResolver{} + defaultOpenNICResolverServers = []string{ + "94.247.43.254", + "152.53.15.127", + "185.181.61.24", + } + + commonResolver = NewDoHResolver(defaultDoHServer) + opennicResolver = NewOpenNICResolver(defaultOpenNICResolverServers) commonLock = sync.RWMutex{} opennicLock = sync.RWMutex{} @@ -58,12 +67,18 @@ var ( dnsRunLocks sync.Map ) -// Supported dns response types -var ( - ResponseTypeA = 1 - ResponseTypeAAAA = 28 +const ( + openNICResolversAPI = "https://api.opennicproject.org/geoip/?bare&res=3&adm=3&rnd=true&ipv=4" + openNICRequestTimeout = 7 * time.Second ) +type resolveResult struct { + source string + ips []string + ttl int + err error +} + func init() { reloadDNS() } @@ -77,14 +92,19 @@ func reloadDNS() { opennicLock.Unlock() }() - commonResolver = UseProviders(GoogleProvider, CloudflareProvider, Quad9Provider) - opennicResolver = dns_resolver.New(config.Get().InternalDNSOpenNic) + commonResolver = NewDoHResolver(config.Get().InternalDNSServer) + + if config.Get().InternalDNSOpenNicUse { + opennicResolvers, source := fetchOpenNICResolvers() + opennicResolver = NewOpenNICResolver(opennicResolvers) + log.Debugf("Configured OpenNIC resolvers from %s: %+v", source, opennicResolvers) + } dnsCacheResults.Flush() } // Each request is going through this workflow: -// Check cache -> Query Opennic (if address belongs to Opennic domains) -> Query DoH providers -> Save cache +// Check cache -> Query DoH & Query Opennic (if enabled for everything or if address belongs to Opennic domains) -> Save cache func resolveAddr(addr string) (ret []string, err error) { defer perf.ScopeTimer()() @@ -103,41 +123,94 @@ func resolveAddr(addr string) (ret []string, err error) { } mu.Lock() + defer mu.Unlock() - cacheTTL := 300 + if cached := dnsCacheResults.Get(addr); cached != nil { + return cached.([]string), nil + } - defer func() { - if len(ret) > 0 { - dnsCacheResults.Set(addr, ret, int64(cacheTTL)) + zone := getZone(addr) + isOpenNICZone := isOpennicDomain(zone) + + runDoH := !isOpenNICZone + runOpenNIC := config.Get().InternalDNSOpenNicUse && (isOpenNICZone || !config.Get().InternalDNSOpenNicOnlySpecialZones) + + results := make(chan resolveResult, 2) + queriesCount := 0 + + if runDoH { + queriesCount++ + go queryCommonResolver(addr, results) + } + + if runOpenNIC { + queriesCount++ + go queryOpenNICResolver(addr, results) + } + + if queriesCount == 0 { + return nil, fmt.Errorf("dns: no resolver selected for host %s", addr) + } + + cacheTTL := 0 + seenIPs := make(map[string]struct{}) + var firstErr error + + for i := 0; i < queriesCount; i++ { + result := <-results + if result.err != nil { + if firstErr == nil { + firstErr = result.err + } + + log.Debugf("DNS resolver %s failed for %s: %s", result.source, addr, result.err) + continue } - mu.Unlock() - }() + log.Debugf("DNS resolver %s returned %v IPs for %s", result.source, result.ips, addr) //TODO: remove - // Resolve Opennic address - if isOpennicDomain(getZone(addr)) { - if ips := resolveOpennicAddr(addr); len(ips) > 0 { - ret = ips - return + ips := filterResolvedIPs(result.ips, addr) + if len(ips) == 0 { + continue } - } - commonLock.RLock() - defer commonLock.RUnlock() + for _, ip := range ips { + if _, exists := seenIPs[ip]; exists { + continue + } + + seenIPs[ip] = struct{}{} + ret = append(ret, ip) + } - // Resolve with common resolver using DoH - if resp, err := commonResolver.Query(context.TODO(), dns.Domain(addr), dns.TypeA); err == nil { - ret = IPs(resp) - cacheTTL = TTL(resp) + if result.ttl > 0 && (cacheTTL == 0 || result.ttl < cacheTTL) { + cacheTTL = result.ttl + } + } - return ret, err - } else { - return nil, err + if len(ret) == 0 { + if firstErr == nil { + firstErr = fmt.Errorf("dns: all resolvers failed for %s", addr) + } + + return nil, firstErr + } + if cacheTTL <= 0 { + cacheTTL = defaultDNSCacheTTL } + + dnsCacheResults.Set(addr, ret, int64(cacheTTL)) + + return ret, nil } func getZone(addr string) string { + addr = strings.TrimSpace(strings.TrimSuffix(addr, ".")) + if addr == "" { + return "" + } + ary := strings.Split(addr, ".") - return ary[len(ary)-1] + return strings.ToLower(ary[len(ary)-1]) } func isOpennicDomain(zone string) bool { @@ -150,20 +223,130 @@ func isOpennicDomain(zone string) bool { return false } -func resolveOpennicAddr(host string) (ips []string) { +func queryCommonResolver(host string, ch chan<- resolveResult) { + defer perf.ScopeTimer()() + + commonLock.RLock() + resolver := commonResolver + commonLock.RUnlock() + + if resolver == nil { + ch <- resolveResult{source: "doh", err: fmt.Errorf("dns: common resolver is not configured")} + return + } + + ips, ttl, err := resolver.Resolve(context.Background(), host) + ch <- resolveResult{ + source: "doh", + ips: ips, + ttl: ttl, + err: err, + } +} + +func queryOpenNICResolver(host string, ch chan<- resolveResult) { defer perf.ScopeTimer()() opennicLock.RLock() - defer opennicLock.RUnlock() + resolver := opennicResolver + opennicLock.RUnlock() - ipsResolved, err := opennicResolver.LookupHost(host) - if err == nil && len(ipsResolved) > 0 { - for _, i := range ipsResolved { - ips = append(ips, i.String()) + if resolver == nil { + ch <- resolveResult{source: "opennic", err: fmt.Errorf("dns: OpenNIC resolver is not configured")} + return + } + + ips, ttl, err := resolver.Resolve(context.Background(), host) + ch <- resolveResult{ + source: "opennic", + ips: ips, + ttl: ttl, + err: err, + } +} + +func fetchOpenNICResolvers() ([]string, string) { + client := &http.Client{ + Timeout: openNICRequestTimeout, + } + + req, err := http.NewRequest(http.MethodGet, openNICResolversAPI, nil) + if err != nil { + log.Warningf("Could not prepare OpenNIC API request: %s", err) + return defaultOpenNICResolverServers, "fallback" + } + + resp, err := client.Do(req) + if err != nil { + log.Warningf("Could not fetch OpenNIC resolvers, using fallback list: %s", err) + return defaultOpenNICResolverServers, "fallback" + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Warningf("OpenNIC API returned status %d, using fallback list", resp.StatusCode) + return defaultOpenNICResolverServers, "fallback" + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + log.Warningf("Could not read OpenNIC API response, using fallback list: %s", err) + return defaultOpenNICResolverServers, "fallback" + } + + resolvers := parseOpenNICResolvers(string(body)) + if len(resolvers) == 0 { + log.Warningf("OpenNIC API returned no valid resolvers, using fallback list") + return defaultOpenNICResolverServers, "fallback" + } + + return resolvers, "api" +} + +func parseOpenNICResolvers(body string) []string { + servers := make([]string, 0) + seen := make(map[string]struct{}) + + for line := range strings.SplitSeq(body, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue } - return + if ip := net.ParseIP(line); ip == nil { + log.Debugf("Ignoring invalid OpenNIC resolver from API: %s", line) + continue + } + + resolverAddr := net.JoinHostPort(line, "53") + if _, exists := seen[resolverAddr]; exists { + continue + } + + seen[resolverAddr] = struct{}{} + servers = append(servers, resolverAddr) + } + + return servers +} + +func filterResolvedIPs(ips []string, host string) []string { + filtered := make([]string, 0, len(ips)) + isLocalhost := strings.EqualFold(strings.TrimSuffix(strings.TrimSpace(host), "."), "localhost") + + for _, ipStr := range ips { + ip := net.ParseIP(strings.TrimSpace(ipStr)) + if ip == nil { + continue + } + + normalized := ip.String() + if !isLocalhost && normalized == "127.0.0.1" { + continue + } + + filtered = append(filtered, normalized) } - return + return filtered } diff --git a/proxy/resolver.go b/proxy/resolver.go index 54c12a2b..30b84c5f 100644 --- a/proxy/resolver.go +++ b/proxy/resolver.go @@ -3,190 +3,243 @@ package proxy import ( "context" "fmt" - "slices" + "io" + "net" + "net/http" + "net/netip" + "net/url" "strings" - "sync" - - "github.com/likexian/doh/dns" - "github.com/likexian/doh/provider/cloudflare" - "github.com/likexian/doh/provider/dnspod" - "github.com/likexian/doh/provider/google" - "github.com/likexian/doh/provider/quad9" - "github.com/likexian/gokit/xcache" - "github.com/likexian/gokit/xhash" + "time" + + "codeberg.org/miekg/dns" + "codeberg.org/miekg/dns/dnshttp" ) -// Slightly modified DNS resolver, taken from github.com/likexian/doh/dns -// Differences: -// - Does not store stats, so always will query all providers and return fastest response. -// - Checks whether response points to localhost (127.0.0.1) to ignore that response. +const ( + defaultDoHServer = "https://dns.quad9.net/dns-query" + defaultDNSCacheTTL = 300 +) -// provider is provider -type provider uint +type DoHResolver struct { + endpoint string + client *http.Client +} -// Provider is the provider interface -type Provider interface { - Query(context.Context, dns.Domain, dns.Type, ...dns.ECS) (*dns.Response, error) - String() string +type OpenNICResolver struct { + servers []string + client *dns.Client } -// DoH is doh client -type DoH struct { - providers []Provider - cache xcache.Cachex - sync.RWMutex +func NewDoHResolver(server string) *DoHResolver { + return &DoHResolver{ + endpoint: normalizeDoHEndpoint(server), + client: &http.Client{ + Timeout: 5 * time.Second, + }, + } } -// DoH Providers enum -const ( - CloudflareProvider provider = iota - DNSPodProvider - GoogleProvider - Quad9Provider -) +func NewOpenNICResolver(servers []string) *OpenNICResolver { + cleanServers := make([]string, 0, len(servers)) + seen := make(map[string]struct{}, len(servers)) + for _, s := range servers { + resolverAddr, ok := ensureResolverAddress(s) + if !ok { + continue + } + if _, exists := seen[resolverAddr]; exists { + continue + } -// DoH Providers list -var ( - Providers = []provider{ - CloudflareProvider, - DNSPodProvider, - GoogleProvider, - Quad9Provider, + seen[resolverAddr] = struct{}{} + cleanServers = append(cleanServers, resolverAddr) } -) -// New returns a new DoH client, quad9 is default -func New(provider provider) Provider { - switch provider { - case CloudflareProvider: - return cloudflare.NewClient() - case DNSPodProvider: - return dnspod.NewClient() - case GoogleProvider: - return google.NewClient() - default: - return quad9.NewClient() + return &OpenNICResolver{ + servers: cleanServers, + client: dns.NewClient(), } } -func UseProviders(provider ...provider) *DoH { - c := &DoH{ - providers: []Provider{}, - cache: xcache.New(xcache.MemoryCache), +func (c *DoHResolver) Resolve(ctx context.Context, host string) ([]string, int, error) { + m := dns.NewMsg(host, dns.TypeA) + if m == nil { + return nil, 0, fmt.Errorf("invalid DNS query type for host %s", host) } - if len(provider) == 0 { - provider = Providers + req, err := dnshttp.NewRequest(http.MethodPost, c.endpoint, m) + if err != nil { + return nil, 0, err } - for _, v := range provider { - c.providers = append(c.providers, New(v)) + req = req.WithContext(ctx) + resp, err := c.client.Do(req) + if err != nil { + return nil, 0, err } - return c -} + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 256)) + resp.Body.Close() + + return nil, 0, fmt.Errorf("doh: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + dnsResp, err := dnshttp.Response(resp) + if err != nil { + return nil, 0, err + } + if dnsResp == nil { + return nil, 0, fmt.Errorf("doh: empty response") + } + if dnsResp.Rcode != dns.RcodeSuccess { + return nil, 0, fmt.Errorf("doh: rcode=%d", dnsResp.Rcode) + } -// Close close doh client -func (c *DoH) Close() { - if c.cache != nil { - c.cache.Close() + ips, ttl := extractIPsAndTTL(dnsResp) + if len(ips) == 0 { + return nil, 0, fmt.Errorf("doh: no A/AAAA answers") } + + return ips, ttl, nil } -// Query do DoH query -func (c *DoH) Query(ctx context.Context, d dns.Domain, t dns.Type, s ...dns.ECS) (*dns.Response, error) { - cacheKey := "" - if c.cache != nil { - var ss string - if len(s) > 0 && s[0] != "" { - ss = strings.TrimSpace(string(s[0])) +func (c *OpenNICResolver) Resolve(ctx context.Context, host string) ([]string, int, error) { + if len(c.servers) == 0 { + return nil, 0, fmt.Errorf("opennic: no resolvers configured") + } + + var lastErr error + for _, server := range c.servers { + m := dns.NewMsg(host, dns.TypeA) + if m == nil { + return nil, 0, fmt.Errorf("invalid DNS query type for host %s", host) + } + + resp, _, err := c.client.Exchange(ctx, m, "udp", server) + if err != nil { + lastErr = fmt.Errorf("opennic resolver %s: %w", server, err) + continue } - cacheKey = xhash.Sha1(string(d), string(t), ss).Hex() - v := c.cache.Get(cacheKey) - if v != nil { - return v.(*dns.Response), nil + if resp == nil { + lastErr = fmt.Errorf("opennic resolver %s: empty response", server) + continue + } + if resp.Rcode != dns.RcodeSuccess { + lastErr = fmt.Errorf("opennic resolver %s: rcode=%d", server, resp.Rcode) + continue } - } - ctxs, cancels := context.WithCancel(ctx) - defer cancels() + ips, ttl := extractIPsAndTTL(resp) + if len(ips) == 0 { + lastErr = fmt.Errorf("opennic resolver %s: no A/AAAA answers", server) + continue + } - r := make(chan *dns.Response, len(c.providers)) + return ips, ttl, nil + } - result := &dns.Response{ - Status: -1, + if lastErr == nil { + lastErr = fmt.Errorf("opennic: all resolvers failed") } - var wg sync.WaitGroup - for _, p := range c.providers { - wg.Add(1) - go func(p Provider) { - defer wg.Done() + return nil, 0, lastErr +} - resp, err := p.Query(ctxs, d, t, s...) - if err != nil { - return - } +func normalizeDoHEndpoint(server string) string { + server = strings.TrimSpace(server) + if server == "" { + server = defaultDoHServer + } + if !strings.Contains(server, "://") { + server = "https://" + server + } - // Ignoring results that point to ourselves, unless we want to resolve localhost - if ips := IPs(resp); d != "localhost" && slices.Contains(ips, "127.0.0.1") { - return - } + u, err := url.Parse(server) + if err != nil || u.Host == "" { + return defaultDoHServer + } - cancels() + u.RawQuery = "" + u.Fragment = "" + u.Path = trimDoHPath(u.Path) - if cacheKey != "" { - ttl := 30 - if len(result.Answer) > 0 { - ttl = result.Answer[0].TTL - } - _ = c.cache.Set(cacheKey, resp, int64(ttl)) - } + return strings.TrimRight(u.String(), "/") +} - r <- resp - }(p) +func trimDoHPath(path string) string { + path = strings.TrimSpace(path) + if path == "" || path == "/" { + return "" } - go func() { - wg.Wait() - close(r) - }() - - result = <-r + path = strings.TrimRight(path, "/") + path = strings.TrimSuffix(path, dnshttp.Path) - if result == nil || result.Status == -1 { - return nil, fmt.Errorf("doh: all query failed") + if path == "/" { + return "" } - return result, nil + return path } -func IPs(resp *dns.Response) []string { - if resp != nil && resp.Answer != nil { - ips := make([]string, 0, len(resp.Answer)) - for _, a := range resp.Answer { - if a.Type != ResponseTypeA && a.Type != ResponseTypeAAAA { - continue - } - ips = append(ips, a.Data) +func extractIPsAndTTL(resp *dns.Msg) ([]string, int) { + if resp == nil { + return nil, defaultDNSCacheTTL + } + + ips := make([]string, 0, len(resp.Answer)) + ttl := 0 + + for _, rr := range resp.Answer { + var ip netip.Addr + + switch v := rr.(type) { + case *dns.A: + ip = v.Addr + case *dns.AAAA: + ip = v.Addr + default: + continue + } + + if !ip.IsValid() { + continue + } + + ips = append(ips, ip.String()) + + recordTTL := int(rr.Header().TTL) + if recordTTL > 0 && (ttl == 0 || recordTTL < ttl) { + ttl = recordTTL } - return ips } - return nil + if ttl <= 0 { + ttl = defaultDNSCacheTTL + } + + return ips, ttl } -func TTL(resp *dns.Response) int { - ttl := 300 - if len(resp.Answer) > 0 { - for _, a := range resp.Answer { - if a.Type != ResponseTypeA && a.Type != ResponseTypeAAAA { - continue - } - ttl = a.TTL - break +func ensureResolverAddress(value string) (string, bool) { + rawHost := strings.TrimSpace(value) + if rawHost == "" { + return "", false + } + dnsPort := "53" + + if host, port, err := net.SplitHostPort(rawHost); err == nil { + rawHost = host + if port != "" { + dnsPort = port } } - return ttl + ip := net.ParseIP(rawHost) + if ip == nil { + return "", false + } + + return net.JoinHostPort(ip.String(), dnsPort), true } From 65872f9250bd8ac5c56848d2a97d6c84aeca258f Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Thu, 5 Mar 2026 11:33:06 +0300 Subject: [PATCH 3/6] OpenNIC --- config/config.go | 8 ++++---- proxy/dns.go | 15 +++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index ed1b4456..4d967e74 100644 --- a/config/config.go +++ b/config/config.go @@ -243,8 +243,8 @@ type Configuration struct { InternalDNSSkipIPv6 bool InternalDNSServer string InternalDNSServerCustom string - InternalDNSOpenNicUse bool - InternalDNSOpenNicOnlySpecialZones bool + InternalDNSOpenNICUse bool + InternalDNSOpenNICOnlySpecialZones bool InternalProxyEnabled bool InternalProxyLogging bool @@ -786,8 +786,8 @@ func Reload() (ret *Configuration, err error) { InternalDNSEnabled: settings.ToBool("internal_dns_enabled"), InternalDNSSkipIPv6: settings.ToBool("internal_dns_skip_ipv6"), - InternalDNSOpenNicUse: settings.ToBool("internal_dns_opennic_use"), - InternalDNSOpenNicOnlySpecialZones: settings.ToBool("internal_dns_opennic_only_special_zones"), + InternalDNSOpenNICUse: settings.ToBool("internal_dns_opennic_use"), + InternalDNSOpenNICOnlySpecialZones: settings.ToBool("internal_dns_opennic_only_special_zones"), InternalProxyEnabled: settings.ToBool("internal_proxy_enabled"), InternalProxyLogging: settings.ToBool("internal_proxy_logging"), diff --git a/proxy/dns.go b/proxy/dns.go index 496c8e1f..97e6b08e 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -17,7 +17,7 @@ import ( ) var ( - // opennicZones contains all zones from Opennic services. + // opennicZones contains all zones from OpenNIC services. // List can be taken here: https://wiki.opennic.org/opennic/dot opennicZones = []string{ "bbs", @@ -94,7 +94,7 @@ func reloadDNS() { commonResolver = NewDoHResolver(config.Get().InternalDNSServer) - if config.Get().InternalDNSOpenNicUse { + if config.Get().InternalDNSOpenNICUse { opennicResolvers, source := fetchOpenNICResolvers() opennicResolver = NewOpenNICResolver(opennicResolvers) log.Debugf("Configured OpenNIC resolvers from %s: %+v", source, opennicResolvers) @@ -104,12 +104,13 @@ func reloadDNS() { } // Each request is going through this workflow: -// Check cache -> Query DoH & Query Opennic (if enabled for everything or if address belongs to Opennic domains) -> Save cache +// Check cache -> Query DoH & Query OpenNIC (if enabled for everything or if address belongs to OpenNIC domains) -> Save cache func resolveAddr(addr string) (ret []string, err error) { defer perf.ScopeTimer()() // Check for results in the cache if cached := dnsCacheResults.Get(addr); cached != nil { + log.Debugf("1 DNS cache hit for %s", addr) //TODO: remove return cached.([]string), nil } @@ -126,14 +127,15 @@ func resolveAddr(addr string) (ret []string, err error) { defer mu.Unlock() if cached := dnsCacheResults.Get(addr); cached != nil { + log.Debugf("2 DNS cache hit for %s", addr) //TODO: remove return cached.([]string), nil } zone := getZone(addr) - isOpenNICZone := isOpennicDomain(zone) + isOpenNICZone := isOpenNICDomain(zone) runDoH := !isOpenNICZone - runOpenNIC := config.Get().InternalDNSOpenNicUse && (isOpenNICZone || !config.Get().InternalDNSOpenNicOnlySpecialZones) + runOpenNIC := config.Get().InternalDNSOpenNICUse && (isOpenNICZone || !config.Get().InternalDNSOpenNICOnlySpecialZones) results := make(chan resolveResult, 2) queriesCount := 0 @@ -197,6 +199,7 @@ func resolveAddr(addr string) (ret []string, err error) { if cacheTTL <= 0 { cacheTTL = defaultDNSCacheTTL } + log.Debugf("DNS cache TTL for %s is %d seconds", addr, cacheTTL) //TODO: remove dnsCacheResults.Set(addr, ret, int64(cacheTTL)) @@ -213,7 +216,7 @@ func getZone(addr string) string { return strings.ToLower(ary[len(ary)-1]) } -func isOpennicDomain(zone string) bool { +func isOpenNICDomain(zone string) bool { for _, z := range opennicZones { if z == zone { return true From 8fb040480446eb0ca648f7454e7273eae3d3cc76 Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Thu, 5 Mar 2026 12:25:51 +0300 Subject: [PATCH 4/6] USER_AGENT --- proxy/antizapret.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/antizapret.go b/proxy/antizapret.go index e29c9777..dc9b7159 100644 --- a/proxy/antizapret.go +++ b/proxy/antizapret.go @@ -27,7 +27,7 @@ import ( "github.com/elgatito/elementum/util" ) -const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" +const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" const CONFIG_FILE_NAME = "antizapret_config.gob" // Using gob for caching From 636bc9a8a3f51e5e873b0053268e05cfb966a867 Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Sat, 7 Mar 2026 16:09:45 +0300 Subject: [PATCH 5/6] update defaultOpenNICResolverServers --- proxy/dns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/dns.go b/proxy/dns.go index 97e6b08e..1aa51d45 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -54,7 +54,7 @@ var ( defaultOpenNICResolverServers = []string{ "94.247.43.254", "152.53.15.127", - "185.181.61.24", + "95.216.99.249", } commonResolver = NewDoHResolver(defaultDoHServer) From 3bea35b9fa131db34ed77d9e1f6a8b7cad1cd7fd Mon Sep 17 00:00:00 2001 From: Anton Soroko Date: Sun, 8 Mar 2026 18:22:26 +0300 Subject: [PATCH 6/6] remove TODOs --- AGENTS.md | 2 +- proxy/client.go | 2 +- proxy/dns.go | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 821802a3..ae6a9f8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Build outputs are written to `build/_/`. Operational fixtures and manu - `make` builds all platform targets defined in `Makefile`. - `make zip PLATFORM=linux-x64` packages a platform artifact zip. - `./test_build.sh local restart-local` builds locally and copies binaries into local Kodi addon paths. -- `/bin/bash test_build.sh sanitize` builds with Address/Leak Sanitizers for memory debugging. +- `./test_build.sh sanitize` builds with Address/Leak Sanitizers for memory debugging. ## Coding Style & Naming Conventions This is a Go codebase; follow standard Go formatting and naming. diff --git a/proxy/client.go b/proxy/client.go index 0808db71..a0bd8647 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -84,7 +84,7 @@ func CustomDialContext(ctx context.Context, network, addr string) (net.Conn, err if len(addrs) == 2 && len(addrs[0]) > 2 && strings.Contains(addrs[0], ".") { if ipTest := net.ParseIP(addrs[0]); ipTest == nil { if ips, err := resolveAddr(addrs[0]); err == nil && len(ips) > 0 { - log.Debugf("Resolved DNS %s to %+v", addrs[0], ips) //TODO: remove + //log.Debugf("Resolved DNS %s to %+v", addrs[0], ips) for _, i := range ips { if config.Get().InternalDNSSkipIPv6 { if ip := net.ParseIP(i); ip == nil || ip.To4() == nil { diff --git a/proxy/dns.go b/proxy/dns.go index 1aa51d45..00602990 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -110,7 +110,6 @@ func resolveAddr(addr string) (ret []string, err error) { // Check for results in the cache if cached := dnsCacheResults.Get(addr); cached != nil { - log.Debugf("1 DNS cache hit for %s", addr) //TODO: remove return cached.([]string), nil } @@ -127,7 +126,6 @@ func resolveAddr(addr string) (ret []string, err error) { defer mu.Unlock() if cached := dnsCacheResults.Get(addr); cached != nil { - log.Debugf("2 DNS cache hit for %s", addr) //TODO: remove return cached.([]string), nil } @@ -168,7 +166,7 @@ func resolveAddr(addr string) (ret []string, err error) { log.Debugf("DNS resolver %s failed for %s: %s", result.source, addr, result.err) continue } - log.Debugf("DNS resolver %s returned %v IPs for %s", result.source, result.ips, addr) //TODO: remove + //log.Debugf("DNS resolver %s returned %v IPs for %s", result.source, result.ips, addr) ips := filterResolvedIPs(result.ips, addr) if len(ips) == 0 { @@ -199,7 +197,6 @@ func resolveAddr(addr string) (ret []string, err error) { if cacheTTL <= 0 { cacheTTL = defaultDNSCacheTTL } - log.Debugf("DNS cache TTL for %s is %d seconds", addr, cacheTTL) //TODO: remove dnsCacheResults.Set(addr, ret, int64(cacheTTL))