Repository: miekg/dns
Pull request #1402 (2022-11-17 08:42:51)

Add AMTRELAY and IPSECKEY RR types
This PR adds the AMTRELAY and IPSECKEY RR types to this library, and bumps dependencies because `go generate` appears to be broken otherwise.

miekg: looks good in general, but I've got a bunch of nits and questions.

You can also update the README and add RFC 8777 and 4025
miekg: @tmthrgd you happy with this now? If so, you (or I) can click 'squash and merge'
tmthrgd: @miekg There's still one un-resolved review comment about `DiscoveryOptional`. I think that either needs to be marked as resolved or changed first.

Patch:
diff --git a/README.md b/README.md
index 16b35965..ec954039 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,7 @@ A not-so-up-to-date-list-that-may-be-actually-current:
 * https://github.com/slackhq/nebula
 * https://github.com/dnschecktool/dow-proxy
 * https://dnscheck.tools/
+* https://github.com/egbakou/domainverifier
 
 
 Send pull request if you want to be listed here.
@@ -143,6 +144,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
 * 340{1,2,3} - NAPTR record
 * 3445 - Limiting the scope of (DNS)KEY
 * 3597 - Unknown RRs
+* 4025 - A Method for Storing IPsec Keying Material in DNS
 * 403{3,4,5} - DNSSEC + validation functions
 * 4255 - SSHFP record
 * 4343 - Case insensitivity
@@ -178,6 +180,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
 * 8080 - EdDSA for DNSSEC
 * 8499 - DNS Terminology
 * 8659 - DNS Certification Authority Authorization (CAA) Resource Record
+* 8777 - DNS Reverse IP Automatic Multicast Tunneling (AMT) Discovery
 * 8914 - Extended DNS Errors
 * 8976 - Message Digest for DNS Zones (ZONEMD RR)
 
diff --git a/duplicate_generate.go b/duplicate_generate.go
index adfd3062..d61c15f4 100644
--- a/duplicate_generate.go
+++ b/duplicate_generate.go
@@ -146,6 +146,18 @@ func main() {
 				o2("if !r1.%s.Equal(r2.%s) {\nreturn false\n}")
 			case `dns:"cdomain-name"`, `dns:"domain-name"`:
 				o2("if !isDuplicateName(r1.%s, r2.%s) {\nreturn false\n}")
+			case `dns:"ipsechost"`, `dns:"amtrelayhost"`:
+				o2(`switch r1.GatewayType {
+				case IPSECGatewayIPv4, IPSECGatewayIPv6:
+					if !r1.GatewayAddr.Equal(r2.GatewayAddr) {
+						return false
+					}
+				case IPSECGatewayHost:
+					if !isDuplicateName(r1.%s, r2.%s) {
+						return false
+					}
+				}
+				`)
 			default:
 				o2("if r1.%s != r2.%s {\nreturn false\n}")
 			}
diff --git a/go.mod b/go.mod
index f7363204..808feafa 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/miekg/dns
 go 1.14
 
 require (
-	golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
-	golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2
+	golang.org/x/net v0.2.0
+	golang.org/x/sync v0.1.0
+	golang.org/x/sys v0.2.0
+	golang.org/x/tools v0.3.0
 )
