Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
24bdc6c
feat: tune session defaults and hide session-check-interval flag
crazydi4mond Apr 1, 2026
280e389
chore(main): release 0.2.7 (#61)
github-actions[bot] Apr 1, 2026
c4150b2
docs(server): update stale dnstt references in package doc comments
crazydi4mond Apr 1, 2026
f0337b7
docs(client): add missing fields and fix stale defaults in client lib…
crazydi4mond Apr 3, 2026
056810f
build: add CGO_ENABLED=0 to CI and release workflows
crazydi4mond Apr 4, 2026
3533148
feat: add -v flag for printing version (#71)
crazydi4mond Apr 4, 2026
8354db2
feat: add NULL and CAA record type support and fix server EDNS mtu ad…
ebpfx Apr 10, 2026
0d87441
test(dns): add NULL and CAA encode/decode unit tests
crazydi4mond Apr 10, 2026
a0ff701
chore(main): release 0.2.8 (#65)
github-actions[bot] Apr 10, 2026
4dcdee0
fix(multi-resolver): isolate single-entry transport errors
crazydi4mond Apr 11, 2026
922faf0
test(e2e): add multi-resolver integration tests
crazydi4mond Apr 11, 2026
2041dd1
Merge branch 'main' into multidns
crazydi4mond Apr 11, 2026
fcd228a
fix(client): apply uTLS fingerprint to DoT resolvers in multi mode
crazydi4mond Apr 11, 2026
6f35ceb
fix(multi-resolver): move health counters under mu to close decay race
crazydi4mond Apr 11, 2026
04d4811
fix(multi-resolver): per-resolver forged response tracking with label…
crazydi4mond Apr 12, 2026
453e0de
fix(client): preserve NewTunnel single-resolver API, add NewTunnelMulti
crazydi4mond Apr 12, 2026
aa1ab75
fix(multi-resolver): decouple counter decay from per-response health …
crazydi4mond Apr 12, 2026
207be17
refactor(multi-resolver): remove dead selection modes, add startup lo…
crazydi4mond Apr 12, 2026
4f8962f
test(e2e): add dnstt-compat + multi-resolver regression test
crazydi4mond Apr 12, 2026
68022f4
refactor(multi-resolver): cleanup dead code, fix edge cases, improve …
crazydi4mond Apr 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
go-version: '1.24'

- name: Build
run: go build -v ./...
run: CGO_ENABLED=0 go build -v -ldflags="-X main.version=${GITHUB_SHA::7}" ./...

- name: Test
run: go test -v ./...
Expand Down Expand Up @@ -60,6 +60,7 @@ jobs:

- name: Build
env:
CGO_ENABLED: '0'
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
Expand All @@ -69,8 +70,9 @@ jobs:
ARCH="armv7"
fi
SUFFIX="${{ matrix.suffix }}"
go build -trimpath -ldflags="-s -w" -o "vaydns-client-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-client
go build -trimpath -ldflags="-s -w" -o "vaydns-server-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-server
VERSION="${GITHUB_SHA::7}"
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o "vaydns-client-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-client
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o "vaydns-server-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-server

- name: Upload artifacts
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -118,5 +120,6 @@ jobs:
context: .
file: Dockerfile
push: true
build-args: VERSION=dev-${{ env.SHORT_SHA }}
tags: ghcr.io/${{ github.repository }}:dev-${{ env.SHORT_SHA }}

8 changes: 6 additions & 2 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:

- name: Build
env:
CGO_ENABLED: '0'
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
Expand All @@ -77,8 +78,10 @@ jobs:
ARCH="armv7"
fi
SUFFIX="${{ matrix.suffix }}"
go build -trimpath -ldflags="-s -w" -o "vaydns-client-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-client
go build -trimpath -ldflags="-s -w" -o "vaydns-server-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-server
TAG="${{ needs.release-please.outputs.tag_name }}"
VERSION="${TAG#v}"
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o "vaydns-client-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-client
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o "vaydns-server-${{ matrix.goos }}-${ARCH}${SUFFIX}" ./vaydns-server
chmod +x vaydns-client-* vaydns-server-*

- name: Upload artifact
Expand Down Expand Up @@ -147,6 +150,7 @@ jobs:
context: .
file: Dockerfile
push: true
build-args: VERSION=${{ steps.version.outputs.VERSION }}
tags: |
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }}
ghcr.io/${{ github.repository }}:latest
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.2.6"
".": "0.2.8"
}
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## [0.2.8](https://github.com/net2share/vaydns/compare/v0.2.7...v0.2.8) (2026-04-10)


