diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 054e8bc..e7cf9b3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.7" + ".": "0.2.8" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fb45f67..5b48bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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) diff --git a/README.md b/README.md index b002a90..7259542 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,7 @@ 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 | +| `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 | diff --git a/dns/dns.go b/dns/dns.go index 909e054..57eb1a3 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -726,6 +726,7 @@ func DecodeRDataCAA(p []byte) ([]byte, error) { func EncodeRDataCAA(p []byte) []byte { const tag = "issue" rdata := make([]byte, 2+len(tag)+len(p)) + // rdata[0] = 0 (flags; bit 7 is the "critical" flag per RFC 8659 §4.1) rdata[1] = byte(len(tag)) copy(rdata[2:], tag) copy(rdata[2+len(tag):], p) diff --git a/dns/dns_test.go b/dns/dns_test.go index 4c90b9e..59fe22f 100644 --- a/dns/dns_test.go +++ b/dns/dns_test.go @@ -805,6 +805,102 @@ func TestEncodeDecodeRDataAAAA(t *testing.T) { } } +func TestEncodeDecodeRDataNULL(t *testing.T) { + for _, p := range [][]byte{ + {}, + {0x00}, + {0x01, 0x02, 0x03}, + bytes.Repeat([]byte{0xab}, 100), + bytes.Repeat([]byte{0xff}, 1000), + } { + rdata := EncodeRDataNULL(p) + decoded, err := DecodeRDataNULL(rdata) + if err != nil { + t.Errorf("DecodeRDataNULL(%x): %v", rdata, err) + continue + } + if !bytes.Equal(decoded, p) { + t.Errorf("NULL round-trip failed for len=%d: got len=%d", len(p), len(decoded)) + } + } +} + +func TestRDataNULLIdentity(t *testing.T) { + // NULL encode/decode should be identity — no framing overhead. + p := []byte{0x01, 0x02, 0x03} + if !bytes.Equal(EncodeRDataNULL(p), p) { + t.Error("EncodeRDataNULL should return input unchanged") + } + decoded, _ := DecodeRDataNULL(p) + if !bytes.Equal(decoded, p) { + t.Error("DecodeRDataNULL should return input unchanged") + } +} + +func TestDecodeRDataCAA(t *testing.T) { + for _, test := range []struct { + desc string + p []byte + decoded []byte + err error + }{ + {"empty input", []byte{}, nil, io.ErrUnexpectedEOF}, + {"single byte", []byte{0x00}, nil, io.ErrUnexpectedEOF}, + {"tag length exceeds data", []byte{0x00, 0x05, 'a'}, nil, io.ErrUnexpectedEOF}, + {"tag only, no value", []byte{0x00, 0x05, 'i', 's', 's', 'u', 'e'}, []byte{}, nil}, + {"tag + value", []byte{0x00, 0x05, 'i', 's', 's', 'u', 'e', 0xaa, 0xbb}, []byte{0xaa, 0xbb}, nil}, + {"zero-length tag", []byte{0x00, 0x00, 0x01, 0x02}, []byte{0x01, 0x02}, nil}, + {"flags byte ignored", []byte{0x80, 0x05, 'i', 's', 's', 'u', 'e', 0xff}, []byte{0xff}, nil}, + } { + decoded, err := DecodeRDataCAA(test.p) + if err != test.err { + t.Errorf("%s: got err %v, want %v", test.desc, err, test.err) + continue + } + if err == nil && !bytes.Equal(decoded, test.decoded) { + t.Errorf("%s: got %x, want %x", test.desc, decoded, test.decoded) + } + } +} + +func TestEncodeRDataCAA(t *testing.T) { + p := []byte{0x01, 0x02, 0x03} + rdata := EncodeRDataCAA(p) + // Expected: flags(0) + tagLen(5) + "issue" + payload + expected := append([]byte{0x00, 0x05, 'i', 's', 's', 'u', 'e'}, p...) + if !bytes.Equal(rdata, expected) { + t.Errorf("EncodeRDataCAA(%x) = %x, want %x", p, rdata, expected) + } +} + +func TestEncodeRDataCAAEmpty(t *testing.T) { + rdata := EncodeRDataCAA([]byte{}) + // Even with empty payload, should have flags + tagLen + tag. + if len(rdata) != 7 { + t.Errorf("EncodeRDataCAA(empty) length = %d, want 7", len(rdata)) + } +} + +func TestRDataCAARoundTrip(t *testing.T) { + for _, p := range [][]byte{ + {}, + {0x00}, + {0x01, 0x02, 0x03}, + bytes.Repeat([]byte{0xab}, 100), + bytes.Repeat([]byte{0xff}, 1000), + } { + rdata := EncodeRDataCAA(p) + decoded, err := DecodeRDataCAA(rdata) + if err != nil { + t.Errorf("CAA round-trip decode error for len=%d: %v", len(p), err) + continue + } + if !bytes.Equal(decoded, p) { + t.Errorf("CAA round-trip failed for len=%d: got len=%d", len(p), len(decoded)) + } + } +} + func TestReadRRMXCompression(t *testing.T) { // DNS message with MX answer using compression pointer in exchange name. msg := []byte{ diff --git a/docs/client-library.md b/docs/client-library.md index 25514ec..391b6b0 100644 --- a/docs/client-library.md +++ b/docs/client-library.md @@ -84,7 +84,7 @@ ts.ClientIDSize = 1 // smaller ClientID ts.MaxQnameLen = 101 // QNAME length constraint ts.MaxNumLabels = 2 // label count constraint ts.RPS = 200 // rate limit queries/second -ts.RecordType = "cname" // DNS record type for downstream data (default: "txt") +ts.RecordType = "cname" // DNS record type for downstream data: txt, null, cname, a, aaaa, mx, ns, srv, caa (default: "txt") // Session options t.IdleTimeout = 60 * time.Second