Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
671a23e6e1 | ||
|
|
5d4fa94286 | ||
|
|
683a2a69fa | ||
|
|
2983846f74 | ||
|
|
1a0cb1f301 | ||
|
|
3da28da9a2 | ||
|
|
43af39dfd7 | ||
|
|
25e6039bde | ||
|
|
d6063eb636 |
+36
-2
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version-file: 'go.mod'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Vet
|
- name: Vet
|
||||||
@@ -28,8 +28,42 @@ jobs:
|
|||||||
|
|
||||||
- name: Check internal package coverage
|
- name: Check internal package coverage
|
||||||
run: |
|
run: |
|
||||||
COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/hits/ExploreDNS/internal" | awk '{print $3}' | sed 's/%//' | awk '{sum+=$1; count++} END {if(count>0) print sum/count; else print 0}')
|
COVERAGE=$(go tool cover -func=coverage.out | grep "^gitea.hansenits.com.au/hits/ExploreDNS/internal" | awk '{print $3}' | sed 's/%//' | awk '{sum+=$1; count++} END {if(count>0) print sum/count; else print 0}')
|
||||||
echo "Average internal package coverage: ${COVERAGE}%"
|
echo "Average internal package coverage: ${COVERAGE}%"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.hansenits.com.au
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push CLI image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.cli
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-cli:latest
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-cli:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Build and push web image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.web
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-web:latest
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-web:${{ github.sha }}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/exploredns ./cmd/exploredns
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
COPY --from=builder /out/exploredns /usr/local/bin/exploredns
|
||||||
|
|
||||||
|
ENTRYPOINT ["exploredns"]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
COPY --from=builder /out/server /usr/local/bin/server
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["server"]
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
BINARY_NAME=exploredns
|
BINARY_NAME=exploredns
|
||||||
|
SERVER_BINARY_NAME=exploredns-server
|
||||||
BUILD_DIR=bin
|
BUILD_DIR=bin
|
||||||
GO=go
|
GO=go
|
||||||
GOFLAGS=-v
|
GOFLAGS=-v
|
||||||
|
|
||||||
.PHONY: build test lint clean cover
|
.PHONY: build build-server build-all test lint clean cover
|
||||||
|
|
||||||
build:
|
build:
|
||||||
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/exploredns
|
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/exploredns
|
||||||
|
|
||||||
|
build-server:
|
||||||
|
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(SERVER_BINARY_NAME) ./cmd/server
|
||||||
|
|
||||||
|
build-all: build build-server
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test -v -race -coverprofile=coverage.out ./...
|
$(GO) test -v -race -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ binary with no runtime dependencies.
|
|||||||
- **Fast mode** — shares glue across branches for speed; disable for independent
|
- **Fast mode** — shares glue across branches for speed; disable for independent
|
||||||
paths
|
paths
|
||||||
- **CNAME tracking** — follows CNAME chains and detects loops
|
- **CNAME tracking** — follows CNAME chains and detects loops
|
||||||
|
- **Web interface** — browser-based UI backed by an HTTP API server with
|
||||||
|
real-time Server-Sent Events progress streaming
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,12 +46,15 @@ Requires Go 1.21 or later.
|
|||||||
git clone https://gitea.hansenits.com.au/hits/ExploreDNS.git
|
git clone https://gitea.hansenits.com.au/hits/ExploreDNS.git
|
||||||
cd ExploreDNS
|
cd ExploreDNS
|
||||||
make build # produces bin/exploredns
|
make build # produces bin/exploredns
|
||||||
|
make build-server # produces bin/exploredns-server
|
||||||
|
make build-all # produces both binaries
|
||||||
```
|
```
|
||||||
|
|
||||||
### go install
|
### go install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install github.com/hits/ExploreDNS/cmd/exploredns@latest
|
go install gitea.hansenits.com.au/hits/ExploreDNS/cmd/exploredns@latest
|
||||||
|
go install gitea.hansenits.com.au/hits/ExploreDNS/cmd/server@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -138,7 +143,113 @@ Output Options:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Output Formats
|
## Web Interface
|
||||||
|
|
||||||
|
ExploreDNS ships a second binary — `exploredns-server` — that exposes a
|
||||||
|
browser-based UI and a JSON REST API backed by the same traversal engine as
|
||||||
|
the CLI.
|
||||||
|
|
||||||
|
### Starting the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Default: listen on :8080
|
||||||
|
./bin/exploredns-server
|
||||||
|
|
||||||
|
# Custom address
|
||||||
|
./bin/exploredns-server --addr :9090
|
||||||
|
./bin/exploredns-server --addr 127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Make:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build-server
|
||||||
|
./bin/exploredns-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8080` in your browser. The SPA lets you enter a domain,
|
||||||
|
choose a record type, and watch the traversal progress in real time. When the
|
||||||
|
traversal completes the full result tree is displayed in the browser.
|
||||||
|
|
||||||
|
### API endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `POST` | `/api/traverse` | Start an asynchronous traversal |
|
||||||
|
| `GET` | `/api/traverse/{id}` | Poll traversal status and results |
|
||||||
|
| `GET` | `/api/traverse/{id}/stream` | Server-Sent Events live progress stream |
|
||||||
|
| `GET` | `/api/health` | Health check — returns `{"status":"ok"}` |
|
||||||
|
|
||||||
|
#### POST /api/traverse
|
||||||
|
|
||||||
|
Request body (JSON):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"domain": "www.example.com",
|
||||||
|
"type": "A",
|
||||||
|
"all_roots": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`type` defaults to `"A"` if omitted. `all_roots` queries all 13 root server
|
||||||
|
sets in parallel (equivalent to `--all-root-servers` in the CLI).
|
||||||
|
|
||||||
|
Response (`202 Accepted`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"status": "running"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/traverse/{id}
|
||||||
|
|
||||||
|
Returns a snapshot of the job including the full result list once complete.
|
||||||
|
`status` is one of `running`, `complete`, or `error`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"status": "complete",
|
||||||
|
"domain": "www.example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"started_at": "2024-01-01T12:00:00Z",
|
||||||
|
"done_at": "2024-01-01T12:00:02Z",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"depth": 2,
|
||||||
|
"probability": 1.0,
|
||||||
|
"response_type": "Answer",
|
||||||
|
"server": "192.0.2.53:53",
|
||||||
|
"answers": ["www.example.com. 3600 IN A 93.184.216.34"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/traverse/{id}/stream
|
||||||
|
|
||||||
|
An [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
||||||
|
stream of `ProgressEvent` objects, one per `data:` message. Past events
|
||||||
|
recorded before the client connected are replayed immediately, then live events
|
||||||
|
follow. The stream ends with `event: done`.
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"stage":"start","depth":1,"name":"www.example.com","qtype":"A","bailiwick":"com"}
|
||||||
|
|
||||||
|
data: {"stage":"complete","depth":1,"name":"www.example.com","qtype":"A","server":"192.0.2.53:53","bailiwick":"com"}
|
||||||
|
|
||||||
|
event: done
|
||||||
|
data: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Completed jobs are kept in memory for one hour before being purged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Text (default)
|
### Text (default)
|
||||||
|
|
||||||
@@ -173,12 +284,14 @@ responding server, the response type, and the decoded DNS records.
|
|||||||
|
|
||||||
```
|
```
|
||||||
cmd/exploredns/ CLI entry point and flag parsing
|
cmd/exploredns/ CLI entry point and flag parsing
|
||||||
|
cmd/server/ HTTP API server entry point
|
||||||
internal/config/ Configuration types, validation, and usage text
|
internal/config/ Configuration types, validation, and usage text
|
||||||
internal/dns/ DNS query layer, root discovery, transport
|
internal/dns/ DNS query layer, root discovery, transport
|
||||||
internal/traverse/ Core traversal engine, referral resolution, caching
|
internal/traverse/ Core traversal engine, referral resolution, caching
|
||||||
internal/fingerprint/ DNS server version fingerprinting (version.bind CHAOS)
|
internal/fingerprint/ DNS server version fingerprinting (version.bind CHAOS)
|
||||||
internal/output/ Result formatting — text tree and JSON renderers
|
internal/output/ Result formatting — text tree and JSON renderers
|
||||||
internal/integration/ End-to-end integration tests
|
internal/integration/ End-to-end integration tests
|
||||||
|
web/api/ HTTP handler, job store, SSE streaming, static assets
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -186,7 +299,9 @@ internal/integration/ End-to-end integration tests
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make build # compile binary to bin/exploredns
|
make build # compile CLI binary to bin/exploredns
|
||||||
|
make build-server # compile server binary to bin/exploredns-server
|
||||||
|
make build-all # compile both binaries
|
||||||
make test # run all unit and integration tests
|
make test # run all unit and integration tests
|
||||||
make lint # run go vet
|
make lint # run go vet
|
||||||
make clean # remove build artefacts
|
make clean # remove build artefacts
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
+10
-4
@@ -7,10 +7,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/config"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/config"
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/output"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/output"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -21,6 +21,7 @@ func main() {
|
|||||||
allRootServers := flag.Bool("all-root-servers", cfg.AllRootServers, "Use all 13 root servers")
|
allRootServers := flag.Bool("all-root-servers", cfg.AllRootServers, "Use all 13 root servers")
|
||||||
rootAAAA := flag.Bool("root-aaaa", cfg.RootAAAA, "Include IPv6 root addresses")
|
rootAAAA := flag.Bool("root-aaaa", cfg.RootAAAA, "Include IPv6 root addresses")
|
||||||
followAAAA := flag.Bool("follow-aaaa", cfg.FollowAAAA, "Only follow AAAA for referrals")
|
followAAAA := flag.Bool("follow-aaaa", cfg.FollowAAAA, "Only follow AAAA for referrals")
|
||||||
|
dnsUpstream := flag.String("dns-upstream", cfg.DNSUpstream, "Upstream resolver for root discovery (e.g. 8.8.8.8:53, default: system)")
|
||||||
udpSize := flag.Int("udp-size", cfg.UDPSize, "EDNS0 buffer size (512-4096)")
|
udpSize := flag.Int("udp-size", cfg.UDPSize, "EDNS0 buffer size (512-4096)")
|
||||||
allowTCP := flag.Bool("allow-tcp", cfg.AllowTCP, "TCP fallback on truncation")
|
allowTCP := flag.Bool("allow-tcp", cfg.AllowTCP, "TCP fallback on truncation")
|
||||||
alwaysTCP := flag.Bool("always-tcp", cfg.AlwaysTCP, "Always use TCP")
|
alwaysTCP := flag.Bool("always-tcp", cfg.AlwaysTCP, "Always use TCP")
|
||||||
@@ -71,6 +72,7 @@ func main() {
|
|||||||
cfg.AllRootServers = *allRootServers
|
cfg.AllRootServers = *allRootServers
|
||||||
cfg.RootAAAA = *rootAAAA
|
cfg.RootAAAA = *rootAAAA
|
||||||
cfg.FollowAAAA = *followAAAA
|
cfg.FollowAAAA = *followAAAA
|
||||||
|
cfg.DNSUpstream = *dnsUpstream
|
||||||
cfg.UDPSize = *udpSize
|
cfg.UDPSize = *udpSize
|
||||||
cfg.AllowTCP = *allowTCP
|
cfg.AllowTCP = *allowTCP
|
||||||
cfg.AlwaysTCP = *alwaysTCP
|
cfg.AlwaysTCP = *alwaysTCP
|
||||||
@@ -157,6 +159,7 @@ func main() {
|
|||||||
IncludeAAAA: cfg.RootAAAA,
|
IncludeAAAA: cfg.RootAAAA,
|
||||||
Server: rootServerAddr,
|
Server: rootServerAddr,
|
||||||
AllRoots: cfg.AllRootServers,
|
AllRoots: cfg.AllRootServers,
|
||||||
|
Resolver: cfg.DNSUpstream,
|
||||||
}
|
}
|
||||||
|
|
||||||
traverserConfig := &traverse.TraverserConfig{
|
traverserConfig := &traverse.TraverserConfig{
|
||||||
@@ -179,6 +182,9 @@ func main() {
|
|||||||
fmt.Fprintf(os.Stderr, " Allow TCP: %v\n", cfg.AllowTCP)
|
fmt.Fprintf(os.Stderr, " Allow TCP: %v\n", cfg.AllowTCP)
|
||||||
fmt.Fprintf(os.Stderr, " Always TCP: %v\n", cfg.AlwaysTCP)
|
fmt.Fprintf(os.Stderr, " Always TCP: %v\n", cfg.AlwaysTCP)
|
||||||
fmt.Fprintf(os.Stderr, " Fast Mode: %v\n", cfg.Fast)
|
fmt.Fprintf(os.Stderr, " Fast Mode: %v\n", cfg.Fast)
|
||||||
|
if cfg.DNSUpstream != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, " DNS Upstream: %s\n", cfg.DNSUpstream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Quiet && !*jsonOutput {
|
if !cfg.Quiet && !*jsonOutput {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/web/api"
|
"gitea.hansenits.com.au/hits/ExploreDNS/web/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/hits/ExploreDNS
|
module gitea.hansenits.com.au/hits/ExploreDNS
|
||||||
|
|
||||||
go 1.25.6
|
go 1.25.6
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ type Config struct {
|
|||||||
AllRootServers bool
|
AllRootServers bool
|
||||||
RootAAAA bool
|
RootAAAA bool
|
||||||
FollowAAAA bool
|
FollowAAAA bool
|
||||||
|
// DNSUpstream is the upstream resolver used for root server discovery.
|
||||||
|
// Format: "host:port" (e.g. "8.8.8.8:53"). Empty means use the system resolver.
|
||||||
|
DNSUpstream string
|
||||||
UDPSize int
|
UDPSize int
|
||||||
AllowTCP bool
|
AllowTCP bool
|
||||||
AlwaysTCP bool
|
AlwaysTCP bool
|
||||||
@@ -127,6 +130,12 @@ func (c *Config) Validate() error {
|
|||||||
return ErrAlwaysTCPRequiresTCP
|
return ErrAlwaysTCPRequiresTCP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.DNSUpstream != "" {
|
||||||
|
if _, _, err := net.SplitHostPort(c.DNSUpstream); err != nil {
|
||||||
|
return fmt.Errorf("--dns-upstream %q is not a valid host:port address", c.DNSUpstream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +182,7 @@ func PrintUsage() {
|
|||||||
{"--all-root-servers", "Use all 13 root servers"},
|
{"--all-root-servers", "Use all 13 root servers"},
|
||||||
{"--root-aaaa", "Include IPv6 root addresses"},
|
{"--root-aaaa", "Include IPv6 root addresses"},
|
||||||
{"--follow-aaaa", "Only follow AAAA for referrals"},
|
{"--follow-aaaa", "Only follow AAAA for referrals"},
|
||||||
|
{"--dns-upstream", "Upstream resolver for root discovery (default: system)"},
|
||||||
},
|
},
|
||||||
"Transport Options": {
|
"Transport Options": {
|
||||||
{"--udp-size", "EDNS0 buffer size (default 2048)"},
|
{"--udp-size", "EDNS0 buffer size (default 2048)"},
|
||||||
@@ -220,6 +230,7 @@ func DefaultConfig() *Config {
|
|||||||
AllRootServers: false,
|
AllRootServers: false,
|
||||||
RootAAAA: false,
|
RootAAAA: false,
|
||||||
FollowAAAA: false,
|
FollowAAAA: false,
|
||||||
|
DNSUpstream: "",
|
||||||
UDPSize: 2048,
|
UDPSize: 2048,
|
||||||
AllowTCP: true,
|
AllowTCP: true,
|
||||||
AlwaysTCP: false,
|
AlwaysTCP: false,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -234,12 +233,5 @@ func FormatRecord(rr dns.RR) string {
|
|||||||
if rr == nil {
|
if rr == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
header := rr.Header()
|
return rr.String()
|
||||||
return fmt.Sprintf("%s %d %s %s %s",
|
|
||||||
header.Name,
|
|
||||||
header.Ttl,
|
|
||||||
dns.ClassToString[header.Class],
|
|
||||||
QNameType(header.Rrtype),
|
|
||||||
rr.String(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
// RootHints contains the 13 IANA root name servers with their well-known
|
||||||
|
// IPv4 and IPv6 addresses as published at https://www.iana.org/domains/root/servers.
|
||||||
|
// These addresses change very rarely and are safe to embed as application constants.
|
||||||
|
var RootHints = []RootServer{
|
||||||
|
{
|
||||||
|
Name: "a.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("198.41.0.4")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:503:ba3e::2:30")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "b.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("170.247.170.2")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2801:1b8:10::b")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "c.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.33.4.12")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:2::c")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "d.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("199.7.91.13")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:2d::d")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "e.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.203.230.10")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:a8::e")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "f.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.5.5.241")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:2f::f")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "g.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.112.36.4")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:12::d0d")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "h.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("198.97.190.53")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:1::53")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "i.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.36.148.17")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:7fe::53")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "j.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("192.58.128.30")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:503:c27::2:30")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "k.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("193.0.14.129")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:7fd::1")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "l.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("199.7.83.42")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:500:9f::42")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "m.root-servers.net.",
|
||||||
|
IPv4: []net.IP{net.ParseIP("202.12.27.33")},
|
||||||
|
IPv6: []net.IP{net.ParseIP("2001:dc3::35")},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRootHintsCount(t *testing.T) {
|
||||||
|
if len(RootHints) != 13 {
|
||||||
|
t.Errorf("expected 13 root hints, got %d", len(RootHints))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsNames(t *testing.T) {
|
||||||
|
wantNames := []string{
|
||||||
|
"a.root-servers.net.",
|
||||||
|
"b.root-servers.net.",
|
||||||
|
"c.root-servers.net.",
|
||||||
|
"d.root-servers.net.",
|
||||||
|
"e.root-servers.net.",
|
||||||
|
"f.root-servers.net.",
|
||||||
|
"g.root-servers.net.",
|
||||||
|
"h.root-servers.net.",
|
||||||
|
"i.root-servers.net.",
|
||||||
|
"j.root-servers.net.",
|
||||||
|
"k.root-servers.net.",
|
||||||
|
"l.root-servers.net.",
|
||||||
|
"m.root-servers.net.",
|
||||||
|
}
|
||||||
|
for i, rs := range RootHints {
|
||||||
|
if rs.Name != wantNames[i] {
|
||||||
|
t.Errorf("RootHints[%d].Name = %q, want %q", i, rs.Name, wantNames[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsHaveIPv4(t *testing.T) {
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
if len(rs.IPv4) == 0 {
|
||||||
|
t.Errorf("root server %q has no IPv4 address", rs.Name)
|
||||||
|
}
|
||||||
|
for _, ip := range rs.IPv4 {
|
||||||
|
if ip.To4() == nil {
|
||||||
|
t.Errorf("root server %q: expected IPv4, got %v", rs.Name, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsHaveIPv6(t *testing.T) {
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
if len(rs.IPv6) == 0 {
|
||||||
|
t.Errorf("root server %q has no IPv6 address", rs.Name)
|
||||||
|
}
|
||||||
|
for _, ip := range rs.IPv6 {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
t.Errorf("root server %q: expected IPv6, got IPv4-mappable %v", rs.Name, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsAllIPsIPv4Only(t *testing.T) {
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
ips := rs.AllIPs(false)
|
||||||
|
if len(ips) != len(rs.IPv4) {
|
||||||
|
t.Errorf("root server %q: AllIPs(false) = %d, want %d", rs.Name, len(ips), len(rs.IPv4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsAllIPsBoth(t *testing.T) {
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
ips := rs.AllIPs(true)
|
||||||
|
want := len(rs.IPv4) + len(rs.IPv6)
|
||||||
|
if len(ips) != want {
|
||||||
|
t.Errorf("root server %q: AllIPs(true) = %d, want %d", rs.Name, len(ips), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsNoParseFail(t *testing.T) {
|
||||||
|
// Ensure none of the IPs failed to parse (net.ParseIP returns nil on failure).
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
for _, ip := range rs.IPv4 {
|
||||||
|
if ip == nil {
|
||||||
|
t.Errorf("root server %q has nil IPv4 (parse failed)", rs.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ip := range rs.IPv6 {
|
||||||
|
if ip == nil {
|
||||||
|
t.Errorf("root server %q has nil IPv6 (parse failed)", rs.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHintsKnownAddress(t *testing.T) {
|
||||||
|
// Spot-check a.root-servers.net. which has been stable for decades.
|
||||||
|
for _, rs := range RootHints {
|
||||||
|
if rs.Name == "a.root-servers.net." {
|
||||||
|
want := net.ParseIP("198.41.0.4")
|
||||||
|
if !rs.IPv4[0].Equal(want) {
|
||||||
|
t.Errorf("a.root-servers.net. IPv4 = %v, want %v", rs.IPv4[0], want)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("a.root-servers.net. not found in RootHints")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverFromConfigExplicit(t *testing.T) {
|
||||||
|
cfg := &RootDiscoveryConfig{Resolver: "8.8.8.8:53"}
|
||||||
|
got := resolverFromConfig(cfg)
|
||||||
|
if got != "8.8.8.8:53" {
|
||||||
|
t.Errorf("resolverFromConfig = %q, want 8.8.8.8:53", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverFromConfigEmpty(t *testing.T) {
|
||||||
|
cfg := &RootDiscoveryConfig{}
|
||||||
|
got := resolverFromConfig(cfg)
|
||||||
|
// Should return the system resolver; just check it's non-empty and contains a port.
|
||||||
|
if got == "" {
|
||||||
|
t.Error("resolverFromConfig with empty Resolver returned empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverFromConfigNil(t *testing.T) {
|
||||||
|
got := resolverFromConfig(nil)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("resolverFromConfig(nil) returned empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemResolverNonEmpty(t *testing.T) {
|
||||||
|
got := systemResolver()
|
||||||
|
if got == "" {
|
||||||
|
t.Error("systemResolver() returned empty string")
|
||||||
|
}
|
||||||
|
// Must contain a colon (host:port format).
|
||||||
|
host, port, err := splitHostPort(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("systemResolver() = %q: not a valid host:port: %v", got, err)
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
t.Errorf("systemResolver() host is empty in %q", got)
|
||||||
|
}
|
||||||
|
if port == "" {
|
||||||
|
t.Errorf("systemResolver() port is empty in %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitHostPort is a thin wrapper around net.SplitHostPort for test use.
|
||||||
|
func splitHostPort(addr string) (host, port string, err error) {
|
||||||
|
return net.SplitHostPort(addr)
|
||||||
|
}
|
||||||
@@ -217,7 +217,7 @@ func TestResolveRootServerDirect(t *testing.T) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
servers, err := resolveRootServer(ctx, "a.root-servers.net.", false)
|
servers, err := resolveRootServer(ctx, systemResolver(), "a.root-servers.net.", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("skipping (no local DNS): %v", err)
|
t.Skipf("skipping (no local DNS): %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-20
@@ -26,7 +26,13 @@ func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RootDiscoveryConfig struct {
|
type RootDiscoveryConfig struct {
|
||||||
|
// Server overrides which root server to use as the traversal starting point.
|
||||||
|
// When empty, a root server is discovered via the upstream resolver.
|
||||||
Server string
|
Server string
|
||||||
|
// Resolver is the upstream DNS resolver used to resolve root server names.
|
||||||
|
// When empty, the system resolver from /etc/resolv.conf is used.
|
||||||
|
// Format: "host:port" (e.g. "8.8.8.8:53" or "1.1.1.1:53").
|
||||||
|
Resolver string
|
||||||
AllRoots bool
|
AllRoots bool
|
||||||
IncludeAAAA bool
|
IncludeAAAA bool
|
||||||
}
|
}
|
||||||
@@ -43,20 +49,44 @@ func DiscoverRoots(ctx context.Context, cfg *RootDiscoveryConfig) ([]RootServer,
|
|||||||
cfg = DefaultRootDiscoveryConfig()
|
cfg = DefaultRootDiscoveryConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolver := resolverFromConfig(cfg)
|
||||||
|
|
||||||
if cfg.Server != "" {
|
if cfg.Server != "" {
|
||||||
return discoverRootOverride(ctx, cfg.Server, cfg.IncludeAAAA)
|
return discoverRootOverride(ctx, resolver, cfg.Server, cfg.IncludeAAAA)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.AllRoots {
|
if cfg.AllRoots {
|
||||||
return discoverAllRoots(ctx, cfg.IncludeAAAA)
|
servers, err := discoverAllRoots(ctx, resolver, cfg.IncludeAAAA)
|
||||||
|
if err != nil {
|
||||||
|
return filterHints(RootHints, cfg.IncludeAAAA), nil
|
||||||
|
}
|
||||||
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return discoverSingleRoot(ctx, cfg.IncludeAAAA)
|
servers, err := discoverSingleRoot(ctx, resolver, cfg.IncludeAAAA)
|
||||||
|
if err != nil {
|
||||||
|
hints := filterHints(RootHints, cfg.IncludeAAAA)
|
||||||
|
if len(hints) > 0 {
|
||||||
|
return hints[:1], nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func discoverRootOverride(ctx context.Context, server string, includeAAAA bool) ([]RootServer, error) {
|
// filterHints returns a copy of hints with IPv6 addresses stripped when includeAAAA is false.
|
||||||
resolver := systemResolver()
|
func filterHints(hints []RootServer, includeAAAA bool) []RootServer {
|
||||||
|
out := make([]RootServer, len(hints))
|
||||||
|
for i, h := range hints {
|
||||||
|
out[i] = RootServer{Name: h.Name, IPv4: h.IPv4}
|
||||||
|
if includeAAAA {
|
||||||
|
out[i].IPv6 = h.IPv6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverRootOverride(ctx context.Context, resolver, server string, includeAAAA bool) ([]RootServer, error) {
|
||||||
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query root NS records: %w", err)
|
return nil, fmt.Errorf("query root NS records: %w", err)
|
||||||
@@ -70,16 +100,14 @@ func discoverRootOverride(ctx context.Context, server string, includeAAAA bool)
|
|||||||
normalized := normalizeServerName(server)
|
normalized := normalizeServerName(server)
|
||||||
for _, name := range nsSet {
|
for _, name := range nsSet {
|
||||||
if normalizeServerName(name) == normalized {
|
if normalizeServerName(name) == normalized {
|
||||||
return resolveRootServer(ctx, name, includeAAAA)
|
return resolveRootServer(ctx, resolver, name, includeAAAA)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveRootServer(ctx, server, includeAAAA)
|
return resolveRootServer(ctx, resolver, server, includeAAAA)
|
||||||
}
|
}
|
||||||
|
|
||||||
func discoverSingleRoot(ctx context.Context, includeAAAA bool) ([]RootServer, error) {
|
func discoverSingleRoot(ctx context.Context, resolver string, includeAAAA bool) ([]RootServer, error) {
|
||||||
resolver := systemResolver()
|
|
||||||
|
|
||||||
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query root NS records: %w", err)
|
return nil, fmt.Errorf("query root NS records: %w", err)
|
||||||
@@ -95,12 +123,10 @@ func discoverSingleRoot(ctx context.Context, includeAAAA bool) ([]RootServer, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
pick := nsSet[0]
|
pick := nsSet[0]
|
||||||
return resolveRootServer(ctx, pick, includeAAAA)
|
return resolveRootServer(ctx, resolver, pick, includeAAAA)
|
||||||
}
|
}
|
||||||
|
|
||||||
func discoverAllRoots(ctx context.Context, includeAAAA bool) ([]RootServer, error) {
|
func discoverAllRoots(ctx context.Context, resolver string, includeAAAA bool) ([]RootServer, error) {
|
||||||
resolver := systemResolver()
|
|
||||||
|
|
||||||
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
nsMsg, err := queryResolver(ctx, resolver, ".", dns.TypeNS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query root NS records: %w", err)
|
return nil, fmt.Errorf("query root NS records: %w", err)
|
||||||
@@ -117,7 +143,7 @@ func discoverAllRoots(ctx context.Context, includeAAAA bool) ([]RootServer, erro
|
|||||||
|
|
||||||
var servers []RootServer
|
var servers []RootServer
|
||||||
for _, name := range nsSet {
|
for _, name := range nsSet {
|
||||||
resolved, err := resolveRootServer(ctx, name, includeAAAA)
|
resolved, err := resolveRootServer(ctx, resolver, name, includeAAAA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
servers = append(servers, RootServer{Name: name})
|
servers = append(servers, RootServer{Name: name})
|
||||||
continue
|
continue
|
||||||
@@ -132,9 +158,7 @@ func discoverAllRoots(ctx context.Context, includeAAAA bool) ([]RootServer, erro
|
|||||||
return servers, nil
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveRootServer(ctx context.Context, name string, includeAAAA bool) ([]RootServer, error) {
|
func resolveRootServer(ctx context.Context, resolver, name string, includeAAAA bool) ([]RootServer, error) {
|
||||||
resolver := systemResolver()
|
|
||||||
|
|
||||||
var ipv4 []net.IP
|
var ipv4 []net.IP
|
||||||
aMsg, err := queryResolver(ctx, resolver, name, dns.TypeA)
|
aMsg, err := queryResolver(ctx, resolver, name, dns.TypeA)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -156,15 +180,32 @@ func resolveRootServer(ctx context.Context, name string, includeAAAA bool) ([]Ro
|
|||||||
return []RootServer{{Name: name, IPv4: ipv4, IPv6: ipv6}}, nil
|
return []RootServer{{Name: name, IPv4: ipv4, IPv6: ipv6}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolverFromConfig returns the upstream DNS resolver address to use.
|
||||||
|
// If cfg.Resolver is set, it is used directly. Otherwise the system resolver
|
||||||
|
// is read from /etc/resolv.conf. Falls back to 127.0.0.1:53 if neither is available.
|
||||||
|
func resolverFromConfig(cfg *RootDiscoveryConfig) string {
|
||||||
|
if cfg != nil && cfg.Resolver != "" {
|
||||||
|
return cfg.Resolver
|
||||||
|
}
|
||||||
|
return systemResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemResolver returns the first nameserver from the system DNS configuration.
|
||||||
|
// This is Unix-only: it reads /etc/resolv.conf, which does not exist on Windows.
|
||||||
|
// On Windows (or any system without /etc/resolv.conf) the fallback 127.0.0.1:53 applies.
|
||||||
func systemResolver() string {
|
func systemResolver() string {
|
||||||
|
cc, err := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||||
|
if err != nil || len(cc.Servers) == 0 {
|
||||||
return "127.0.0.1:53"
|
return "127.0.0.1:53"
|
||||||
|
}
|
||||||
|
return net.JoinHostPort(cc.Servers[0], cc.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryResolver(ctx context.Context, resolverAddr, name string, qtype uint16) (*dns.Msg, error) {
|
func queryResolver(ctx context.Context, resolverAddr, name string, qtype uint16) (*dns.Msg, error) {
|
||||||
c := &dns.Client{
|
c := &dns.Client{
|
||||||
Net: "udp",
|
Net: "udp",
|
||||||
ReadTimeout: 5,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 5,
|
WriteTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
if deadline, ok := ctx.Deadline(); ok {
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
c.ReadTimeout = time.Until(deadline)
|
c.ReadTimeout = time.Until(deadline)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dnsinternal "github.com/hits/ExploreDNS/internal/dns"
|
dnsinternal "gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Format int
|
type Format int
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type jsonFormatter struct {
|
type jsonFormatter struct {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/fingerprint"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/fingerprint"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Config, formatter Formatter, domain string) ([]traverse.TraversalResult, error) {
|
func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Config, formatter Formatter, domain string) ([]traverse.TraversalResult, error) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +144,8 @@ func summaryTypeLabel(respType string) string {
|
|||||||
return "query type not implemented by server"
|
return "query type not implemented by server"
|
||||||
case "cname_loop":
|
case "cname_loop":
|
||||||
return "resulted in a CNAME loop"
|
return "resulted in a CNAME loop"
|
||||||
|
case "ns_error":
|
||||||
|
return "nameserver lookup failed"
|
||||||
case "error":
|
case "error":
|
||||||
return "resulted in an error"
|
return "resulted in an error"
|
||||||
case "referral":
|
case "referral":
|
||||||
@@ -194,3 +196,54 @@ func containsString(items []string, target string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeduplicateResults collapses terminal results that represent the same
|
||||||
|
// outcome from the same server into a single entry with summed probability.
|
||||||
|
// This prevents the same nameserver failure (or answer) from appearing once
|
||||||
|
// per delegation path when several parent servers all refer to the same child.
|
||||||
|
func DeduplicateResults(results []traverse.TraversalResult) []traverse.TraversalResult {
|
||||||
|
type entry struct {
|
||||||
|
result traverse.TraversalResult
|
||||||
|
prob float64
|
||||||
|
}
|
||||||
|
keys := make(map[string]*entry)
|
||||||
|
var order []string
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Response == nil || r.Referral == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := resultDeduplicationKey(r)
|
||||||
|
if e, ok := keys[key]; ok {
|
||||||
|
e.prob += r.Referral.Prob
|
||||||
|
} else {
|
||||||
|
keys[key] = &entry{result: r, prob: r.Referral.Prob}
|
||||||
|
order = append(order, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped := make([]traverse.TraversalResult, 0, len(order))
|
||||||
|
for _, key := range order {
|
||||||
|
e := keys[key]
|
||||||
|
refCopy := *e.result.Referral
|
||||||
|
refCopy.Prob = e.prob
|
||||||
|
deduped = append(deduped, traverse.TraversalResult{
|
||||||
|
Referral: &refCopy,
|
||||||
|
Response: e.result.Response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return deduped
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultDeduplicationKey(r traverse.TraversalResult) string {
|
||||||
|
bailiwick := strings.TrimSuffix(r.Referral.Bailiwick, ".")
|
||||||
|
switch r.Response.Type {
|
||||||
|
case traverse.RespAnswer:
|
||||||
|
key, _ := answerKey(r.Response)
|
||||||
|
return "answer:" + bailiwick + ":" + key
|
||||||
|
case traverse.RespNSResolutionFailed:
|
||||||
|
return "ns_error:" + r.Response.ErrorMessage
|
||||||
|
default:
|
||||||
|
return r.Response.Type.String() + ":" + bailiwick + ":" + r.Response.ErrorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ func TestSummaryTypeLabelAllTypes(t *testing.T) {
|
|||||||
"refused": "query refused by server",
|
"refused": "query refused by server",
|
||||||
"notimp": "query type not implemented by server",
|
"notimp": "query type not implemented by server",
|
||||||
"cname_loop": "resulted in a CNAME loop",
|
"cname_loop": "resulted in a CNAME loop",
|
||||||
|
"ns_error": "nameserver lookup failed",
|
||||||
"error": "resulted in an error",
|
"error": "resulted in an error",
|
||||||
"referral": "resulted in a referral",
|
"referral": "resulted in a referral",
|
||||||
"unknown_type": "unknown_type",
|
"unknown_type": "unknown_type",
|
||||||
|
|||||||
+15
-7
@@ -6,8 +6,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type textFormatter struct {
|
type textFormatter struct {
|
||||||
@@ -120,7 +120,8 @@ func (f *textFormatter) writeResults(results []traverse.TraversalResult) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal := terminalResults(results)
|
terminal := terminalResults(results)
|
||||||
for _, result := range terminal {
|
deduped := DeduplicateResults(terminal)
|
||||||
|
for _, result := range deduped {
|
||||||
prefix := strings.Repeat(" ", result.Referral.Depth+1)
|
prefix := strings.Repeat(" ", result.Referral.Depth+1)
|
||||||
line := prefix + f.formatResultLine(result)
|
line := prefix + f.formatResultLine(result)
|
||||||
if _, err := fmt.Fprintln(f.w, line); err != nil {
|
if _, err := fmt.Fprintln(f.w, line); err != nil {
|
||||||
@@ -196,14 +197,15 @@ func (f *textFormatter) formatResultLine(result traverse.TraversalResult) string
|
|||||||
prob := formatProbability(result.Referral.Prob)
|
prob := formatProbability(result.Referral.Prob)
|
||||||
switch result.Response.Type {
|
switch result.Response.Type {
|
||||||
case traverse.RespAnswer:
|
case traverse.RespAnswer:
|
||||||
key, rrs := answerKey(result.Response)
|
key, _ := answerKey(result.Response)
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return fmt.Sprintf("%s resulted in answer", prob)
|
return fmt.Sprintf("%s resulted in answer", prob)
|
||||||
}
|
}
|
||||||
if len(rrs) == 1 {
|
nsLabel := ""
|
||||||
return f.colorize(fmt.Sprintf("%s answered with %s", prob, rrs[0]), colorGreen)
|
if result.Referral.Bailiwick != "" && result.Referral.Bailiwick != "." {
|
||||||
|
nsLabel = trimDomain(result.Referral.Bailiwick) + " "
|
||||||
}
|
}
|
||||||
return f.colorize(fmt.Sprintf("%s answered with %s", prob, strings.Join(rrs, " / ")), colorGreen)
|
return f.colorize(fmt.Sprintf("%s %sanswered with %s", prob, nsLabel, key), colorGreen)
|
||||||
case traverse.RespNODATA:
|
case traverse.RespNODATA:
|
||||||
return fmt.Sprintf("%s found no such record", prob)
|
return fmt.Sprintf("%s found no such record", prob)
|
||||||
case traverse.RespNXDOMAIN:
|
case traverse.RespNXDOMAIN:
|
||||||
@@ -220,6 +222,12 @@ func (f *textFormatter) formatResultLine(result traverse.TraversalResult) string
|
|||||||
msg = result.Response.ErrorMessage
|
msg = result.Response.ErrorMessage
|
||||||
}
|
}
|
||||||
return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorRed)
|
return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorRed)
|
||||||
|
case traverse.RespNSResolutionFailed:
|
||||||
|
msg := "nameserver lookup failed"
|
||||||
|
if result.Response.ErrorMessage != "" {
|
||||||
|
msg = result.Response.ErrorMessage
|
||||||
|
}
|
||||||
|
return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorYellow)
|
||||||
case traverse.RespError:
|
case traverse.RespError:
|
||||||
msg := "resulted in an error"
|
msg := "resulted in an error"
|
||||||
if result.Response.ErrorMessage != "" {
|
if result.Response.ErrorMessage != "" {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,6 +121,7 @@ Answers: []miekgdns.RR{
|
|||||||
{"refused", traverse.RespREFUSED, nil, ""},
|
{"refused", traverse.RespREFUSED, nil, ""},
|
||||||
{"notimp", traverse.RespNOTIMPL, nil, ""},
|
{"notimp", traverse.RespNOTIMPL, nil, ""},
|
||||||
{"cname_loop", traverse.RespCNAMELoop, nil, "loop detected"},
|
{"cname_loop", traverse.RespCNAMELoop, nil, "loop detected"},
|
||||||
|
{"ns_error", traverse.RespNSResolutionFailed, nil, "nameserver ns1.example.com could not be resolved"},
|
||||||
{"error", traverse.RespError, nil, "something went wrong"},
|
{"error", traverse.RespError, nil, "something went wrong"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dnsinternal "github.com/hits/ExploreDNS/internal/dns"
|
dnsinternal "gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
@@ -230,7 +230,10 @@ func (r *Referral) Resolve(ctx context.Context, traverser *Traverser, cache *Inf
|
|||||||
if resp.Type == RespReferral {
|
if resp.Type == RespReferral {
|
||||||
children := resp.ChildReferrals()
|
children := resp.ChildReferrals()
|
||||||
for _, child := range children {
|
for _, child := range children {
|
||||||
if visited != nil && visited[child.Name] {
|
// Only skip visited names when they have no addresses; if glue
|
||||||
|
// was included in the referral response we still need to query
|
||||||
|
// that child to get the authoritative answer.
|
||||||
|
if visited != nil && visited[child.Name] && !child.HasAddresses() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !stack.Push(child) {
|
if !stack.Push(child) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TypeA = dns.TypeA
|
const TypeA = dns.TypeA
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package traverse
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ const (
|
|||||||
RespNOTIMPL
|
RespNOTIMPL
|
||||||
RespCNAMELoop
|
RespCNAMELoop
|
||||||
RespError
|
RespError
|
||||||
|
// RespNSResolutionFailed indicates that the traversal could not resolve the
|
||||||
|
// IP address of an in-bailiwick nameserver. The domain may still be
|
||||||
|
// reachable in practice (e.g. via glue records held by the registry), but
|
||||||
|
// the iterative traversal could not complete that path.
|
||||||
|
RespNSResolutionFailed
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rt ResponseType) String() string {
|
func (rt ResponseType) String() string {
|
||||||
@@ -44,6 +49,8 @@ func (rt ResponseType) String() string {
|
|||||||
return "cname_loop"
|
return "cname_loop"
|
||||||
case RespError:
|
case RespError:
|
||||||
return "error"
|
return "error"
|
||||||
|
case RespNSResolutionFailed:
|
||||||
|
return "ns_error"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
@@ -231,7 +238,7 @@ func (r *Response) resolveGlue(child *Referral) {
|
|||||||
|
|
||||||
func (r *Response) IsTerminal() bool {
|
func (r *Response) IsTerminal() bool {
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError:
|
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError, RespNSResolutionFailed:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ func TestResponseIsTerminal(t *testing.T) {
|
|||||||
{RespNXDOMAIN, true},
|
{RespNXDOMAIN, true},
|
||||||
{RespSERVFAIL, true},
|
{RespSERVFAIL, true},
|
||||||
{RespError, true},
|
{RespError, true},
|
||||||
|
{RespNSResolutionFailed, true},
|
||||||
{RespReferral, false},
|
{RespReferral, false},
|
||||||
{RespCNAMEFollow, false},
|
{RespCNAMEFollow, false},
|
||||||
}
|
}
|
||||||
@@ -272,6 +273,7 @@ func TestResponseTypeString(t *testing.T) {
|
|||||||
{RespNXDOMAIN, "nxdomain"},
|
{RespNXDOMAIN, "nxdomain"},
|
||||||
{RespSERVFAIL, "servfail"},
|
{RespSERVFAIL, "servfail"},
|
||||||
{RespError, "error"},
|
{RespError, "error"},
|
||||||
|
{RespNSResolutionFailed, "ns_error"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package traverse
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewStack(t *testing.T) {
|
func TestNewStack(t *testing.T) {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/dns"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,15 +227,24 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I
|
|||||||
}
|
}
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
|
||||||
ref.Addresses = t.resolveGlueViaSystem(ctx, ref.Name, cache)
|
// Resolve the nameserver's IP address. The NS hostname is stored in
|
||||||
|
// Bailiwick; ref.Name is the domain being queried (not the NS name).
|
||||||
|
nsToResolve := ref.Bailiwick
|
||||||
|
if nsToResolve == "" || nsToResolve == "." {
|
||||||
|
nsToResolve = ref.Name
|
||||||
|
}
|
||||||
|
nsName := strings.TrimSuffix(nsToResolve, ".")
|
||||||
|
|
||||||
|
ref.Addresses = t.resolveGlueViaSystem(ctx, nsToResolve, cache)
|
||||||
if len(ref.Addresses) > 0 {
|
if len(ref.Addresses) > 0 {
|
||||||
ref.State = StateResolved
|
ref.State = StateResolved
|
||||||
} else {
|
} else {
|
||||||
addrs, err := t.ResolveNS(ctx, ref.Name, cache, visitedCopy, t.depth)
|
addrs, err := t.ResolveNS(ctx, nsToResolve, cache, visitedCopy, t.depth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &Response{
|
return &Response{
|
||||||
Referral: ref,
|
Referral: ref,
|
||||||
Type: RespError,
|
Type: RespNSResolutionFailed,
|
||||||
|
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(addrs) > 0 {
|
if len(addrs) > 0 {
|
||||||
@@ -243,7 +253,8 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I
|
|||||||
} else {
|
} else {
|
||||||
return &Response{
|
return &Response{
|
||||||
Referral: ref,
|
Referral: ref,
|
||||||
Type: RespError,
|
Type: RespNSResolutionFailed,
|
||||||
|
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,7 +371,7 @@ func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCac
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Type == RespSERVFAIL || resp.Type == RespError {
|
if resp.Type == RespSERVFAIL || resp.Type == RespError || resp.Type == RespNSResolutionFailed {
|
||||||
lastErr = fmt.Errorf("server error resolving %s: %s", nsName, resp.Type)
|
lastErr = fmt.Errorf("server error resolving %s: %s", nsName, resp.Type)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -368,7 +379,10 @@ func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCac
|
|||||||
if resp.Type == RespReferral {
|
if resp.Type == RespReferral {
|
||||||
children := resp.ChildReferrals()
|
children := resp.ChildReferrals()
|
||||||
for _, child := range children {
|
for _, child := range children {
|
||||||
if visited != nil && visited[child.Name] {
|
// Only skip visited names when they have no addresses; if glue
|
||||||
|
// was included in the referral response we still need to query
|
||||||
|
// that child to get the authoritative answer.
|
||||||
|
if visited != nil && visited[child.Name] && !child.HasAddresses() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !stack.Push(child) {
|
if !stack.Push(child) {
|
||||||
|
|||||||
+3
-3
@@ -10,9 +10,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/internal/config"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/config"
|
||||||
idns "github.com/hits/ExploreDNS/internal/dns"
|
idns "gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Job status values.
|
// Job status values.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hits/ExploreDNS/web/api"
|
"gitea.hansenits.com.au/hits/ExploreDNS/web/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestServer starts a Server on a random port and returns it.
|
// newTestServer starts a Server on a random port and returns it.
|
||||||
|
|||||||
Reference in New Issue
Block a user