diff --git a/README.md b/README.md index bde923c..b002a90 100644 --- a/README.md +++ b/README.md @@ -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` | @@ -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` | @@ -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. diff --git a/client/dns.go b/client/dns.go index 729dece..0453b78 100644 --- a/client/dns.go +++ b/client/dns.go @@ -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 @@ -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. @@ -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. @@ -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: diff --git a/dns/dns.go b/dns/dns.go index 8f35a6a..909e054 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -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 @@ -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": @@ -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) } } @@ -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) diff --git a/vaydns-client/main.go b/vaydns-client/main.go index 212e5bf..4b94224 100644 --- a/vaydns-client/main.go +++ b/vaydns-client/main.go @@ -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") diff --git a/vaydns-server/main.go b/vaydns-server/main.go index 32b61c4..08bf87e 100644 --- a/vaydns-server/main.go +++ b/vaydns-server/main.go @@ -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 @@ -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{}, }) @@ -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 { @@ -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"), @@ -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) @@ -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 @@ -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") @@ -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