Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-p
| `-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 @@ -224,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 @@ -408,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. | 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
20 changes: 12 additions & 8 deletions client/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ func forgedInfoMilestone(total uint64) bool {

// DNSPacketConn provides a packet-sending and -receiving interface over various
// forms of DNS. It handles the details of how packets and padding are encoded
// as a DNS name in the Question section of an upstream query, and as a TXT RR
// in downstream responses.
// as a DNS name in the Question section of an upstream query, and as an RR in
// downstream responses.
//
// DNSPacketConn does not handle the mechanics of actually sending and receiving
// encoded DNS messages. That is rather the responsibility of some other
Expand All @@ -166,7 +166,8 @@ type DNSPacketConn struct {
clientID turbotunnel.ClientID
wireConfig turbotunnel.WireConfig
domain dns.Name
// rrType is the DNS record type used for downstream data (TXT, CNAME, A, AAAA, MX, NS, or SRV).
// rrType is the DNS record type used for downstream data (TXT, NULL, CNAME,
// A, AAAA, MX, NS, SRV, or CAA).
rrType uint16
// Sending on pollChan permits sendLoop to send an empty polling query.
// sendLoop also does its own polling according to a time schedule.
Expand Down Expand Up @@ -259,11 +260,10 @@ func (c *DNSPacketConn) TransportErrors() <-chan error {
return c.transportErr
}

// dnsResponsePayload extracts the downstream payload of a DNS response, encoded
// into the RDATA of a TXT or CNAME RR. It returns (nil, true) when the response
// has a non-NoError RCODE, indicating a forged or hijacked response. It returns
// (payload, false) on success or (nil, false) when the response doesn't pass
// format checks.
// dnsResponsePayload extracts the downstream payload of a DNS response. It
// returns (nil, true) when the response has a non-NoError RCODE, indicating a
// forged or hijacked response. It returns (payload, false) on success or
// (nil, false) when the response doesn't pass format checks.
func dnsResponsePayload(resp *dns.Message, domain dns.Name, rrType uint16) ([]byte, bool) {
if resp.Flags&0x8000 != 0x8000 {
// QR != 1, this is not a response.
Expand Down Expand Up @@ -318,6 +318,10 @@ func dnsResponsePayload(resp *dns.Message, domain dns.Name, rrType uint16) ([]by
var payload []byte
var err error
switch rrType {
case dns.RRTypeNULL:
payload, err = dns.DecodeRDataNULL(answer.Data)
case dns.RRTypeCAA:
payload, err = dns.DecodeRDataCAA(answer.Data)
case dns.RRTypeCNAME:
payload, err = dns.DecodeRDataCNAME(answer.Data, domain)
case dns.RRTypeNS:
Expand Down
42 changes: 41 additions & 1 deletion dns/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ const (
RRTypeA = 1
RRTypeNS = 2
RRTypeCNAME = 5
RRTypeNULL = 10
RRTypeMX = 15
RRTypeTXT = 16
RRTypeAAAA = 28
RRTypeSRV = 33
RRTypeCAA = 257
// https://tools.ietf.org/html/rfc6891#section-6.1.1
RRTypeOPT = 41

Expand All @@ -78,6 +80,8 @@ func ParseRecordType(s string) (uint16, error) {
return RRTypeTXT, nil
case "cname":
return RRTypeCNAME, nil
case "null":
return RRTypeNULL, nil
case "a":
return RRTypeA, nil
case "aaaa":
Expand All @@ -88,8 +92,10 @@ func ParseRecordType(s string) (uint16, error) {
return RRTypeNS, nil
case "srv":
return RRTypeSRV, nil
case "caa":
return RRTypeCAA, nil
default:
return 0, fmt.Errorf("unknown record type %q: must be one of: txt, cname, a, aaaa, mx, ns, srv", s)
return 0, fmt.Errorf("unknown record type %q: must be one of: txt, cname, null, a, aaaa, mx, ns, srv, caa", s)
}
}

Expand Down Expand Up @@ -692,6 +698,40 @@ func EncodeRDataTXT(p []byte) []byte {
return buf.Bytes()
}

// DecodeRDataNULL decodes NULL RDATA as a raw byte slice.
// https://tools.ietf.org/html/rfc1035#section-3.3.10
func DecodeRDataNULL(p []byte) ([]byte, error) { return p, nil }

// EncodeRDataNULL encodes a slice of bytes as NULL RDATA.
// https://tools.ietf.org/html/rfc1035#section-3.3.10
func EncodeRDataNULL(p []byte) []byte { return p }

// DecodeRDataCAA decodes CAA RDATA and returns the value portion.
// https://datatracker.ietf.org/doc/html/rfc8659
func DecodeRDataCAA(p []byte) ([]byte, error) {
if len(p) < 2 {
return nil, io.ErrUnexpectedEOF
}
tagLen := int(p[1])
p = p[2:]
if len(p) < tagLen {
return nil, io.ErrUnexpectedEOF
}
return p[tagLen:], nil
}

// EncodeRDataCAA encodes a slice of bytes as CAA RDATA using a fixed
// "issue" tag so the payload lives entirely in the value portion.
// https://datatracker.ietf.org/doc/html/rfc8659
func EncodeRDataCAA(p []byte) []byte {
const tag = "issue"
rdata := make([]byte, 2+len(tag)+len(p))
rdata[1] = byte(len(tag))
copy(rdata[2:], tag)
copy(rdata[2+len(tag):], p)
return rdata
}

// base32Encoding is a base32 encoding without padding, used for CNAME RDATA.
var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)

Expand Down
2 changes: 1 addition & 1 deletion vaydns-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Known TLS fingerprints for -utls are:
flag.BoolVar(&udpAcceptErrors, "udp-accept-errors", false, "accept DNS error responses instead of filtering them (disables censorship evasion)")
flag.BoolVar(&compatDnstt, "dnstt-compat", false, "use original dnstt wire format (8-byte ClientID, padding prefixes)")
flag.IntVar(&clientIDSize, "clientid-size", 2, "client ID size in bytes (ignored when -dnstt-compat is set)")
flag.StringVar(&recordTypeStr, "record-type", "txt", "DNS record type for downstream data (txt, cname, a, aaaa, mx, ns, srv)")
flag.StringVar(&recordTypeStr, "record-type", "txt", "DNS record type for downstream data (txt, null, cname, a, aaaa, mx, ns, srv, caa)")
flag.IntVar(&queueSize, "queue-size", turbotunnel.QueueSize, "packet queue size for transport and DNS layers")
flag.IntVar(&kcpWindowSize, "kcp-window-size", 0, "KCP send/receive window size in packets (0 = queue-size/2)")
flag.StringVar(&queueOverflowStr, "queue-overflow", string(turbotunnel.DefaultQueueOverflowMode), "queue overflow behavior: drop or block")
Expand Down
30 changes: 22 additions & 8 deletions vaydns-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ func nextPacketDnstt(r *bytes.Reader) ([]byte, error) {
// this query. If the returned dns.Message has an Rcode() of dns.RcodeNoError,
// the message is a candidate for for carrying downstream data in a TXT record.
func responseFor(query *dns.Message, domain dns.Name) (*dns.Message, []byte) {
responsePayloadSize := uint16(maxUDPPayload)
if int(responsePayloadSize) != maxUDPPayload {
responsePayloadSize = 0xffff
}

resp := &dns.Message{
ID: query.ID,
Flags: 0x8000, // QR = 1, RCODE = no error
Expand Down Expand Up @@ -455,7 +460,7 @@ func responseFor(query *dns.Message, domain dns.Name) (*dns.Message, []byte) {
resp.Additional = append(resp.Additional, dns.RR{
Name: dns.Name{},
Type: dns.RRTypeOPT,
Class: 4096, // responder's UDP payload size
Class: responsePayloadSize, // responder's UDP payload size
TTL: 0,
Data: []byte{},
})
Expand Down Expand Up @@ -778,6 +783,10 @@ func encodeResponsePayload(rec *record, data []byte, domain dns.Name) error {
Data: chunk,
}
}
case dns.RRTypeNULL:
rec.Resp.Answer[0].Data = dns.EncodeRDataNULL(data)
case dns.RRTypeCAA:
rec.Resp.Answer[0].Data = dns.EncodeRDataCAA(data)
case dns.RRTypeCNAME:
rdata, err := dns.EncodeRDataCNAME(data, domain)
if err != nil {
Expand Down Expand Up @@ -941,15 +950,15 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-
return nil
}

// computeMaxEncodedPayload computes the maximum amount of downstream TXT RR
// data that keep the overall response size less than maxUDPPayload, in the
// computeMaxEncodedPayload computes the maximum amount of downstream single-RR
// payload that keeps the overall response size less than maxUDPPayload, in the
// worst case when the response answers a query that has a maximum-length name
// in its Question section. Returns 0 in the case that no amount of data makes
// the overall response size small enough.
//
// This function needs to be kept in sync with sendLoop with regard to how it
// builds candidate responses.
func computeMaxEncodedPayload(limit int) int {
func computeMaxEncodedPayload(limit int, encode func([]byte) []byte) int {
// 64+64+64+62 octets, needs to be base32-decodable.
maxLengthName, err := dns.NewName([][]byte{
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
Expand Down Expand Up @@ -1014,7 +1023,7 @@ func computeMaxEncodedPayload(limit int) int {
high := 32768
for low+1 < high {
mid := (low + high) / 2
resp.Answer[0].Data = dns.EncodeRDataTXT(make([]byte, mid))
resp.Answer[0].Data = encode(make([]byte, mid))
buf, err := resp.WireFormat()
if err != nil {
panic(err)
Expand Down Expand Up @@ -1136,8 +1145,12 @@ func run(privkey []byte, domain dns.Name, upstream string, dnsConn net.PacketCon
maxEncodedPayload = computeMaxEncodedPayloadMultiRR(maxUDPPayload, 4)
case dns.RRTypeAAAA:
maxEncodedPayload = computeMaxEncodedPayloadMultiRR(maxUDPPayload, 16)
case dns.RRTypeNULL:
maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload, dns.EncodeRDataNULL)
case dns.RRTypeCAA:
maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload, dns.EncodeRDataCAA)
default:
maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload)
maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload, dns.EncodeRDataTXT)
}
// 2 bytes accounts for a packet length prefix.
mtu := maxEncodedPayload - 2
Expand Down Expand Up @@ -1243,7 +1256,7 @@ Example:
flag.StringVar(&keepAliveStr, "keepalive", defaultKeepAlive.String(), "keepalive ping interval (e.g. 2s, 500ms); must be less than idle-timeout")
flag.BoolVar(&compatDnstt, "dnstt-compat", false, "use original dnstt wire format (8-byte ClientID, padding prefixes)")
flag.IntVar(&clientIDSize, "clientid-size", 2, "client ID size in bytes (ignored when -dnstt-compat is set)")
flag.StringVar(&recordTypeStr, "record-type", "txt", "DNS record type for downstream data (txt, cname, a, aaaa, mx, ns, srv)")
flag.StringVar(&recordTypeStr, "record-type", "txt", "DNS record type for downstream data (txt, null, cname, a, aaaa, mx, ns, srv, caa)")
flag.IntVar(&queueSize, "queue-size", turbotunnel.QueueSize, "packet queue size for DNS tunnel transport")
flag.IntVar(&kcpWindowSize, "kcp-window-size", 0, "KCP send/receive window size in packets (0 = queue-size/2)")
flag.StringVar(&queueOverflowStr, "queue-overflow", string(turbotunnel.DefaultQueueOverflowMode), "queue overflow behavior: drop or block")
Expand Down Expand Up @@ -1453,7 +1466,8 @@ Example:
}
log.Infof("wire config: clientid-size=%d compat=%v", wireConfig.ClientIDSize, wireConfig.Compat)

if recordType != dns.RRTypeTXT {
switch recordType {
case dns.RRTypeCNAME, dns.RRTypeNS, dns.RRTypeMX, dns.RRTypeSRV:
explicitFlags := make(map[string]bool)
flag.Visit(func(f *flag.Flag) {
explicitFlags[f.Name] = true
Expand Down
Loading