├── .dockerignore ├── .github └── workflows │ ├── docker.yml │ ├── golangci.yml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── azure-pipelines.yaml ├── cmd └── snd │ └── snd.go ├── contrib ├── build │ ├── setcap │ └── upx ├── config │ └── config.toml ├── goreleaser │ └── goreleaser.yaml └── perf │ └── perf.sh ├── go.mod ├── go.sum └── pkg ├── config ├── config.go ├── parser.go └── utils.go ├── dns_server ├── dns_handler.go ├── dns_handler_default.go ├── dns_handler_ns.go ├── dns_handler_ptr.go ├── dns_handler_soa.go ├── dns_handler_txt.go └── dns_server.go └── version └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | LICENSE 3 | *.yaml 4 | *.yml 5 | /build 6 | /dist 7 | .github/ 8 | .idea/ 9 | 10 | !/contrib/goreleaser/goreleaser.yaml 11 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # IDEA auto formatter is causing trouble 2 | # @formatter:off 3 | name: Docker Image Build 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - uses: actions/checkout@v3 13 | 14 | - name: Build the Docker image 15 | run: docker build . --tag snd 16 | 17 | - name: Verify image 18 | run: docker run snd -version 19 | -------------------------------------------------------------------------------- /.github/workflows/golangci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | golangci: 14 | strategy: 15 | matrix: 16 | go: [ "1.20" ] 17 | os: [ "ubuntu-latest" ] 18 | name: lint 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: "${{ matrix.go }}" 28 | cache: false 29 | 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@v3 32 | continue-on-error: true 33 | with: 34 | only-new-issues: true 35 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser-release 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | GOPATH: /tmp/go 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | env: 18 | flags: "" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Fetch all tags 26 | run: git fetch --force --tags 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@v3 30 | 31 | - name: Check 32 | uses: goreleaser/goreleaser-action@v4 33 | with: 34 | distribution: goreleaser 35 | version: latest 36 | args: check --config contrib/goreleaser/goreleaser.yaml 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 41 | run: echo "flags=--snapshot" >> $GITHUB_ENV 42 | 43 | - name: Release 44 | uses: goreleaser/goreleaser-action@v4 45 | with: 46 | distribution: goreleaser 47 | version: latest 48 | args: release --config contrib/goreleaser/goreleaser.yaml --clean ${{ env.flags }} 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | .idea/ 4 | 5 | # Created by https://www.gitignore.io/api/go,macos,windows,intellij,visualstudiocode 6 | # Edit at https://www.gitignore.io/?templates=go,macos,windows,intellij,visualstudiocode 7 | 8 | ### Go ### 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | ### Go Patch ### 26 | /vendor/ 27 | /Godeps/ 28 | 29 | ### Intellij ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 32 | 33 | # User-specific stuff 34 | .idea/**/workspace.xml 35 | .idea/**/tasks.xml 36 | .idea/**/usage.statistics.xml 37 | .idea/**/dictionaries 38 | .idea/**/shelf 39 | 40 | # Generated files 41 | .idea/**/contentModel.xml 42 | 43 | # Sensitive or high-churn files 44 | .idea/**/dataSources/ 45 | .idea/**/dataSources.ids 46 | .idea/**/dataSources.local.xml 47 | .idea/**/sqlDataSources.xml 48 | .idea/**/dynamic.xml 49 | .idea/**/uiDesigner.xml 50 | .idea/**/dbnavigator.xml 51 | 52 | # Gradle 53 | .idea/**/gradle.xml 54 | .idea/**/libraries 55 | 56 | # Gradle and Maven with auto-import 57 | # When using Gradle or Maven with auto-import, you should exclude module files, 58 | # since they will be recreated, and may cause churn. Uncomment if using 59 | # auto-import. 60 | # .idea/modules.xml 61 | # .idea/*.iml 62 | # .idea/modules 63 | # *.iml 64 | # *.ipr 65 | 66 | # CMake 67 | cmake-build-*/ 68 | 69 | # Mongo Explorer plugin 70 | .idea/**/mongoSettings.xml 71 | 72 | # File-based project format 73 | *.iws 74 | 75 | # IntelliJ 76 | out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Cursive Clojure plugin 85 | .idea/replstate.xml 86 | 87 | # Crashlytics plugin (for Android Studio and IntelliJ) 88 | com_crashlytics_export_strings.xml 89 | crashlytics.properties 90 | crashlytics-build.properties 91 | fabric.properties 92 | 93 | # Editor-based Rest Client 94 | .idea/httpRequests 95 | 96 | # Android studio 3.1+ serialized cache file 97 | .idea/caches/build_file_checksums.ser 98 | 99 | ### Intellij Patch ### 100 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 101 | 102 | # *.iml 103 | # modules.xml 104 | # .idea/misc.xml 105 | # *.ipr 106 | 107 | # Sonarlint plugin 108 | .idea/**/sonarlint/ 109 | 110 | # SonarQube Plugin 111 | .idea/**/sonarIssues.xml 112 | 113 | # Markdown Navigator plugin 114 | .idea/**/markdown-navigator.xml 115 | .idea/**/markdown-navigator/ 116 | 117 | ### macOS ### 118 | # General 119 | .DS_Store 120 | .AppleDouble 121 | .LSOverride 122 | 123 | # Icon must end with two \r 124 | Icon 125 | 126 | # Thumbnails 127 | ._* 128 | 129 | # Files that might appear in the root of a volume 130 | .DocumentRevisions-V100 131 | .fseventsd 132 | .Spotlight-V100 133 | .TemporaryItems 134 | .Trashes 135 | .VolumeIcon.icns 136 | .com.apple.timemachine.donotpresent 137 | 138 | # Directories potentially created on remote AFP share 139 | .AppleDB 140 | .AppleDesktop 141 | Network Trash Folder 142 | Temporary Items 143 | .apdisk 144 | 145 | ### VisualStudioCode ### 146 | .vscode/* 147 | !.vscode/settings.json 148 | !.vscode/tasks.json 149 | !.vscode/launch.json 150 | !.vscode/extensions.json 151 | 152 | ### VisualStudioCode Patch ### 153 | # Ignore all local history of files 154 | .history 155 | 156 | ### Windows ### 157 | # Windows thumbnail cache files 158 | Thumbs.db 159 | Thumbs.db:encryptable 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Dump file 164 | *.stackdump 165 | 166 | # Folder config file 167 | [Dd]esktop.ini 168 | 169 | # Recycle Bin used on file shares 170 | $RECYCLE.BIN/ 171 | 172 | # Windows Installer files 173 | *.cab 174 | *.msi 175 | *.msix 176 | *.msm 177 | *.msp 178 | 179 | # Windows shortcuts 180 | *.lnk 181 | 182 | # End of https://www.gitignore.io/api/go,macos,windows,intellij,visualstudiocode -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 5m 4 | 5 | # include test files or not, default is true 6 | tests: true 7 | 8 | # default is true. Enables skipping of directories: 9 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 10 | skip-dirs-use-default: true 11 | 12 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 13 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 14 | # automatic updating of go.mod described above. Instead, it fails when any changes 15 | # to go.mod are needed. This setting is most useful to check that go.mod does 16 | # not need updates, such as in a continuous integration and testing system. 17 | # If invoked with -mod=vendor, the go command assumes that the vendor 18 | # directory holds the correct copies of dependencies and ignores 19 | # the dependency descriptions in go.mod. 20 | # modules-download-mode: readonly 21 | 22 | # all available settings of specific linters 23 | linters-settings: 24 | errcheck: 25 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 26 | # default is false: such cases aren't reported by default. 27 | check-type-assertions: true 28 | 29 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 30 | # default is false: such cases aren't reported by default. 31 | check-blank: true 32 | 33 | govet: 34 | # report about shadowed variables 35 | check-shadowing: true 36 | 37 | # settings per analyzer 38 | settings: 39 | printf: # analyzer name, run `go tool vet help` to see all analyzers 40 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 41 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 42 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 43 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 44 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 45 | golint: 46 | # minimal confidence for issues, default is 0.8 47 | min-confidence: 0.8 48 | gofmt: 49 | # simplify code: gofmt with `-s` option, true by default 50 | simplify: true 51 | goimports: 52 | # put imports beginning with prefix after 3rd-party packages; 53 | # it's a comma-separated list of prefixes 54 | local-prefixes: github.com/Jamesits/SND 55 | gocyclo: 56 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 57 | min-complexity: 10 58 | gocognit: 59 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 60 | min-complexity: 10 61 | maligned: 62 | # print struct with more effective memory layout or not, false by default 63 | suggest-new: true 64 | dupl: 65 | # tokens count to trigger issue, 150 by default 66 | threshold: 100 67 | goconst: 68 | # minimal length of string constant, 3 by default 69 | min-len: 3 70 | # minimal occurrences count to trigger, 3 by default 71 | min-occurrences: 2 72 | depguard: 73 | list-type: blacklist 74 | include-go-root: false 75 | packages: 76 | - github.com/sirupsen/logrus 77 | packages-with-error-messages: 78 | # specify an error message to output when a blacklisted package is used 79 | github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 80 | unused: 81 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 82 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 83 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 84 | # with golangci-lint call it on a directory with the changed file. 85 | check-exported: false 86 | unparam: 87 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 88 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 89 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 90 | # with golangci-lint call it on a directory with the changed file. 91 | check-exported: false 92 | nakedret: 93 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 94 | max-func-lines: 30 95 | prealloc: 96 | # XXX: we don't recommend using this linter before doing performance profiling. 97 | # For most programs usage of prealloc will be a premature optimization. 98 | 99 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 100 | # True by default. 101 | simple: true 102 | range-loops: true # Report preallocation suggestions on range loops, true by default 103 | for-loops: false # Report preallocation suggestions on for loops, false by default 104 | gocritic: 105 | enabled-tags: 106 | - diagnostic 107 | - experimental 108 | - opinionated 109 | - performance 110 | - style 111 | disabled-checks: 112 | - wrapperFunc 113 | - dupImport # https://github.com/go-critic/go-critic/issues/845 114 | - ifElseChain 115 | - octalLiteral 116 | - emptyStringTest 117 | dogsled: 118 | # checks assignments with too many blank identifiers; default is 2 119 | max-blank-identifiers: 2 120 | whitespace: 121 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 122 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 123 | wsl: 124 | # If true append is only allowed to be cuddled if appending value is 125 | # matching variables, fields or types on line above. Default is true. 126 | strict-append: true 127 | # Allow calls and assignments to be cuddled as long as the lines have any 128 | # matching variables, fields or types. Default is true. 129 | allow-assign-and-call: true 130 | # Allow multiline assignments to be cuddled. Default is true. 131 | allow-multiline-assign: true 132 | # Allow declarations (var) to be cuddled. 133 | allow-cuddle-declarations: false 134 | # Allow trailing comments in ending of blocks 135 | allow-trailing-comment: false 136 | # Force newlines in end of case at this limit (0 = never). 137 | force-case-trailing-whitespace: 0 138 | 139 | linters: 140 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 141 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 142 | disable-all: true 143 | enable: 144 | - bodyclose 145 | - deadcode 146 | - depguard 147 | - dogsled 148 | - dupl 149 | - errcheck 150 | - gochecknoinits 151 | - goconst 152 | - gocritic 153 | - gocyclo 154 | - gofmt 155 | - goimports 156 | - golint 157 | - gosec 158 | - gosimple 159 | - govet 160 | - ineffassign 161 | - interfacer 162 | - nakedret 163 | - scopelint 164 | - staticcheck 165 | - structcheck 166 | - stylecheck 167 | - typecheck 168 | - unconvert 169 | - unparam 170 | - unused 171 | - varcheck 172 | - whitespace 173 | - gocognit 174 | - maligned 175 | - prealloc 176 | 177 | issues: 178 | # Excluding configuration per-path, per-linter, per-text and per-source 179 | exclude-rules: 180 | # Exclude some linters from running on tests files. 181 | - path: _test\.go 182 | linters: 183 | - gocyclo 184 | - errcheck 185 | - dupl 186 | - gosec 187 | 188 | # Exclude known linters from partially hard-vendored code, 189 | # which is impossible to exclude via "nolint" comments. 190 | - path: internal/hmac/ 191 | text: "weak cryptographic primitive" 192 | linters: 193 | - gosec 194 | 195 | # Exclude lll issues for long lines with go:generate 196 | - linters: 197 | - lll 198 | source: "^//go:generate " 199 | 200 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 201 | max-issues-per-linter: 0 202 | 203 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 204 | max-same-issues: 0 205 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang:1.22-bullseye as builder 3 | 4 | ARG GOPATH=/tmp/go 5 | RUN apt-get update -y \ 6 | && apt-get install -y upx libcap2-bin \ 7 | && go install github.com/goreleaser/goreleaser@latest 8 | 9 | WORKDIR /root/snd 10 | COPY . /root/snd/ 11 | RUN /tmp/go/bin/goreleaser build --config contrib/goreleaser/goreleaser.yaml --single-target --id "snd" --output "dist/snd" --snapshot --clean 12 | 13 | # production stage 14 | FROM debian:bullseye-slim 15 | LABEL org.opencontainers.image.authors="docker@public.swineson.me" 16 | 17 | # Import the user and group files from the builder. 18 | COPY --from=builder /etc/passwd /etc/group /etc/ 19 | 20 | COPY --from=builder /root/snd/dist/snd /usr/local/bin/ 21 | COPY --from=builder /root/snd/contrib/config/config.toml /etc/snd/ 22 | # nope 23 | # See: https://github.com/moby/moby/issues/8460 24 | # USER nobody:nogroup 25 | 26 | EXPOSE 53/tcp 53/udp 27 | ENTRYPOINT [ "/usr/local/bin/snd" ] 28 | CMD [ "-config", "/etc/snd/config.toml" ] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 James Swineson 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SND 2 | 3 | Minimal authoritative PTR (rDNS, reverse DNS) resolver with automatic generation of records. 4 | 5 | [![Build Status](https://dev.azure.com/nekomimiswitch/General/_apis/build/status/SND?branchName=master)](https://dev.azure.com/nekomimiswitch/General/_build/latest?definitionId=71&branchName=master) 6 | [![](https://images.microbadger.com/badges/image/jamesits/snd.svg)](https://microbadger.com/images/jamesits/snd "Get your own image badge on microbadger.com") 7 | 8 | ## Motivation 9 | 10 | Say you have a large collection of IP addresses (thousands of IPv4 /24 blocks, or one IPv6 /32 block), and you want to have PTR records on all of your IPs. Writing a zonefile and hosting it using any traditional authoritative DNS server is unrealistic: the zonefile will be of multiple GBs and you need an enormous amount of memory to even load it. 11 | 12 | SND provides you a simple alternative option: you name a base domain, and SND generates PTR records for you on the fly based on a set of pre-defined rules. 13 | 14 | ``` 15 | 1.1.168.192.in-addr.arpa. 1000 IN PTR 192.168.1.1.ptr.example.com. 16 | 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.d.f.ip6.arpa. 1000 IN PTR fd00.1.0.0.0.0.0.1.ptr.example.com. 17 | ``` 18 | 19 | ## Requirements 20 | 21 | ### Hardware 22 | 23 | SND can run on very little processing power (Raspberry Pis are fine) and a very small memory footprint (a few MBs) although the performance will be not optimal. 24 | 25 | As of version 0.1.2, on a 4-core Intel E5-2670 VM with more than 2GiB memory, SND can process around 25K RPS. 26 | 27 | ### Software 28 | 29 | Officially supported OS: 30 | 31 | * Linux (kernel 4.19+ with glibc) 32 | * Windows (Windows Server 2016 or later, Windows 10 Desktop 1809 or later) 33 | 34 | Other OSes are not currently tested because of the lack of resources available to me. 35 | 36 | ## Usage 37 | 38 | ### Configure SND 39 | 40 | Copy over the self-documented [example config](contrib/config/config.toml) and tweak it for your own need. Please do not 41 | leave any `example.com` things in your own config. Remove what you don't need. 42 | 43 | Currently no strict config file format checking is implemented -- you might crash the program if some important keys are 44 | missing. 45 | 46 | ### Set up SND 47 | 48 | In most cases you are going to need 2 servers (or one server with 2 different IP addresses if you don't care about availability issues). Copy the exact same config file to both servers and launch SND on both of them: 49 | 50 | Download the pre-compiled binary from [releases](https://github.com/Jamesits/SND/releases) to your server and run it: 51 | 52 | ```shell 53 | chmod +x ./snd 54 | ./snd -config path/to/config.toml 55 | ``` 56 | 57 | Or, if you prefer Docker: 58 | 59 | ```shell 60 | docker run --rm -p 53:53 -p 53:53/udp -v path/to/config.toml:/etc/snd/config.toml:ro snd:latest 61 | ``` 62 | 63 | Run a simple test using dig: 64 | 65 | ```shell 66 | $ dig @localhost -x 192.0.2.1 67 | 68 | ; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @localhost -x 192.0.2.1 69 | ; (1 server found) 70 | ;; global options: +cmd 71 | ;; Got answer: 72 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50924 73 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 74 | ;; WARNING: recursion requested but not available 75 | 76 | ;; QUESTION SECTION: 77 | ;1.2.0.192.in-addr.arpa. IN PTR 78 | 79 | ;; ANSWER SECTION: 80 | 1.2.0.192.in-addr.arpa. 3600 IN PTR 192.0.2.1.example.com. 81 | ``` 82 | 83 | ### Set up DNS records 84 | 85 | You need at least 2 `A` or `AAAA` records pointing to each of your SND servers. You might need to set them up as glue records based on your actual config. 86 | 87 | ``` 88 | ns1.example.com. 3600 IN A 192.0.2.1 89 | ns2.example.com. 3600 IN A 192.0.2.2 90 | ``` 91 | 92 | ### Set up PTR record delegation 93 | 94 | Set up a `domain` object at your RIR like this. 95 | 96 | ``` 97 | domain: 98 | descr: 99 | admin-c: 100 | tech-c: 101 | zone-c: 102 | nserver: ns1.example.com 103 | nserver: ns2.example.com 104 | mnt-by: 105 | ``` 106 | 107 | Detailed instructions are provided per RIR: 108 | 109 | * [AfriNIC](https://afrinic.net/support/requesting-reverse-delegation) 110 | * [ARIN](https://www.arin.net/resources/manage/reverse/) 111 | * [APNIC](https://www.apnic.net/manage-ip/manage-resources/reverse-dns/) 112 | * [LACNIC](https://www.lacnic.net/685/2/lacnic/5-delegation-of-reverse-resolution) 113 | * [RIPE NCC](https://www.ripe.net/manage-ips-and-asns/db/support/configuring-reverse-dns) 114 | 115 | Notes: 116 | 117 | * The smallest IP block sizes available for delegation differ 118 | * Only RIPE NCC is currently tested because I cannot afford IP blocks from the other RIRs 119 | 120 | ## Compilation 121 | 122 | Golang 1.22 or later is officially supported. Before starting, make sure the `GOROOT` and `GOPATH` environment 123 | variables are set correctly and there is a `go` binary is in your `PATH`. 124 | 125 | ```shell 126 | git clone https://github.com/Jamesits/SND.git 127 | cd SND 128 | go build github.com/jamesits/snd/cmd/snd 129 | ``` 130 | -------------------------------------------------------------------------------- /azure-pipelines.yaml: -------------------------------------------------------------------------------- 1 | name: $(SourceBranchName)-$(Date:yyyyMMdd).$(Rev:r) 2 | variables: 3 | GOVER: '1.20.3' 4 | GOPATH: '/tmp/go' 5 | 6 | trigger: 7 | batch: true 8 | branches: 9 | include: [ "master", "develop" ] 10 | paths: 11 | exclude: [ "README.md", "LICENSE", ".github/*", "contrib/config/*", ".golangci.yml" ] 12 | 13 | pr: 14 | autoCancel: true 15 | branches: 16 | include: [ "master", "develop" ] 17 | paths: 18 | exclude: [ "README.md", "LICENSE", ".github/*", "contrib/config/*", ".golangci.yml" ] 19 | 20 | jobs: 21 | - job: binary 22 | displayName: "binary build" 23 | pool: 24 | vmImage: "ubuntu-latest" 25 | workspace: 26 | clean: all 27 | timeoutInMinutes: 15 28 | 29 | steps: 30 | - task: GoTool@0 31 | displayName: 'Use Go' 32 | inputs: 33 | version: $(GOVER) 34 | 35 | - bash: | 36 | export DEBIAN_FRONTEND=noninteractive 37 | sudo apt-get update 38 | sudo apt-get install -y upx libcap2-bin 39 | displayName: 'Install Dependencies' 40 | 41 | - task: goreleaser@0 42 | inputs: 43 | version: 'latest' 44 | distribution: 'goreleaser' 45 | args: 'build --config contrib/goreleaser/goreleaser.yaml --snapshot --clean' 46 | workdir: '$(Build.SourcesDirectory)' 47 | 48 | - bash: | 49 | cp -rv dist/* ${BUILD_ARTIFACTSTAGINGDIRECTORY} 50 | displayName: 'Copy Artifact' 51 | 52 | - task: PublishBuildArtifacts@1 53 | displayName: 'Publish Artifact' 54 | inputs: 55 | ArtifactName: 'snd' 56 | -------------------------------------------------------------------------------- /cmd/snd/snd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/BurntSushi/toml" 7 | "github.com/jamesits/libiferr/exception" 8 | "github.com/jamesits/snd/pkg/config" 9 | "github.com/jamesits/snd/pkg/dns_server" 10 | "github.com/jamesits/snd/pkg/version" 11 | "log" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | var conf *config.Config 17 | var configFilePath *string 18 | var showVersionOnly *bool 19 | var mainThreadWaitGroup = &sync.WaitGroup{} 20 | 21 | func main() { 22 | configFilePath = flag.String("config", "/etc/snd/config.toml", "config file") 23 | showVersionOnly = flag.Bool("version", false, "show version and quit") 24 | flag.Parse() 25 | 26 | if *showVersionOnly { 27 | fmt.Println(version.GetVersionFullString()) 28 | return 29 | } else { 30 | log.Println(version.GetVersionFullString()) 31 | } 32 | 33 | // parse config file 34 | conf = &config.Config{} 35 | metaData, err := toml.DecodeFile(*configFilePath, conf) 36 | exception.HardFailWithReason("failed to read the config file", err) 37 | 38 | // print unknown configs 39 | for _, key := range metaData.Undecoded() { 40 | log.Printf("Unknown key %q in the Config file, maybe a typo?", key.String()) 41 | } 42 | 43 | // fix config and fill in defaults 44 | conf.FixConfig() 45 | 46 | // listen on all the configured listeners 47 | for _, elem := range conf.Listen { 48 | r := strings.SplitN(*elem, ":", 2) 49 | mainThreadWaitGroup.Add(1) 50 | go func() { 51 | defer mainThreadWaitGroup.Done() 52 | dns_server.ListenSync(conf, r[0], r[1]) 53 | }() 54 | } 55 | 56 | mainThreadWaitGroup.Wait() 57 | } 58 | -------------------------------------------------------------------------------- /contrib/build/setcap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if command -v sudo >/dev/null 2>&1; then 4 | exec sudo setcap "$@" 5 | else 6 | exec setcap "$@" 7 | fi 8 | -------------------------------------------------------------------------------- /contrib/build/upx: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if command -v upx >/dev/null 2>&1; then 4 | upx "$@" || true 5 | fi 6 | 7 | exit 0 8 | -------------------------------------------------------------------------------- /contrib/config/config.toml: -------------------------------------------------------------------------------- 1 | # This is an example config of SND. 2 | 3 | listen = [ 4 | # protocol:ip.addr:port 5 | "tcp::53", 6 | "udp::53", 7 | ] 8 | 9 | # set to the domain names of your authoritive DNS servers 10 | NS = [ 11 | "ns1.example.com.", 12 | "ns2.example.com.", 13 | ] 14 | 15 | # use DNS pointer compression 16 | compress_dns_messages = true 17 | 18 | # disable querying DNS server version by a DNS request 19 | allow_version_reporting = false 20 | # alternatively, you can fake the version string 21 | # version_string = "bind-⑨" 22 | 23 | # This will become the default SOA record for all your networks, unless overrided. 24 | # Make it exactly the same across all servers. 25 | [SOA] 26 | # your primary NS, usually the domain name of one of your NS servers, dot at end 27 | MName = "ns1.example.com." 28 | # your email in dot notation, dot at end 29 | RName = "dnsmaster.example.com." 30 | ## Optional fields, see Wikipedia for explanation 31 | # Serial = 1970010101 32 | # Refresh = 86400 33 | # Retry = 7200 34 | # Expire = 3600000 35 | # TTL = 172800 36 | 37 | # Define your nets here 38 | [[net]] 39 | net = "192.168.1.0/24" 40 | mode = "prefix_ltr" 41 | domain = "example.com" 42 | # Generates "192.168.1.1.example.com." 43 | 44 | [[net]] 45 | net = "192.168.2.0/24" 46 | mode = "prefix_rtl" 47 | domain = "example.com" 48 | # Generates "1.2.168.192.example.com." 49 | 50 | [[net]] 51 | net = "192.168.3.0/24" 52 | mode = "fixed" 53 | domain = "example.com" 54 | # Generates "example.com." 55 | 56 | [[net]] 57 | net = "192.168.4.0/24" 58 | mode = "prefix_ltr" 59 | domain = "example.com" 60 | domain_prefix = "yes." 61 | # Generates "yes.192.168.4.1.example.com." 62 | # Note: domain_prefix is applied without any sanity check. Make sure it doesn't violate the rules. 63 | 64 | [[net]] 65 | net = "fd00::/48" 66 | mode = "prefix_ltr" 67 | domain = "example.com" 68 | # Works for IPv6 too! Generates "f.d.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.example.com." 69 | 70 | [[net]] 71 | net = "fd00:1::/48" 72 | mode = "prefix_ltr" 73 | domain = "example.com" 74 | ipv6_notation = "four_hexs" 75 | # Default IPv6 notation is too long for you? This generates "fd00.1.0.0.0.0.0.1.example.com." 76 | 77 | [host] 78 | # quick way to generate a static record for a static IP 79 | "10.10.10.10" = "internal10.example.com" 80 | "10.10.10.20" = "internal20.example.com" 81 | -------------------------------------------------------------------------------- /contrib/goreleaser/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # IDEA auto formatter is causing trouble 2 | # @formatter:off 3 | before: 4 | hooks: 5 | - "go mod tidy" 6 | - "go generate ./..." 7 | 8 | env: 9 | - "GO111MODULE=on" 10 | - "CGO_ENABLED=0" 11 | 12 | gomod: 13 | proxy: true 14 | mod: mod 15 | env: 16 | - "GOPROXY=https://proxy.golang.org,direct" 17 | - "GOSUMDB=sum.golang.org" 18 | - "GOPRIVATE=github.com/jamesits/snd" 19 | 20 | builds: 21 | - id: "snd" 22 | main: "./cmd/snd" 23 | binary: "snd" 24 | mod_timestamp: "{{ .CommitTimestamp }}" 25 | goos: 26 | - "linux" 27 | - "windows" 28 | - "darwin" 29 | goarch: 30 | - "amd64" 31 | - "arm" 32 | - "arm64" 33 | goarm: 34 | - "6" 35 | - "7" 36 | flags: 37 | - "-v" 38 | - "-trimpath" 39 | - "-buildvcs=false" 40 | asmflags: 41 | - "all=-trimpath={{ .Env.GOPATH }}" 42 | gcflags: 43 | - "all=-trimpath={{ .Env.GOPATH }}" 44 | ldflags: 45 | - "-X 'github.com/jamesits/snd/pkg/version.version={{ .Version }}'" 46 | - "-X 'github.com/jamesits/snd/pkg/version.versionGitCommitHash={{ .Commit }}'" 47 | - "-X 'github.com/jamesits/snd/pkg/version.versionCompileTime={{ .CommitDate }}'" 48 | - "-X 'github.com/jamesits/snd/pkg/version.BuiltBy=goreleaser'" 49 | - "-s" 50 | - "-w" 51 | tags: 52 | - "static" 53 | - "static_build" 54 | hooks: 55 | post: 56 | - "./contrib/build/upx \"{{ .Path }}\"" 57 | - "./contrib/build/setcap 'cap_net_bind_service=+ep' \"{{ .Path }}\"" 58 | 59 | snapshot: 60 | name_template: "{{ incpatch .Version }}-next" 61 | 62 | archives: 63 | - id: "release" 64 | format: "tar.xz" 65 | wrap_in_directory: true 66 | rlcp: true 67 | format_overrides: 68 | - goos: "windows" 69 | format: "zip" 70 | name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 71 | files: 72 | - src: "contrib/config/config.toml" 73 | dst: "config.toml" 74 | 75 | nfpms: 76 | - id: "snd" 77 | package_name: "snd" 78 | vendor: "James Swineson" 79 | homepage: "https://github.com/Jamesits/SND" 80 | maintainer: "James Swineson " 81 | description: |- 82 | Minimal authoritative PTR (rDNS, reverse DNS) resolver with automatic generation of records. 83 | license: "MIT License" 84 | formats: 85 | - "apk" 86 | - "deb" 87 | - "rpm" 88 | - "termux.deb" 89 | - "archlinux" 90 | contents: 91 | - src: "contrib/config/config.toml" 92 | dst: "/etc/snd/config.toml" 93 | type: "config|noreplace" 94 | 95 | checksum: 96 | name_template: "checksums.txt" 97 | algorithm: "sha256" 98 | 99 | changelog: 100 | sort: "asc" 101 | filters: 102 | exclude: 103 | - "^doc:" 104 | - "^docs:" 105 | - "^test:" 106 | - "^cleanup:" 107 | - "^ci:" 108 | - "typo" 109 | - "readme" 110 | - "README" 111 | - "comment" 112 | -------------------------------------------------------------------------------- /contrib/perf/perf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. 5 | 6 | DNSPERF_DATA_FILE="build/dnsperf.txt" 7 | 8 | echo "" > "$DNSPERF_DATA_FILE" 9 | 10 | # useful requests 11 | # it takes a while to generate them -- be patient with bash 12 | for i in `seq 1 20000`; do 13 | LOW_64_BITS=$(printf '%016x\n' $i | rev | fold -b1 | paste -sd'.' -) 14 | echo "$LOW_64_BITS.0.0.0.0.0.0.0.0.1.0.0.0.0.0.d.f.ip6.arpa. PTR" >> "$DNSPERF_DATA_FILE" 15 | done 16 | 17 | # errornous requests 18 | # https://www.techietown.info/2017/03/load-testing-dns-using-dnsperf/ 19 | for i in `seq 1 2000000`; do 20 | echo "$i.example.com A" >> "$DNSPERF_DATA_FILE" 21 | done 22 | 23 | # download and compile https://github.com/cobblau/dnsperf 24 | # need around 5mins for 5000000 requests 25 | dnsperf -s 127.0.0.1 -d build/dnsperf.txt -c 1000 -Q 5000000 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jamesits/snd 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/jamesits/libiferr v0.0.0-20230830160344-0e47eade4350 8 | github.com/miekg/dns v1.1.58 9 | ) 10 | 11 | require ( 12 | github.com/sirupsen/logrus v1.9.3 // indirect 13 | golang.org/x/mod v0.16.0 // indirect 14 | golang.org/x/net v0.22.0 // indirect 15 | golang.org/x/sys v0.18.0 // indirect 16 | golang.org/x/tools v0.19.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 4 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/jamesits/libiferr v0.0.0-20230312172222-bd3c95a20420 h1:mz3okJ4qD7uLpDS7AGET1Ohc6alaKavcW9URvz1KGwo= 9 | github.com/jamesits/libiferr v0.0.0-20230312172222-bd3c95a20420/go.mod h1:his4Tyo0wLYXIUUwzKmI9I1V5G9bh6ZEB5NnaLyn58I= 10 | github.com/jamesits/libiferr v0.0.0-20230830160344-0e47eade4350 h1:iPU85bdI6ncCkhBo6VPM+TGHNeIlP2CbhatwTgDJseo= 11 | github.com/jamesits/libiferr v0.0.0-20230830160344-0e47eade4350/go.mod h1:/2cAJ7KJhFEju7YIRKS1lsUSx6wYBW+O7WGbwZuxKyw= 12 | github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= 13 | github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= 14 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 15 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 19 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 20 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 21 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 25 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 26 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 27 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 28 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 29 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 30 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 31 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 32 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 33 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 34 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 35 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 37 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 39 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 41 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= 43 | golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= 44 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 45 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 49 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "net" 4 | 5 | type PtrGenerationMode int 6 | 7 | const ( 8 | Fixed PtrGenerationMode = iota 9 | PrependLeftToRight 10 | PrependRightToLeft 11 | PrependLeftToRightDash 12 | PrependRightToLeftDash 13 | PrependRightToLeftOnlyip 14 | PrependLeftToRightOnlyip 15 | ) 16 | 17 | type IPv6NotationMode int 18 | 19 | const ( 20 | ArpaNotation IPv6NotationMode = iota 21 | FourHexsNotation 22 | ) 23 | 24 | // Config file 25 | type Config struct { 26 | Debug bool `toml:"debug"` 27 | Listen []*string `toml:"listen"` 28 | PerNetConfigs []*PerNetConfig `toml:"net"` 29 | PerIPv4NetConfigs []*PerNetConfig `toml:""` 30 | PerIPv6NetConfigs []*PerNetConfig `toml:""` 31 | PerHostConfigs map[string]string `toml:"host"` 32 | DefaultNSes []*string `toml:"ns"` 33 | OverrideVersionString string `toml:"version_string"` 34 | DefaultSOARecord *SOARecord `toml:"SOA"` 35 | DefaultTTL uint32 `toml:"default_ttl"` 36 | CompressDNSMessages bool `toml:"compress_dns_messages"` 37 | AllowVersionReporting bool `toml:"allow_version_reporting"` 38 | } 39 | 40 | type SOARecord struct { 41 | MName *string `toml:"MNAME"` 42 | RName *string `toml:"RNAME"` 43 | Serial uint32 `toml:"SERIAL"` 44 | Refresh uint32 `toml:"REFRESH"` 45 | Retry uint32 `toml:"RETRY"` 46 | Expire uint32 `toml:"EXPIRE"` 47 | TTL uint32 `toml:"TTL"` 48 | } 49 | 50 | type PerNetConfig struct { 51 | IPNetString *string `toml:"net"` 52 | IPNet *net.IPNet `toml:""` 53 | PtrGenerationModeString *string `toml:"mode"` 54 | PtrGenerationMode PtrGenerationMode `toml:""` 55 | IPv6NotationString *string `toml:"ipv6_notation"` 56 | IPv6NotationMode IPv6NotationMode `toml:""` 57 | Domain *string `toml:"domain"` 58 | DomainPrefix *string `toml:"domain_prefix"` 59 | TTL uint32 `toml:"ttl"` 60 | SOARecord *SOARecord `toml:"SOA"` 61 | } 62 | -------------------------------------------------------------------------------- /pkg/config/parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/jamesits/libiferr/exception" 5 | "log" 6 | "net" 7 | "slices" 8 | "strings" 9 | ) 10 | 11 | func (config *Config) SOARecordFillDefault(r *SOARecord, useDefaultRecord bool) { 12 | if useDefaultRecord { 13 | if len(*r.MName) == 0 { 14 | r.MName = config.DefaultSOARecord.MName 15 | } 16 | 17 | if len(*r.RName) == 0 { 18 | r.RName = config.DefaultSOARecord.RName 19 | } 20 | 21 | if r.Serial == 0 { 22 | r.Serial = config.DefaultSOARecord.Serial 23 | } 24 | 25 | if r.Refresh == 0 { 26 | r.Refresh = config.DefaultSOARecord.Refresh 27 | } 28 | 29 | if r.Retry == 0 { 30 | r.Retry = config.DefaultSOARecord.Retry 31 | } 32 | 33 | if r.Expire == 0 { 34 | r.Expire = config.DefaultSOARecord.Expire 35 | } 36 | 37 | if r.TTL == 0 { 38 | r.TTL = config.DefaultSOARecord.TTL 39 | } 40 | } else { 41 | if r.Serial == 0 { 42 | r.Serial = 114514 43 | } 44 | 45 | if r.Refresh == 0 { 46 | r.Refresh = 86400 47 | } 48 | 49 | if r.Retry == 0 { 50 | r.Retry = 7200 51 | } 52 | 53 | if r.Expire == 0 { 54 | r.Expire = 3600000 55 | } 56 | 57 | if r.TTL == 0 { 58 | r.TTL = 172800 59 | } 60 | } 61 | 62 | if len(*r.MName) == 0 { 63 | log.Fatalf("a SOA record is missing MName") 64 | } 65 | 66 | if len(*r.RName) == 0 { 67 | log.Fatalf("a SOA record is missing RName") 68 | } 69 | 70 | r.RName = ensureDotAtRight(r.RName) 71 | r.MName = ensureDotAtRight(r.MName) 72 | } 73 | 74 | // FixConfig fixes Config and fill in defaults 75 | func (config *Config) FixConfig() { 76 | var err error 77 | 78 | if config.DefaultTTL == 0 { 79 | config.DefaultTTL = 114514 80 | } 81 | 82 | for index, ns := range config.DefaultNSes { 83 | config.DefaultNSes[index] = ensureDotAtRight(ns) 84 | } 85 | 86 | if config.DefaultSOARecord == nil { 87 | config.DefaultSOARecord = new(SOARecord) 88 | } 89 | config.SOARecordFillDefault(config.DefaultSOARecord, false) 90 | 91 | fixedHosts := make([]*PerNetConfig, 0) 92 | for network, domain := range config.PerHostConfigs { 93 | netCIDR := "" 94 | for i := 0; i < len(network); i++ { 95 | switch network[i] { 96 | case '.': 97 | netCIDR = network + "/32" 98 | break 99 | case ':': 100 | netCIDR = network + "/128" 101 | break 102 | } 103 | } 104 | if netCIDR == "" { 105 | break 106 | } 107 | log.Printf("Loading host %s -> %s\n", netCIDR, domain) 108 | mode := "fixed" 109 | for _, d := range strings.Split(domain, ",") { 110 | thisHost := &PerNetConfig{ 111 | IPNetString: &netCIDR, 112 | PtrGenerationModeString: &mode, 113 | Domain: &d, 114 | } 115 | fixedHosts = append(fixedHosts, thisHost) 116 | } 117 | } 118 | config.PerNetConfigs = append(fixedHosts, config.PerNetConfigs...) 119 | 120 | // note that range is byVal so we use index here 121 | for _, currentConfig := range config.PerNetConfigs { 122 | // fill IPNet 123 | _, currentConfig.IPNet, err = net.ParseCIDR(*currentConfig.IPNetString) 124 | exception.HardFailWithReason("failed to parse CIDR", err) 125 | 126 | log.Printf("Loading network %s\n", currentConfig.IPNet.String()) 127 | 128 | // fill Mode 129 | if currentConfig.PtrGenerationModeString == nil { 130 | log.Fatalf("Missing PTR generation method") 131 | } 132 | switch strings.ToLower(*currentConfig.PtrGenerationModeString) { 133 | case "fixed": 134 | currentConfig.PtrGenerationMode = Fixed 135 | case "prefix_ltr": 136 | currentConfig.PtrGenerationMode = PrependLeftToRight 137 | case "prefix_rtl": 138 | currentConfig.PtrGenerationMode = PrependRightToLeft 139 | case "prefix_ltr_dash": 140 | currentConfig.PtrGenerationMode = PrependLeftToRightDash 141 | case "prefix_rtl_dash": 142 | currentConfig.PtrGenerationMode = PrependRightToLeftDash 143 | case "prefix_ltr_onlyip": 144 | currentConfig.PtrGenerationMode = PrependLeftToRightOnlyip 145 | case "prefix_rtl_onlyip": 146 | currentConfig.PtrGenerationMode = PrependRightToLeftOnlyip 147 | default: 148 | log.Fatalf("Unknown mode \"%s\"", *currentConfig.PtrGenerationModeString) 149 | } 150 | 151 | // fill IPv6Notation 152 | if currentConfig.IPv6NotationString == nil { 153 | currentConfig.IPv6NotationMode = ArpaNotation 154 | } else { 155 | switch strings.ToLower(*currentConfig.IPv6NotationString) { 156 | case "arpa": 157 | currentConfig.IPv6NotationMode = ArpaNotation 158 | case "four_hexs": 159 | currentConfig.IPv6NotationMode = FourHexsNotation 160 | default: 161 | log.Fatalf("Unknown ipv6_notation \"%s\"", *currentConfig.PtrGenerationModeString) 162 | } 163 | } 164 | 165 | // check domain 166 | currentConfig.Domain = ensureDotAtRight(currentConfig.Domain) 167 | currentConfig.Domain = ensureNoDotAtLeft(currentConfig.Domain) 168 | 169 | // fill TTL 170 | if currentConfig.TTL == 0 { 171 | currentConfig.TTL = config.DefaultTTL 172 | } 173 | 174 | // fill SOA 175 | if currentConfig.SOARecord == nil { 176 | currentConfig.SOARecord = config.DefaultSOARecord 177 | } else { 178 | config.SOARecordFillDefault(currentConfig.SOARecord, true) 179 | } 180 | 181 | // Add configuration to dedicated list 182 | if strings.Contains(*currentConfig.IPNetString, ":") { 183 | config.PerIPv6NetConfigs = append(config.PerIPv6NetConfigs, currentConfig) 184 | } else { 185 | config.PerIPv4NetConfigs = append(config.PerIPv4NetConfigs, currentConfig) 186 | } 187 | } 188 | 189 | slices.SortFunc(config.PerNetConfigs, subnetSortingFunc) 190 | slices.SortFunc(config.PerIPv4NetConfigs, subnetSortingFunc) 191 | slices.SortFunc(config.PerIPv6NetConfigs, subnetSortingFunc) 192 | } 193 | 194 | func subnetSortingFunc(a, b *PerNetConfig) int { 195 | var aOnes, _ = a.IPNet.Mask.Size() 196 | var bOnes, _ = b.IPNet.Mask.Size() 197 | 198 | if aOnes > bOnes { 199 | return -1 200 | } 201 | if aOnes < bOnes { 202 | return 1 203 | } 204 | return 0 205 | } 206 | -------------------------------------------------------------------------------- /pkg/config/utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | func ensureDotAtRight(s *string) *string { 6 | if !strings.HasSuffix(*s, ".") { 7 | ret := *s + "." 8 | return &ret 9 | } else { 10 | return s 11 | } 12 | } 13 | 14 | func ensureNoDotAtLeft(s *string) *string { 15 | if !strings.HasPrefix(*s, ".") { 16 | ret := strings.TrimLeft(*s, ".") 17 | return &ret 18 | } else { 19 | return s 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/jamesits/libiferr/exception" 5 | "github.com/jamesits/snd/pkg/config" 6 | "github.com/miekg/dns" 7 | "strings" 8 | ) 9 | 10 | type Handler struct { 11 | config *config.Config 12 | } 13 | 14 | func NewHandler(config *config.Config) *Handler { 15 | return &Handler{config: config} 16 | } 17 | 18 | func (handler *Handler) newDNSReplyMsg() *dns.Msg { 19 | msg := dns.Msg{} 20 | 21 | msg.Compress = handler.config.CompressDNSMessages 22 | 23 | // this is an authoritative DNS server 24 | msg.Authoritative = true 25 | msg.RecursionAvailable = false 26 | 27 | // DNSSEC disabled for now 28 | // TODO: fix DNSSEC 29 | msg.AuthenticatedData = false 30 | msg.CheckingDisabled = true 31 | 32 | return &msg 33 | } 34 | 35 | // send out the generated answer, and if the answer is not correct, send out a SERVFAIL 36 | func (handler *Handler) finishAnswer(w *dns.ResponseWriter, r *dns.Msg) { 37 | err := (*w).WriteMsg(r) 38 | if err != nil { 39 | exception.SoftFailWithReason("failed to send primary DNS answer", err) 40 | 41 | // if answer sanity check (miekg/dns automatically does handler) fails, reply with SERVFAIL 42 | msg := handler.newDNSReplyMsg() 43 | msg.SetReply(r) 44 | msg.Rcode = dns.RcodeServerFailure 45 | err = (*w).WriteMsg(msg) 46 | exception.SoftFailWithReason("failed to send secondary DNS answer", err) 47 | } 48 | } 49 | 50 | // ServeDNS TODO: force TCP for 1) clients which requests too fast; 2) non-existent answers 51 | // See: https://labs.apnic.net/?p=382 52 | func (handler *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { 53 | msg := handler.newDNSReplyMsg() 54 | msg.SetReply(r) 55 | 56 | // on function return, we send out the current answer 57 | defer handler.finishAnswer(&w, msg) 58 | 59 | // sanity check 60 | if len(r.Question) != 1 { 61 | msg.Rcode = dns.RcodeRefused 62 | return 63 | } 64 | 65 | switch r.Question[0].Qclass { 66 | case dns.ClassINET: 67 | switch r.Question[0].Qtype { 68 | case dns.TypeSOA: 69 | handleSOA(handler, r, msg) 70 | return 71 | 72 | case dns.TypeNS: 73 | handleNS(handler, r, msg) 74 | return 75 | 76 | case dns.TypePTR: 77 | handlePTR(handler, r, msg) 78 | return 79 | 80 | default: 81 | handleDefault(handler, r, msg) 82 | return 83 | } 84 | case dns.ClassCHAOS: 85 | switch r.Question[0].Qtype { 86 | case dns.TypeTXT: 87 | if strings.EqualFold(r.Question[0].Name, "version.bind.") { 88 | // we need to reply our software version 89 | // https://serverfault.com/questions/517087/dns-how-to-find-out-which-software-a-remote-dns-server-is-running 90 | handleTXTVersionRequest(handler, r, msg) 91 | } else { 92 | handleDefault(handler, r, msg) 93 | } 94 | return 95 | 96 | default: 97 | handleDefault(handler, r, msg) 98 | return 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler_default.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "log" 6 | ) 7 | 8 | // simply replies NOTIMPL 9 | func handleDefault(handler *Handler, _, msg *dns.Msg) { 10 | if handler.config.Debug { 11 | log.Printf("%d %s not implemented\n", msg.Question[0].Qtype, msg.Question[0].Name) 12 | } 13 | msg.Rcode = dns.RcodeNotImplemented 14 | } 15 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler_ns.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "log" 6 | ) 7 | 8 | func handleNS(handler *Handler, r, msg *dns.Msg) { 9 | if handler.config.Debug { 10 | log.Printf("NS %s\n", msg.Question[0].Name) 11 | } 12 | 13 | // TODO: check if domain exists 14 | // same for root zone 15 | for _, ns := range handler.config.DefaultNSes { 16 | msg.Answer = append(msg.Answer, &dns.NS{ 17 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: handler.config.DefaultSOARecord.TTL}, 18 | Ns: *ns, 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler_ptr.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jamesits/snd/pkg/config" 6 | "log" 7 | "net" 8 | "strings" 9 | 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | func handlePTR(handler *Handler, r, msg *dns.Msg) { 14 | msg.Question[0].Name = strings.ToLower(msg.Question[0].Name) 15 | nameBreakout := strings.Split(msg.Question[0].Name, ".") 16 | index := len(nameBreakout) - 1 17 | 18 | // sanity check 19 | if index < 3 || nameBreakout[index] != "" || nameBreakout[index-1] != "arpa" { 20 | if handler.config.Debug { 21 | log.Printf("PTR %s not rational\n", msg.Question[0].Name) 22 | } 23 | return 24 | } 25 | 26 | var split string 27 | 28 | // parse IP address out of the request 29 | index -= 3 30 | var b strings.Builder 31 | switch nameBreakout[index+1] { 32 | case "in-addr": // IPv4 33 | split = "." 34 | for ; index >= 0; index-- { 35 | b.WriteString(nameBreakout[index]) 36 | b.WriteString(split) 37 | } 38 | case "ip6": // IPv6 39 | split = ":" 40 | for i := 0; index >= 0; { 41 | b.WriteString(nameBreakout[index]) 42 | index-- 43 | i++ 44 | if i%4 == 0 { 45 | b.WriteString(split) 46 | } 47 | } 48 | default: 49 | if handler.config.Debug { 50 | log.Printf("PTR %s unable to parse IP address\n", msg.Question[0].Name) 51 | } 52 | return 53 | } 54 | 55 | ipaddr := net.ParseIP(strings.TrimRight(b.String(), split)) 56 | var lookupTable = &handler.config.PerIPv6NetConfigs 57 | if split == "." { 58 | ipaddr = ipaddr.To4() 59 | lookupTable = &handler.config.PerIPv4NetConfigs 60 | } 61 | 62 | // find a matching Config 63 | // TODO: optimize to less then O(n) 64 | found := false 65 | for _, netBlock := range *lookupTable { 66 | if netBlock.IPNet.Contains(ipaddr) { 67 | found = true 68 | 69 | // construct ptr 70 | var p strings.Builder 71 | if netBlock.DomainPrefix != nil { 72 | p.WriteString(*netBlock.DomainPrefix) 73 | } 74 | 75 | switch netBlock.PtrGenerationMode { 76 | case config.Fixed: 77 | p.WriteString(*netBlock.Domain) 78 | case config.PrependLeftToRight: 79 | p.WriteString(IPToArpaDomain(ipaddr, false, netBlock.IPv6NotationMode)) 80 | p.WriteString(".") 81 | p.WriteString(*netBlock.Domain) 82 | case config.PrependRightToLeft: 83 | p.WriteString(IPToArpaDomain(ipaddr, true, netBlock.IPv6NotationMode)) 84 | p.WriteString(".") 85 | p.WriteString(*netBlock.Domain) 86 | case config.PrependLeftToRightDash: 87 | IPGenerate := IPToArpaDomain(ipaddr, false, netBlock.IPv6NotationMode) 88 | p.WriteString(strings.Replace(IPGenerate, ".", "-", -1)) 89 | p.WriteString(".") 90 | p.WriteString(*netBlock.Domain) 91 | case config.PrependRightToLeftDash: 92 | IPGenerate := IPToArpaDomain(ipaddr, true, netBlock.IPv6NotationMode) 93 | p.WriteString(strings.Replace(IPGenerate, ".", "-", -1)) 94 | p.WriteString(".") 95 | p.WriteString(*netBlock.Domain) 96 | case config.PrependRightToLeftOnlyip: 97 | IPGenerate := IPToArpaDomain(ipaddr, true, netBlock.IPv6NotationMode) 98 | p.WriteString(strings.Replace(IPGenerate, ".", "", -1)) 99 | p.WriteString(".") 100 | p.WriteString(*netBlock.Domain) 101 | case config.PrependLeftToRightOnlyip: 102 | IPGenerate := IPToArpaDomain(ipaddr, false, netBlock.IPv6NotationMode) 103 | p.WriteString(strings.Replace(IPGenerate, ".", "", -1)) 104 | p.WriteString(".") 105 | p.WriteString(*netBlock.Domain) 106 | default: 107 | return 108 | } 109 | 110 | if handler.config.Debug { 111 | log.Printf("PTR %s => %s", ipaddr.String(), p.String()) 112 | } 113 | 114 | // generate an answer 115 | msg.Answer = append(msg.Answer, &dns.PTR{ 116 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: netBlock.TTL}, 117 | Ptr: p.String(), 118 | }) 119 | break 120 | } 121 | } 122 | 123 | if !found && handler.config.Debug { 124 | log.Printf("PTR %s unknown net", ipaddr.String()) 125 | } 126 | } 127 | 128 | func IPToArpaDomain(ip net.IP, reverse bool, ipv6ConversionMode config.IPv6NotationMode) string { 129 | ipv4 := ip.To4() 130 | var ret []string 131 | 132 | if ipv4 == nil { 133 | // ipv6 134 | // TODO: edge cases? 135 | for _, elem := range ip { 136 | s := fmt.Sprintf("%x", elem) // 2 characters per iteration 137 | if len(s) == 2 { 138 | ret = append(ret, s[0:1], s[1:2]) 139 | } else { 140 | ret = append(ret, "0", s[0:1]) 141 | } 142 | } 143 | } else { 144 | // ipv4 145 | for _, elem := range ipv4 { 146 | ret = append(ret, fmt.Sprintf("%d", elem)) 147 | } 148 | } 149 | 150 | switch ipv6ConversionMode { 151 | case config.ArpaNotation: 152 | break 153 | case config.FourHexsNotation: 154 | reverse = !reverse // in this mode, ret is processed in reverse, so we need to reverse it again before returning 155 | var ret2 []string 156 | for i := len(ret) - 1; i >= 0; i -= 4 { 157 | var b strings.Builder 158 | var isLeadingZero = true 159 | for j := 3; j >= 0; j-- { 160 | if i-j < 0 || i-j > len(ret) { 161 | log.Panicf("Assertion for IP length can be divided in 4 failed") 162 | } 163 | // noinspection GoNilness 164 | if isLeadingZero { 165 | if ret[i-j] != "0" { 166 | isLeadingZero = false 167 | b.WriteString(ret[i-j]) 168 | } 169 | } else { 170 | b.WriteString(ret[i-j]) 171 | } 172 | } 173 | if b.Len() == 0 { 174 | b.WriteString("0") 175 | } 176 | 177 | ret2 = append(ret2, b.String()) 178 | } 179 | 180 | ret = ret2 181 | default: 182 | break 183 | } 184 | 185 | if reverse { 186 | var ret2 []string 187 | for i := len(ret) - 1; i >= 0; i-- { 188 | ret2 = append(ret2, ret[i]) 189 | } 190 | 191 | ret = ret2 192 | } 193 | 194 | return strings.Join(ret, ".") 195 | } 196 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler_soa.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "log" 6 | ) 7 | 8 | func handleSOA(handler *Handler, r, msg *dns.Msg) { 9 | if handler.config.Debug { 10 | log.Printf("SOA %s\n", msg.Question[0].Name) 11 | } 12 | 13 | // TODO: check if domain exists, and use the actual SOA for that domain 14 | // if not our case, we should reply with authority section containing root zone 15 | // See: http://www-inf.int-evry.fr/~hennequi/CoursDNS/NOTES-COURS_eng/msg.html 16 | // Get root zone from https://www.internic.net/domain/named.root 17 | msg.Answer = append(msg.Answer, &dns.SOA{ 18 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: handler.config.DefaultSOARecord.TTL}, 19 | Ns: *handler.config.DefaultSOARecord.MName, 20 | Mbox: *handler.config.DefaultSOARecord.RName, 21 | Serial: handler.config.DefaultSOARecord.Serial, 22 | Refresh: handler.config.DefaultSOARecord.Refresh, 23 | Retry: handler.config.DefaultSOARecord.Retry, 24 | Expire: handler.config.DefaultSOARecord.Expire, 25 | Minttl: handler.config.DefaultSOARecord.TTL, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_handler_txt.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/jamesits/snd/pkg/version" 5 | "github.com/miekg/dns" 6 | "log" 7 | ) 8 | 9 | // replies a TXT record containing server name and version 10 | func handleTXTVersionRequest(handler *Handler, r, msg *dns.Msg) { 11 | if handler.config.Debug { 12 | log.Printf("TXT %s\n", msg.Question[0].Name) 13 | } 14 | 15 | if !handler.config.AllowVersionReporting { 16 | msg.Rcode = dns.RcodeRefused 17 | return 18 | } 19 | 20 | var versionString string 21 | if len(handler.config.OverrideVersionString) == 0 { 22 | versionString = version.GetVersionFullString() 23 | } else { 24 | versionString = handler.config.OverrideVersionString 25 | } 26 | 27 | msg.Answer = append(msg.Answer, &dns.TXT{ 28 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: handler.config.DefaultSOARecord.TTL}, 29 | Txt: []string{versionString}, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/dns_server/dns_server.go: -------------------------------------------------------------------------------- 1 | package dns_server 2 | 3 | import ( 4 | "github.com/jamesits/libiferr/exception" 5 | "github.com/jamesits/snd/pkg/config" 6 | "github.com/miekg/dns" 7 | "log" 8 | ) 9 | 10 | // ListenSync listens on a specific endpoint 11 | // proto can be "udp" or "tcp" 12 | // endpoint is "ip.address:port" 13 | func ListenSync(config *config.Config, proto, endpoint string) { 14 | log.Printf("Listening on %s %s", proto, endpoint) 15 | srv := &dns.Server{Addr: endpoint, Net: proto} 16 | srv.Handler = NewHandler(config) 17 | if err := srv.ListenAndServe(); err != nil { 18 | log.Printf("Failed to set %s listener %s\n", proto, endpoint) 19 | exception.HardFailWithReason("failed to enable listener", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | var version string 6 | var versionGitCommitHash string 7 | var versionCompileTime string 8 | var versionCompileHost string 9 | 10 | func GetVersionNumberString() string { 11 | return version 12 | } 13 | 14 | func GetVersionFullString() string { 15 | if len(versionCompileHost) == 0 { 16 | versionCompileHost = "localhost" 17 | } 18 | 19 | if len(versionGitCommitHash) == 0 { 20 | versionGitCommitHash = "UNKNOWN" 21 | } 22 | 23 | if len(versionCompileTime) == 0 { 24 | versionCompileTime = "UNKNOWN TIME" 25 | } 26 | 27 | return fmt.Sprintf("SND/%s (+https://github.com/Jamesits/SND; Compiled on %s for commit %s at %s)", GetVersionNumberString(), versionCompileHost, versionGitCommitHash, versionCompileTime) 28 | } 29 | --------------------------------------------------------------------------------