### Features

* add -v flag for printing version ([#71](https://github.com/net2share/vaydns/issues/71)) ([3533148](https://github.com/net2share/vaydns/commit/35331484bce1b628372dc37accae4e96b507841f))
* add NULL and CAA record type support and fix server EDNS mtu advertisement ([#70](https://github.com/net2share/vaydns/issues/70)) ([8354db2](https://github.com/net2share/vaydns/commit/8354db2a080ce9543594bc4e71592f33e6489d82))

## [0.2.7](https://github.com/net2share/vaydns/compare/v0.2.6...v0.2.7) (2026-04-01)


### Features

* tune session defaults and hide session-check-interval flag ([24bdc6c](https://github.com/net2share/vaydns/commit/24bdc6cc37bdd9a3c91900cf61e477207517cb1f))


### Bug Fixes

* **client:** make max streams unlimited by default ([#63](https://github.com/net2share/vaydns/issues/63)) ([4cc8228](https://github.com/net2share/vaydns/commit/4cc8228190d415cc75a32f59d83f7b77ae2af68b))
* **server:** clarify server accept session/stream warning logs ([#57](https://github.com/net2share/vaydns/issues/57)) ([654b2f1](https://github.com/net2share/vaydns/commit/654b2f1f3dbde6001e19c9f8391436c28e55eca9))

## [0.2.6](https://github.com/net2share/vaydns/compare/v0.2.5...v0.2.6) (2026-03-29)


Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .

RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /vaydns-server ./vaydns-server
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /vaydns-client ./vaydns-client
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /vaydns-server ./vaydns-server
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /vaydns-client ./vaydns-client

FROM alpine
RUN apk add --no-cache curl
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-p
| `-privkey HEX` | Server private key as hex string | — |
| `-gen-key` | Generate a new keypair and exit | — |
| `-mtu N` | Max UDP payload size for responses | `1232` |
| `-idle-timeout D` | Session idle timeout (must match client) | `60s` |
| `-keepalive D` | Keepalive ping interval (must match client, must be < idle-timeout) | `10s` |
| `-idle-timeout D` | Session idle timeout (must match client) | `10s` |
| `-keepalive D` | Keepalive ping interval (must match client, must be < idle-timeout) | `2s` |
| `-fallback ADDR` | UDP endpoint to forward non-DNS packets to (e.g. `127.0.0.1:8888`) | — |
| `-dnstt-compat` | Use original dnstt wire format (8-byte ClientID, padding prefixes). Also sets `-idle-timeout` to 2m and `-keepalive` to 10s unless explicitly overridden. | `false` |
| `-clientid-size N` | ClientID size in bytes (ignored when `-dnstt-compat` is set) | `2` |
| `-record-type TYPE` | DNS record type for downstream data: `txt`, `cname`, `a`, `aaaa`, `mx`, `ns`, `srv`. Must match the client. Ignored (forced to `txt`) when `-dnstt-compat` is set. | `txt` |
| `-record-type TYPE` | DNS record type for downstream data: `txt`, `null`, `cname`, `a`, `aaaa`, `mx`, `ns`, `srv`, `caa`. Must match the client. Ignored (forced to `txt`) when `-dnstt-compat` is set. | `txt` |
| `-queue-size N` | Packet queue size for transport and DNS layers | `512` |
| `-kcp-window-size N` | KCP send/receive window size in packets (0 = queue-size/2) | `0` |
| `-queue-overflow MODE` | Queue overflow behavior: `drop` (silent discard) or `block` (backpressure) | `drop` |
Expand Down Expand Up @@ -164,17 +164,16 @@ sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-p

| Flag | Description | Default |
| --------------------------- | -------------------------------------------------- | ------- |
| `-idle-timeout D` | Session idle timeout (must match server) | `60s` |
| `-keepalive D` | Keepalive ping interval (must match server, must be < idle-timeout) | `10s` |
| `-idle-timeout D` | Session idle timeout (must match server) | `10s` |
| `-keepalive D` | Keepalive ping interval (must match server, must be < idle-timeout) | `2s` |
| `-max-streams N` | Max concurrent streams per session (0 = unlimited) | `0` |
| `-open-stream-timeout D` | Timeout for opening an smux stream | `10s` |
| `-reconnect-min D` | Initial backoff delay for session reconnect | `1s` |
| `-reconnect-max D` | Max backoff delay (must be >= reconnect-min) | `30s` |
| `-session-check-interval D` | How often to check if the session is alive (should be shorter than idle-timeout) | `20s` |

> **Note:** `idle-timeout` and `keepalive` must be set to the same values on both client and server — mismatched values will cause one side to close the session before the other detects it. Keep `keepalive` well below `idle-timeout` (the default 6x ratio allows ~6 ping attempts before timeout).
> **Note:** `idle-timeout` and `keepalive` must be set to the same values on both client and server — mismatched values will cause one side to close the session before the other detects it. Keep `keepalive` well below `idle-timeout` (the default 5x ratio allows ~5 ping attempts before timeout).
>
> `session-check-interval` controls how quickly the client detects a dead session and starts reconnecting — it does not affect when the session dies. A lower value means faster reconnection but can cause unnecessary churn on lossy networks. It does not need to match on client and server.
> **How they relate:** `keepalive` controls how often smux sends ping frames to prove the session is alive. `idle-timeout` is how long smux waits with no received data (including pings) before declaring the session dead — it applies symmetrically on both sides.

#### UDP transport tuning

Expand Down Expand Up @@ -225,7 +224,7 @@ These reduce upstream throughput but improve compatibility. The minimum effectiv
| `-rps N` | Rate limit outgoing DNS queries per second (0 = unlimited). Uses a token bucket with 1-second burst allowance. | `0` |
| `-dnstt-compat` | Use original dnstt wire format (8-byte ClientID, padding prefixes). Sets `-max-qname-len` to 253 unless explicitly overridden. Forces `-record-type` to `txt` with a warning if another type is set. | `false` |
| `-clientid-size N` | ClientID size in bytes (ignored when `-dnstt-compat` is set) | `2` |
| `-record-type TYPE` | DNS record type for downstream data: `txt`, `cname`, `a`, `aaaa`, `mx`, `ns`, `srv`. Must match the server. | `txt` |
| `-record-type TYPE` | DNS record type for downstream data: `txt`, `null`, `cname`, `a`, `aaaa`, `mx`, `ns`, `srv`, `caa`. Must match the server. | `txt` |
| `-utls SPEC` | TLS fingerprint distribution (see below) | weighted random |
| `-log-level LEVEL` | Log level: debug, info, warning, error | `info` |

Expand Down Expand Up @@ -393,14 +392,14 @@ On both client and server, `-dnstt-compat` switches to the original dnstt wire f
| Setting | VayDNS default | With `-dnstt-compat` | Applies to |
| ------- | -------------- | -------------------- | ---------- |
| `-max-qname-len` | `101` | `253` | client |
| `-idle-timeout` | `60s` | `2m` | client and server |
| `-keepalive` | `10s` | `10s` | client and server |
| `-idle-timeout` | `10s` | `2m` | client and server |
| `-keepalive` | `2s` | `10s` | client and server |

All three can be explicitly overridden even when `-dnstt-compat` is set — the flag only changes the defaults, it does not lock the values. For example, `-dnstt-compat -idle-timeout 30s` uses the dnstt wire format with a 30-second idle timeout.

> **Note:** `-dnstt-compat` forces `-record-type` to `txt` (with a warning if another type was set). dnstt only supports TXT records, so other record types are incompatible.
>
> The timeout defaults are critical for interop with original dnstt binaries. dnstt uses a 10-second keepalive interval (smux default) and a 2-minute idle timeout. Setting `-idle-timeout` below 10s in compat mode will cause sessions to churn because dnstt peers only send keepalives every 10 seconds. When mixing with dnstt, keep the compat defaults unless you know what you're doing.
> The timeout defaults are critical for interop with original dnstt binaries. dnstt uses a 10-second keepalive interval (smux default) and a 2-minute idle timeout. Setting `-idle-timeout` below 10s in compat mode will cause sessions to churn because dnstt peers only send keepalives every 10 seconds. When connecting to dnstt, keep the compat defaults unless you know what you're doing.

### Record types

Expand All @@ -409,12 +408,14 @@ VayDNS supports multiple DNS record types for downstream data encoding. Both cli
| Type | Description | Capacity |
| ---- | ----------- | -------- |
| `txt` | TXT record (default). Highest capacity, compatible with dnstt. | Bounded by UDP payload (~1200 bytes) |
| `null` | NULL record. Raw binary payload in a single RR. Some recursive resolvers may filter or refuse to relay NULL records. | Bounded by UDP payload |
| `cname` | CNAME record. Data encoded as a DNS name under the tunnel domain. | Bounded by 255-byte DNS name limit |
| `ns` | NS record. Same encoding as CNAME. | Same as CNAME |
| `mx` | MX record. 2-byte preference header + name encoding. | Same as CNAME |
| `srv` | SRV record. 6-byte header + name encoding. | Same as CNAME |
| `a` | A records. Data split into 4-byte chunks across multiple answer RRs. | Bounded by UDP payload |
| `aaaa` | AAAA records. Data split into 16-byte chunks across multiple answer RRs. | Bounded by UDP payload |
| `caa` | CAA record. Payload encoded in the value portion of a fixed `issue` property. | Bounded by UDP payload |

> **Compatibility:** Old VayDNS clients (pre-record-type) only send TXT queries. A new server with the default `-record-type txt` is fully compatible with old clients. Using a non-TXT type requires updating both client and server.

Expand Down
64 changes: 35 additions & 29 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
// t.InitiateSmuxSession()
// stream, _ := t.OpenStream() // returns net.Conn
// defer t.Close()
//
// Multi-resolver usage (spread queries across multiple DNS resolvers):
//
// r1, _ := client.NewResolver(client.ResolverTypeUDP, "8.8.8.8:53")
// r2, _ := client.NewResolver(client.ResolverTypeDOH, "https://1.1.1.1/dns-query")
// ts, _ := client.NewTunnelServer("t.example.com", "pubkey-hex")
// t, _ := client.NewTunnelMulti([]client.Resolver{r1, r2}, ts)
// t.ListenAndServe("127.0.0.1:7000")
package client

import (
Expand All @@ -39,12 +47,12 @@ import (

// Default timeouts for VayDNS mode.
const (
DefaultIdleTimeout = 60 * time.Second
DefaultKeepAlive = 10 * time.Second
DefaultIdleTimeout = 10 * time.Second
DefaultKeepAlive = 2 * time.Second
DefaultOpenStreamTimeout = 10 * time.Second
DefaultReconnectDelay = 1 * time.Second
DefaultReconnectMaxDelay = 30 * time.Second
DefaultSessionCheckInterval = 20 * time.Second
DefaultSessionCheckInterval = 500 * time.Millisecond
DefaultUDPResponseTimeout = 500 * time.Millisecond
DefaultUDPWorkers = 100
DefaultMaxStreams = 0 // unlimited
Expand Down Expand Up @@ -198,8 +206,8 @@ type Tunnel struct {
MaxStreams int // default: 0 (0 = unlimited)
ReconnectMinDelay time.Duration // default: 1s
ReconnectMaxDelay time.Duration // default: 30s
SessionCheckInterval time.Duration // default: 20s
HandshakeTimeout time.Duration // default: 30s
SessionCheckInterval time.Duration // default: 500ms
HandshakeTimeout time.Duration // default: 15s
PacketQueueSize int // default: QueueSize (512)
KCPWindowSize int // default: PacketQueueSize/2
QueueOverflowMode turbotunnel.QueueOverflowMode // default: drop
Expand All @@ -215,9 +223,20 @@ type Tunnel struct {
remoteAddr net.Addr
}

// NewTunnel creates a Tunnel with the given resolver and server configuration.
// NewTunnel creates a Tunnel with a single resolver and server configuration.
// For multiple resolvers, use NewTunnelMulti.
func NewTunnel(resolver Resolver, tunnelServer TunnelServer) (*Tunnel, error) {
return NewTunnelMulti([]Resolver{resolver}, tunnelServer)
}

// NewTunnelMulti creates a Tunnel with multiple resolvers and server
// configuration. When more than one resolver is provided, the client
// multiplexes queries across them with health-based routing.
// Zero-value fields use sensible defaults.
func NewTunnel(resolvers []Resolver, tunnelServer TunnelServer) (*Tunnel, error) {
func NewTunnelMulti(resolvers []Resolver, tunnelServer TunnelServer) (*Tunnel, error) {
if len(resolvers) == 0 {
return nil, fmt.Errorf("at least one resolver is required")
}
t := &Tunnel{
Resolvers: resolvers,
TunnelServer: tunnelServer,
Expand Down Expand Up @@ -292,15 +311,21 @@ func (t *Tunnel) effectiveKCPWindowSize() int {
// based on the Resolver configuration.
func (t *Tunnel) InitiateResolverConnection() error {
if len(t.Resolvers) > 1 {
conn, err := NewMultiResolver(t.Resolvers, SelectionRoundRobin, t.effectivePacketQueueSize(), t.effectiveQueueOverflowMode())
conn, err := NewMultiResolver(t.Resolvers, t.effectivePacketQueueSize(), t.effectiveQueueOverflowMode())
if err != nil {
return err
}
t.resolverConn = conn
t.remoteAddr = turbotunnel.DummyAddr{}
// In multi-resolver mode, each entry owns a per-resolver
// ForgedStats. t.forgedStats stays nil so DNSPacketConn
// creates an unlabeled catch-all (which should rarely fire
// since MultiResolver filters forged responses upstream).
return nil
}
conn, addr, err := GetResolverConnection(t.Resolvers[0], t.effectivePacketQueueSize(), t.effectiveQueueOverflowMode())
r := t.Resolvers[0]
t.forgedStats = &ForgedStats{Label: r.ResolverAddr}
conn, addr, err := GetResolverConnection(r, t.effectivePacketQueueSize(), t.effectiveQueueOverflowMode(), t.forgedStats)
if err != nil {
return err
}
Expand Down Expand Up @@ -530,24 +555,6 @@ func (t *Tunnel) closeTransportLayers() {
t.forgedStats = nil
}

// MultiResolverStats returns per-resolver health and count snapshots when the
// active resolver transport is MultiResolver; otherwise it returns nil.
func (t *Tunnel) MultiResolverStats() []ResolverStat {
if mr, ok := t.resolverConn.(*MultiResolver); ok {
return mr.ResolverStats()
}
return nil
}

// MultiResolverValidInvalidCounts returns valid/invalid counters per resolver
// address when the active resolver transport is MultiResolver.
func (t *Tunnel) MultiResolverValidInvalidCounts() map[string][2]int64 {
if mr, ok := t.resolverConn.(*MultiResolver); ok {
return mr.ValidInvalidCounts()
}
return nil
}

// resetTransportLayers tears down existing transport layers and creates fresh
// ones. Used during reconnect to ensure a clean transport stack.
func (t *Tunnel) resetTransportLayers() error {
Expand Down Expand Up @@ -841,10 +848,9 @@ func NewOutbound(resolvers []Resolver, tunnelServers []TunnelServer) *Outbound {
// Start begins accepting connections on bind and forwarding them through the
// first resolver/server pair.
func (o *Outbound) Start(bind string) error {

tunnelServer := o.TunnelServers[0]

tunnel, err := NewTunnel(o.Resolvers, tunnelServer)
tunnel, err := NewTunnelMulti(o.Resolvers, tunnelServer)
if err != nil {
return fmt.Errorf("failed to create tunnel: %w", err)
}
Expand Down
Loading