diff --git a/go.sum b/go.sum
index 008f5fd6..38fa8cc3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,33 +1,35 @@
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
-golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
-golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/msg_generate.go b/msg_generate.go
index 76f1e1be..6a37a865 100644
--- a/msg_generate.go
+++ b/msg_generate.go
@@ -171,6 +171,8 @@ if rr.%s != "-" {
 				o("off, err = packStringAny(rr.%s, msg, off)\n")
 			case st.Tag(i) == `dns:"octet"`:
 				o("off, err = packStringOctet(rr.%s, msg, off)\n")
+			case st.Tag(i) == `dns:"ipsechost"` || st.Tag(i) == `dns:"amtrelayhost"`:
+				o("off, err = packIPSECGateway(rr.GatewayAddr, rr.%s, msg, off, rr.GatewayType, compression, false)\n")
 			case st.Tag(i) == "":
 				switch st.Field(i).Type().(*types.Basic).Kind() {
 				case types.Uint8:
@@ -278,6 +280,8 @@ return off, err
 				o("rr.%s, off, err = unpackStringAny(msg, off, rdStart + int(rr.Hdr.Rdlength))\n")
 			case `dns:"octet"`:
 				o("rr.%s, off, err = unpackStringOctet(msg, off)\n")
+			case `dns:"ipsechost"`, `dns:"amtrelayhost"`:
+				o("rr.GatewayAddr, rr.%s, off, err = unpackIPSECGateway(msg, off, rr.GatewayType)\n")
 			case "":
 				switch st.Field(i).Type().(*types.Basic).Kind() {
 				case types.Uint8:
diff --git a/msg_helpers.go b/msg_helpers.go
index ea2035cd..42d5cd53 100644
--- a/msg_helpers.go
+++ b/msg_helpers.go
@@ -810,3 +810,37 @@ func unpackDataAplPrefix(msg []byte, off int) (APLPrefix, int, error) {
 		Network:  ipnet,
 	}, off, nil
 }
+
+func unpackIPSECGateway(msg []byte, off int, gatewayType uint8) (net.IP, string, int, error) {
+	var retAddr net.IP
+	var retString string
+	var err error
+
+	switch gatewayType {
+	case IPSECGatewayNone: // do nothing
+	case IPSECGatewayIPv4:
+		retAddr, off, err = unpackDataA(msg, off)
+	case IPSECGatewayIPv6:
+		retAddr, off, err = unpackDataAAAA(msg, off)
+	case IPSECGatewayHost:
+		retString, off, err = UnpackDomainName(msg, off)
+	}
+
+	return retAddr, retString, off, err
+}
+
+func packIPSECGateway(gatewayAddr net.IP, gatewayString string, msg []byte, off int, gatewayType uint8, compression compressionMap, compress bool) (int, error) {
+	var err error
+
+	switch gatewayType {
+	case IPSECGatewayNone: // do nothing
+	case IPSECGatewayIPv4:
+		off, err = packDataA(gatewayAddr, msg, off)
+	case IPSECGatewayIPv6:
+		off, err = packDataAAAA(gatewayAddr, msg, off)
+	case IPSECGatewayHost:
+		off, err = packDomainName(gatewayString, msg, off, compression, compress)
+	}
+
+	return off, err
+}
diff --git a/scan_rr.go b/scan_rr.go
index e398484d..68dbff69 100644
--- a/scan_rr.go
+++ b/scan_rr.go
@@ -3,6 +3,7 @@ package dns
 import (
 	"bytes"
 	"encoding/base64"
+	"errors"
 	"net"
 	"strconv"
 	"strings"
@@ -1216,6 +1217,117 @@ func (rr *DS) parse(c *zlexer, o string) *ParseError      { return rr.parseDS(c,
 func (rr *DLV) parse(c *zlexer, o string) *ParseError     { return rr.parseDS(c, o, "DLV") }
 func (rr *CDS) parse(c *zlexer, o string) *ParseError     { return rr.parseDS(c, o, "CDS") }
 
+func (rr *IPSECKEY) parse(c *zlexer, o string) *ParseError {
+	l, _ := c.Next()
+	num, err := strconv.ParseUint(l.token, 10, 8)
+	if err != nil || l.err {
+		return &ParseError{"", "bad IPSECKEY value", l}
+	}
+	rr.Precedence = uint8(num)
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	num, err = strconv.ParseUint(l.token, 10, 8)
+	if err != nil || l.err {
+		return &ParseError{"", "bad IPSECKEY value", l}
+	}
+	rr.GatewayType = uint8(num)
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	num, err = strconv.ParseUint(l.token, 10, 8)
+	if err != nil || l.err {
+		return &ParseError{"", "bad IPSECKEY value", l}
+	}
+	rr.Algorithm = uint8(num)
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	if l.err {
+		return &ParseError{"", "bad IPSECKEY gateway", l}
+	}
+
+	rr.GatewayAddr, rr.GatewayHost, err = parseAddrHostUnion(l.token, o, rr.GatewayType)
+	if err != nil {
+		return &ParseError{"", "AMTRELAY " + err.Error(), l}
+	}
+
+	c.Next() // zBlank
+
+	s, pErr := endingToString(c, "bad IPSECKEY PublicKey")
+	if pErr != nil {
+		return pErr
+	}
+	rr.PublicKey = s
+	return slurpRemainder(c)
+}
+
+func (rr *AMTRELAY) parse(c *zlexer, o string) *ParseError {
+	l, _ := c.Next()
+	num, err := strconv.ParseUint(l.token, 10, 8)
+	if err != nil || l.err {
+		return &ParseError{"", "bad AMTRELAY value", l}
+	}
+	rr.Precedence = uint8(num)
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	if l.err || !(l.token == "0" || l.token == "1") {
+		return &ParseError{"", "bad discovery value", l}
+	}
+	if l.token == "1" {
+		rr.GatewayType = 0x80
+	}
+
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	num, err = strconv.ParseUint(l.token, 10, 8)
+	if err != nil || l.err {
+		return &ParseError{"", "bad AMTRELAY value", l}
+	}
+	rr.GatewayType |= uint8(num)
+	c.Next() // zBlank
+
+	l, _ = c.Next()
+	if l.err {
+		return &ParseError{"", "bad AMTRELAY gateway", l}
+	}
+
+	rr.GatewayAddr, rr.GatewayHost, err = parseAddrHostUnion(l.token, o, rr.GatewayType&0x7f)
+	if err != nil {
+		return &ParseError{"", "AMTRELAY " + err.Error(), l}
+	}
+
+	return slurpRemainder(c)
+}
+
+// same constants and parsing between IPSECKEY and AMTRELAY
+func parseAddrHostUnion(token, o string, gatewayType uint8) (addr net.IP, host string, err error) {
+	switch gatewayType {
+	case IPSECGatewayNone:
+		if token != "." {
+			return addr, host, errors.New("gateway type none with gateway set")
+		}
+	case IPSECGatewayIPv4, IPSECGatewayIPv6:
+		addr = net.ParseIP(token)
+		if addr == nil {
+			return addr, host, errors.New("gateway IP invalid")
+		}
+		if (addr.To4() == nil) == (gatewayType == IPSECGatewayIPv4) {
+			return addr, host, errors.New("gateway IP family mismatch")
+		}
+	case IPSECGatewayHost:
+		var ok bool
+		host, ok = toAbsoluteName(token, o)
+		if !ok {
+			return addr, host, errors.New("invalid gateway host")
+		}
+	}
+
+	return addr, host, nil
+}
+
 func (rr *RKEY) parse(c *zlexer, o string) *ParseError {
 	l, _ := c.Next()
 	i, e := strconv.ParseUint(l.token, 10, 16)
diff --git a/types.go b/types.go
index d9becb67..a34ab602 100644
--- a/types.go
+++ b/types.go
@@ -65,6 +65,7 @@ const (
 	TypeAPL        uint16 = 42
 	TypeDS         uint16 = 43
 	TypeSSHFP      uint16 = 44
+	TypeIPSECKEY   uint16 = 45
 	TypeRRSIG      uint16 = 46
 	TypeNSEC       uint16 = 47
 	TypeDNSKEY     uint16 = 48
@@ -98,6 +99,7 @@ const (
 	TypeURI        uint16 = 256
 	TypeCAA        uint16 = 257
 	TypeAVC        uint16 = 258
+	TypeAMTRELAY   uint16 = 260
 
 	TypeTKEY uint16 = 249
 	TypeTSIG uint16 = 250
@@ -159,6 +161,22 @@ const (
 	ZoneMDHashAlgSHA512 = 2
 )
 
+// Used in IPSEC https://datatracker.ietf.org/doc/html/rfc4025#section-2.3
+const (
+	IPSECGatewayNone uint8 = iota
+	IPSECGatewayIPv4
+	IPSECGatewayIPv6
+	IPSECGatewayHost
+)
+
+// Used in AMTRELAY https://datatracker.ietf.org/doc/html/rfc8777#section-4.2.3
+const (
+	AMTRELAYNone = IPSECGatewayNone
+	AMTRELAYIPv4 = IPSECGatewayIPv4
+	AMTRELAYIPv6 = IPSECGatewayIPv6
+	AMTRELAYHost = IPSECGatewayHost
+)
+
 // Header is the wire format for the DNS packet header.
 type Header struct {
 	Id                                 uint16
@@ -994,6 +1012,69 @@ func (rr *DNSKEY) String() string {
 		" " + rr.PublicKey
 }
 
+// IPSECKEY RR. See RFC 4025.
+type IPSECKEY struct {
+	Hdr         RR_Header
+	Precedence  uint8
+	GatewayType uint8
+	Algorithm   uint8
+	GatewayAddr net.IP `dns:"-"` // packing/unpacking/parsing/etc handled together with GatewayHost
+	GatewayHost string `dns:"ipsechost"`
+	PublicKey   string `dns:"base64"`
+}
+
+func (rr *IPSECKEY) String() string {
+	var gateway string
+	switch rr.GatewayType {
+	case IPSECGatewayIPv4, IPSECGatewayIPv6:
+		gateway = rr.GatewayAddr.String()
+	case IPSECGatewayHost:
+		gateway = rr.GatewayHost
+	case IPSECGatewayNone:
+		fallthrough
+	default:
+		gateway = "."
+	}
+
+	return rr.Hdr.String() + strconv.Itoa(int(rr.Precedence)) +
+		" " + strconv.Itoa(int(rr.GatewayType)) +
+		" " + strconv.Itoa(int(rr.Algorithm)) +
+		" " + gateway +
+		" " + rr.PublicKey
+}
+
+// AMTRELAY RR. See RFC 8777.
+type AMTRELAY struct {
+	Hdr         RR_Header
+	Precedence  uint8
+	GatewayType uint8  // discovery is packed in here at bit 0x80
+	GatewayAddr net.IP `dns:"-"` // packing/unpacking/parsing/etc handled together with GatewayHost
+	GatewayHost string `dns:"amtrelayhost"`
+}
+
+func (rr *AMTRELAY) String() string {
+	var gateway string
+	switch rr.GatewayType & 0x7f {
+	case AMTRELAYIPv4, AMTRELAYIPv6:
+		gateway = rr.GatewayAddr.String()
+	case AMTRELAYHost:
+		gateway = rr.GatewayHost
+	case AMTRELAYNone:
+		fallthrough
+	default:
+		gateway = "."
+	}
+	boolS := "0"
+	if rr.GatewayType&0x80 == 0x80 {
+		boolS = "1"
+	}
+
+	return rr.Hdr.String() + strconv.Itoa(int(rr.Precedence)) +
+		" " + boolS +
+		" " + strconv.Itoa(int(rr.GatewayType&0x7f)) +
+		" " + gateway
+}
+
 // RKEY RR. See https://www.iana.org/assignments/dns-parameters/RKEY/rkey-completed-template.
 type RKEY struct {
 	Hdr       RR_Header
diff --git a/types_generate.go b/types_generate.go
index 78cec5b5..9ff75116 100644
--- a/types_generate.go
+++ b/types_generate.go
@@ -220,6 +220,28 @@ func main() {
 				o("for _, t := range rr.%s { l += len(t) + 1 }\n")
 			case st.Tag(i) == `dns:"uint48"`:
 				o("l += 6 // %s\n")
+			case st.Tag(i) == `dns:"ipsechost"`:
+				o(`switch rr.GatewayType {
+				case IPSECGatewayIPv4:
+					l += net.IPv4len
+				case IPSECGatewayIPv6:
+					l += net.IPv6len
+				case IPSECGatewayHost:
+					l += len(rr.%s) + 1
+				}
+				`)
+			case st.Tag(i) == `dns:"amtrelayhost"`:
+				o(`switch rr.GatewayType {
+				case AMTRELAYIPv4:
+					l += net.IPv4len
+				case AMTRELAYIPv6:
+					l += net.IPv6len
+				case AMTRELAYHost:
+					l += len(rr.%s) + 1
+				}
+				`)
+			case st.Tag(i) == `dns:"amtrelaytype"`:
+				o("l++ // %s\n")
 			case st.Tag(i) == "":
 				switch st.Field(i).Type().(*types.Basic).Kind() {
 				case types.Uint8:
diff --git a/zduplicate.go b/zduplicate.go
index 9eb1dac2..450bbbc2 100644
--- a/zduplicate.go
+++ b/zduplicate.go
@@ -43,6 +43,32 @@ func (r1 *AFSDB) isDuplicate(_r2 RR) bool {
 	return true
 }
 
+func (r1 *AMTRELAY) isDuplicate(_r2 RR) bool {
+	r2, ok := _r2.(*AMTRELAY)
+	if !ok {
+		return false
+	}
+	_ = r2
+	if r1.Precedence != r2.Precedence {
+		return false
+	}
+	if r1.GatewayType != r2.GatewayType {
+		return false
+	}
+	switch r1.GatewayType {
+	case IPSECGatewayIPv4, IPSECGatewayIPv6:
+		if !r1.GatewayAddr.Equal(r2.GatewayAddr) {
+			return false
+		}
+	case IPSECGatewayHost:
+		if !isDuplicateName(r1.GatewayHost, r2.GatewayHost) {
+			return false
+		}
+	}
+
+	return true
+}
+
 func (r1 *ANY) isDuplicate(_r2 RR) bool {
 	r2, ok := _r2.(*ANY)
 	if !ok {
@@ -423,6 +449,38 @@ func (r1 *HTTPS) isDuplicate(_r2 RR) bool {
 	return true
 }
 
+func (r1 *IPSECKEY) isDuplicate(_r2 RR) bool {
+	r2, ok := _r2.(*IPSECKEY)
+	if !ok {
+		return false
+	}
+	_ = r2
+	if r1.Precedence != r2.Precedence {
+		return false
+	}
+	if r1.GatewayType != r2.GatewayType {
+		return false
+	}
+	if r1.Algorithm != r2.Algorithm {
+		return false
+	}
+	switch r1.GatewayType {
+	case IPSECGatewayIPv4, IPSECGatewayIPv6:
+		if !r1.GatewayAddr.Equal(r2.GatewayAddr) {
+			return false
+		}
+	case IPSECGatewayHost:
+		if !isDuplicateName(r1.GatewayHost, r2.GatewayHost) {
+			return false
+		}
+	}
+
+	if r1.PublicKey != r2.PublicKey {
+		return false
+	}
+	return true
+}
+
 func (r1 *KEY) isDuplicate(_r2 RR) bool {
 	r2, ok := _r2.(*KEY)
 	if !ok {
diff --git a/zmsg.go b/zmsg.go
index fc0822f9..3ea0eb42 100644
--- a/zmsg.go
+++ b/zmsg.go
@@ -32,6 +32,22 @@ func (rr *AFSDB) pack(msg []byte, off int, compression compressionMap, compress
 	return off, nil
 }
 
+func (rr *AMTRELAY) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
+	off, err = packUint8(rr.Precedence, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packUint8(rr.GatewayType, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packIPSECGateway(rr.GatewayAddr, rr.GatewayHost, msg, off, rr.GatewayType, compression, false)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *ANY) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
 	return off, nil
 }
@@ -332,6 +348,30 @@ func (rr *HTTPS) pack(msg []byte, off int, compression compressionMap, compress
 	return off, nil
 }
 
+func (rr *IPSECKEY) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
+	off, err = packUint8(rr.Precedence, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packUint8(rr.GatewayType, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packUint8(rr.Algorithm, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packIPSECGateway(rr.GatewayAddr, rr.GatewayHost, msg, off, rr.GatewayType, compression, false)
+	if err != nil {
+		return off, err
+	}
+	off, err = packStringBase64(rr.PublicKey, msg, off)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *KEY) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
 	off, err = packUint16(rr.Flags, msg, off)
 	if err != nil {
@@ -1180,6 +1220,34 @@ func (rr *AFSDB) unpack(msg []byte, off int) (off1 int, err error) {
 	return off, nil
 }
 
+func (rr *AMTRELAY) unpack(msg []byte, off int) (off1 int, err error) {
+	rdStart := off
+	_ = rdStart
+
+	rr.Precedence, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.GatewayType, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.GatewayAddr, rr.GatewayHost, off, err = unpackIPSECGateway(msg, off, rr.GatewayType)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *ANY) unpack(msg []byte, off int) (off1 int, err error) {
 	rdStart := off
 	_ = rdStart
@@ -1636,6 +1704,48 @@ func (rr *HTTPS) unpack(msg []byte, off int) (off1 int, err error) {
 	return off, nil
 }
 
+func (rr *IPSECKEY) unpack(msg []byte, off int) (off1 int, err error) {
+	rdStart := off
+	_ = rdStart
+
+	rr.Precedence, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.GatewayType, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.Algorithm, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.GatewayAddr, rr.GatewayHost, off, err = unpackIPSECGateway(msg, off, rr.GatewayType)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.PublicKey, off, err = unpackStringBase64(msg, off, rdStart+int(rr.Hdr.Rdlength))
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 func (rr *KEY) unpack(msg []byte, off int) (off1 int, err error) {
 	rdStart := off
 	_ = rdStart
diff --git a/ztypes.go b/ztypes.go
index 5d060cfe..17543ea4 100644
--- a/ztypes.go
+++ b/ztypes.go
@@ -12,6 +12,7 @@ var TypeToRR = map[uint16]func() RR{
 	TypeA:          func() RR { return new(A) },
 	TypeAAAA:       func() RR { return new(AAAA) },
 	TypeAFSDB:      func() RR { return new(AFSDB) },
+	TypeAMTRELAY:   func() RR { return new(AMTRELAY) },
 	TypeANY:        func() RR { return new(ANY) },
 	TypeAPL:        func() RR { return new(APL) },
 	TypeAVC:        func() RR { return new(AVC) },
@@ -34,6 +35,7 @@ var TypeToRR = map[uint16]func() RR{
 	TypeHINFO:      func() RR { return new(HINFO) },
 	TypeHIP:        func() RR { return new(HIP) },
 	TypeHTTPS:      func() RR { return new(HTTPS) },
+	TypeIPSECKEY:   func() RR { return new(IPSECKEY) },
 	TypeKEY:        func() RR { return new(KEY) },
 	TypeKX:         func() RR { return new(KX) },
 	TypeL32:        func() RR { return new(L32) },
@@ -90,6 +92,7 @@ var TypeToString = map[uint16]string{
 	TypeA:          "A",
 	TypeAAAA:       "AAAA",
 	TypeAFSDB:      "AFSDB",
+	TypeAMTRELAY:   "AMTRELAY",
 	TypeANY:        "ANY",
 	TypeAPL:        "APL",
 	TypeATMA:       "ATMA",
@@ -114,6 +117,7 @@ var TypeToString = map[uint16]string{
 	TypeHINFO:      "HINFO",
 	TypeHIP:        "HIP",
 	TypeHTTPS:      "HTTPS",
+	TypeIPSECKEY:   "IPSECKEY",
 	TypeISDN:       "ISDN",
 	TypeIXFR:       "IXFR",
 	TypeKEY:        "KEY",
@@ -176,6 +180,7 @@ var TypeToString = map[uint16]string{
 func (rr *A) Header() *RR_Header          { return &rr.Hdr }
 func (rr *AAAA) Header() *RR_Header       { return &rr.Hdr }
 func (rr *AFSDB) Header() *RR_Header      { return &rr.Hdr }
+func (rr *AMTRELAY) Header() *RR_Header   { return &rr.Hdr }
 func (rr *ANY) Header() *RR_Header        { return &rr.Hdr }
 func (rr *APL) Header() *RR_Header        { return &rr.Hdr }
 func (rr *AVC) Header() *RR_Header        { return &rr.Hdr }
@@ -198,6 +203,7 @@ func (rr *GPOS) Header() *RR_Header       { return &rr.Hdr }
 func (rr *HINFO) Header() *RR_Header      { return &rr.Hdr }
 func (rr *HIP) Header() *RR_Header        { return &rr.Hdr }
 func (rr *HTTPS) Header() *RR_Header      { return &rr.Hdr }
+func (rr *IPSECKEY) Header() *RR_Header   { return &rr.Hdr }
 func (rr *KEY) Header() *RR_Header        { return &rr.Hdr }
 func (rr *KX) Header() *RR_Header         { return &rr.Hdr }
 func (rr *L32) Header() *RR_Header        { return &rr.Hdr }
@@ -270,6 +276,20 @@ func (rr *AFSDB) len(off int, compression map[string]struct{}) int {
 	l += domainNameLen(rr.Hostname, off+l, compression, false)
 	return l
 }
+func (rr *AMTRELAY) len(off int, compression map[string]struct{}) int {
+	l := rr.Hdr.len(off, compression)
+	l++ // Precedence
+	l++ // GatewayType
+	switch rr.GatewayType {
+	case AMTRELAYIPv4:
+		l += net.IPv4len
+	case AMTRELAYIPv6:
+		l += net.IPv6len
+	case AMTRELAYHost:
+		l += len(rr.GatewayHost) + 1
+	}
+	return l
+}
 func (rr *ANY) len(off int, compression map[string]struct{}) int {
 	l := rr.Hdr.len(off, compression)
 	return l
@@ -379,6 +399,22 @@ func (rr *HIP) len(off int, compression map[string]struct{}) int {
 	}
 	return l
 }
+func (rr *IPSECKEY) len(off int, compression map[string]struct{}) int {
+	l := rr.Hdr.len(off, compression)
+	l++ // Precedence
+	l++ // GatewayType
+	l++ // Algorithm
+	switch rr.GatewayType {
+	case IPSECGatewayIPv4:
+		l += net.IPv4len
+	case IPSECGatewayIPv6:
+		l += net.IPv6len
+	case IPSECGatewayHost:
+		l += len(rr.GatewayHost) + 1
+	}
+	l += base64.StdEncoding.DecodedLen(len(rr.PublicKey))
+	return l
+}
 func (rr *KX) len(off int, compression map[string]struct{}) int {
 	l := rr.Hdr.len(off, compression)
 	l += 2 // Preference
@@ -706,6 +742,9 @@ func (rr *AAAA) copy() RR {
 func (rr *AFSDB) copy() RR {
 	return &AFSDB{rr.Hdr, rr.Subtype, rr.Hostname}
 }
+func (rr *AMTRELAY) copy() RR {
+	return &AMTRELAY{rr.Hdr, rr.Precedence, rr.GatewayType, copyIP(rr.GatewayAddr), rr.GatewayHost}
+}
 func (rr *ANY) copy() RR {
 	return &ANY{rr.Hdr}
 }
@@ -782,6 +821,9 @@ func (rr *HIP) copy() RR {
 func (rr *HTTPS) copy() RR {
 	return &HTTPS{*rr.SVCB.copy().(*SVCB)}
 }
+func (rr *IPSECKEY) copy() RR {
+	return &IPSECKEY{rr.Hdr, rr.Precedence, rr.GatewayType, rr.Algorithm, copyIP(rr.GatewayAddr), rr.GatewayHost, rr.PublicKey}
+}
 func (rr *KEY) copy() RR {
 	return &KEY{*rr.DNSKEY.copy().(*DNSKEY)}
 }


Test patch:
diff --git a/parse_test.go b/parse_test.go
index fce4aab1..0decbdb6 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -2015,3 +2015,30 @@ func TestParseZONEMD(t *testing.T) {
 		}
 	}
 }
+
+func TestParseIPSECKEY(t *testing.T) {
+	dt := map[string]string{
+		"ipseckey. 3600 IN IPSECKEY 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==": "ipseckey.\t3600\tIN\tIPSECKEY\t10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==",
+	}
+
+	for i, o := range dt {
+		rr := testRR(i).(*IPSECKEY)
+		if s := rr.String(); s != o {
+			t.Errorf("input %#v does not match expected output %#v", s, o)
+		}
+	}
+}
+
+func TestParseAMTRELAY(t *testing.T) {
+	dt := map[string]string{
+		"amtrelay. 3600 IN AMTRELAY 10 0 2 2001:470:30:84:e276:63ff:fe72:3900": "amtrelay.\t3600\tIN\tAMTRELAY\t10 0 2 2001:470:30:84:e276:63ff:fe72:3900",
+		"amtrelay. 3600 IN AMTRELAY 10 1 2 2001:470:30:84:e276:63ff:fe72:3900": "amtrelay.\t3600\tIN\tAMTRELAY\t10 1 2 2001:470:30:84:e276:63ff:fe72:3900",
+	}
+
+	for i, o := range dt {
+		rr := testRR(i).(*AMTRELAY)
+		if s := rr.String(); s != o {
+			t.Errorf("input %#v does not match expected output %#v", s, o)
+		}
+	}
+}