-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
38 changed files
with
2,824 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
FROM qmcgaw/godevcontainer | ||
RUN apk add bind-tools |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package setup | ||
|
||
import ( | ||
"net/netip" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Test_getPrivateIPPrefixes(t *testing.T) { | ||
t.Parallel() | ||
|
||
expectedPrivateIPNets := []netip.Prefix{ | ||
netip.PrefixFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 8), | ||
netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 0, 0, 0}), 8), | ||
netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 16, 0, 0}), 12), | ||
netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 0, 0}), 16), | ||
netip.PrefixFrom(netip.AddrFrom4([4]byte{169, 254, 0, 0}), 16), | ||
// ::1/128 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), 128), | ||
// ::fc00::/7 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfc, 0x00, 0, 0, 0, 0}), 7), | ||
// fe80::/10 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfe, 0x80, 0, 0, 0, 0}), 10), | ||
// ::ffff:7F00:1/104 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0x7f, 0x00, 0, 1}), 104), | ||
// ::ffff:a00:0/104 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0x0a, 0x00, 0, 0}), 104), | ||
// ::ffff:ac10:0/108 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xac, 0x10, 0, 0}), 108), | ||
// ::ffff:c0a8:0/112 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xc0, 0xa8, 0, 0}), 112), | ||
// ::ffff:a9fe:0/112 | ||
netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xa9, 0xfe, 0, 0}), 112), | ||
} | ||
|
||
expectedStrings := []string{ | ||
// IPv4 private addresses | ||
"127.0.0.1/8", | ||
"10.0.0.0/8", | ||
"172.16.0.0/12", | ||
"192.168.0.0/16", | ||
"169.254.0.0/16", | ||
// IPv6 private addresses | ||
"::1/128", | ||
"fc00::/7", | ||
"fe80::/10", | ||
// Private IPv4 addresses wrapped in IPv6 | ||
"::ffff:127.0.0.1/104", // 127.0.0.1/8 | ||
"::ffff:10.0.0.0/104", // 10.0.0.0/8 | ||
"::ffff:172.16.0.0/108", // 172.16.0.0/12 | ||
"::ffff:192.168.0.0/112", // 192.168.0.0/16 | ||
"::ffff:169.254.0.0/112", // 169.254.0.0/16 | ||
} | ||
|
||
privateIPNets, err := getPrivateIPPrefixes() | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, len(expectedPrivateIPNets), len(privateIPNets)) | ||
assert.Equal(t, len(expectedStrings), len(privateIPNets)) | ||
|
||
for i := range privateIPNets { | ||
assert.Equal(t, expectedPrivateIPNets[i], privateIPNets[i]) | ||
assert.Equal(t, expectedStrings[i], privateIPNets[i].String()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package dnssec | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/miekg/dns" | ||
"github.com/qdm12/dns/v2/internal/server" | ||
) | ||
|
||
// buildDelegationChain queries the RRs required for the zone validation. | ||
// It begins the queries at the root zone and then go down the delegation | ||
// chain until it reaches the desired zone, or a not signed zone. | ||
// It returns a delegation chain of signed zones where the | ||
// first signed zone (index 0) is the root zone and the last signed | ||
// zone is the last signed zone, which can be the desired zone. | ||
func buildDelegationChain(ctx context.Context, exchange server.Exchange, | ||
desiredZone string, qClass uint16) ( | ||
delegationChain []signedData, err error) { | ||
zoneNames := desiredZoneToZoneNames(desiredZone) | ||
delegationChain = make([]signedData, 0, len(zoneNames)) | ||
|
||
for _, zoneName := range zoneNames { | ||
// zoneName iterates in this order: ., com., example.com. | ||
data, signed, err := queryDelegation(ctx, exchange, zoneName, qClass) | ||
if err != nil { | ||
return nil, fmt.Errorf("querying delegation for desired zone %s: %w", | ||
desiredZone, err) | ||
} | ||
delegationChain = append(delegationChain, data) | ||
if !signed { | ||
// first zone without a DS RRSet, but it should | ||
// have at least one NSEC or NSEC3 RRSet, even for | ||
// NXDOMAIN responses. | ||
break | ||
} | ||
} | ||
|
||
return delegationChain, nil | ||
} | ||
|
||
func desiredZoneToZoneNames(desiredZone string) (zoneNames []string) { | ||
if desiredZone == "." { | ||
return []string{"."} | ||
} | ||
|
||
zoneParts := strings.Split(desiredZone, ".") | ||
zoneNames = make([]string, len(zoneParts)) | ||
for i := range zoneParts { | ||
zoneNames[i] = dns.Fqdn(strings.Join(zoneParts[len(zoneParts)-1-i:], ".")) | ||
} | ||
return zoneNames | ||
} | ||
|
||
// queryDelegation obtains the DS RRSet and the DNSKEY RRSet | ||
// for a given zone and class, and creates a signed zone with | ||
// this information. It does not query the (non existent) | ||
// DS record for the root zone, which is the trust root anchor. | ||
func queryDelegation(ctx context.Context, exchange server.Exchange, | ||
zone string, qClass uint16) (data signedData, signed bool, err error) { | ||
data.zone = zone | ||
data.class = qClass | ||
|
||
// TODO set root zone DS here! | ||
|
||
// do not query DS for root zone since its DS record | ||
// is the trust root anchor. | ||
if zone != "." { | ||
data.dsRRSet, data.dsAuthorityRRSets, err = queryDS(ctx, exchange, zone, qClass) | ||
if err != nil { | ||
return signedData{}, false, fmt.Errorf("querying DS record: %w", err) | ||
} | ||
|
||
if len(data.dsRRSet.rrSet) == 0 { | ||
// If no DS RRSet is found, the entire zone is not signed. | ||
// This also means no DNSKEY RRSet exists, since child zones are | ||
// also not signed, so return with the error errZoneHasNoDSRcord | ||
// to signal the caller to stop the delegation chain queries for | ||
// child zones when encountering a zone with no DS RRSet. | ||
return data, false, nil | ||
} | ||
} | ||
|
||
data.dnsKeyRRSet, err = queryDNSKeys(ctx, exchange, zone, qClass) | ||
if err != nil { | ||
return signedData{}, true, fmt.Errorf("querying DNSKEY record: %w", err) | ||
} | ||
|
||
return data, true, nil | ||
} | ||
|
||
var ( | ||
ErrDSAndNSECAbsent = errors.New("zone has no DS record and no NSEC record") | ||
) | ||
|
||
func queryDS(ctx context.Context, exchange server.Exchange, | ||
zone string, qClass uint16) (dsRRSet dnssecRRSet, | ||
authorityRRSets []dnssecRRSet, err error) { | ||
answerRRSets, authorityRRSets, signed, err := | ||
queryRRSets(ctx, exchange, zone, qClass, dns.TypeDS) | ||
switch { | ||
case err != nil: | ||
return dnssecRRSet{}, nil, err | ||
case !signed: | ||
// no signed DS answer and no NSEC/NSEC3 authority RR | ||
return dnssecRRSet{}, nil, wrapError( | ||
zone, qClass, dns.TypeDS, ErrDSAndNSECAbsent) | ||
case answerRRSets == nil: // NXDOMAIN | ||
// there is one or more NSEC/NSEC3 authority RRSets. | ||
return dnssecRRSet{}, authorityRRSets, nil | ||
case len(answerRRSets) == 0: // NODATA | ||
// there is one or more NSEC/NSEC3 authority RRSets. | ||
return dnssecRRSet{rrSet: []dns.RR{}}, authorityRRSets, nil | ||
} | ||
|
||
// signed answer RRSet(s) | ||
err = rrSetsIsSingleOfType(answerRRSets, dns.TypeDS) | ||
if err != nil { | ||
return dnssecRRSet{}, nil, | ||
wrapError(zone, qClass, dns.TypeDS, err) | ||
} | ||
dsRRSet = answerRRSets[0] | ||
return dsRRSet, nil, nil | ||
} | ||
|
||
// queryDNSKeys queries the DNSKEY records for a given signed zone | ||
// containing a DS RRSet. It returns an error if the DNSKEY RRSet is | ||
// missing or is not signed. NSEC/NSEC3 RRSet also causes an error. | ||
// Note this returns both the ZSK and KSK DNSKey RRs. | ||
func queryDNSKeys(ctx context.Context, exchange server.Exchange, | ||
qname string, qClass uint16) (dnsKeyRRSet dnssecRRSet, err error) { | ||
// DNSKey RRSet(s) should be present so the NSEC/NSEC3 RRSet is ignored. | ||
answerRRSets, _, signed, err := | ||
queryRRSets(ctx, exchange, qname, qClass, dns.TypeDNSKEY) | ||
switch { | ||
case err != nil: | ||
return dnssecRRSet{}, err | ||
case !signed, len(answerRRSets) == 0: | ||
// no signed DNSKEY answer | ||
return dnssecRRSet{}, fmt.Errorf("for %s: %w", | ||
nameClassTypeToString(qname, qClass, dns.TypeDNSKEY), | ||
ErrDNSKeyNotFound) | ||
} | ||
|
||
err = rrSetsIsSingleOfType(answerRRSets, dns.TypeDNSKEY) | ||
if err != nil { | ||
return dnssecRRSet{}, | ||
wrapError(qname, qClass, dns.TypeDNSKEY, err) | ||
} | ||
dnsKeyRRSet = answerRRSets[0] | ||
|
||
return dnsKeyRRSet, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package dnssec | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_desiredZoneToZoneNames(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
desiredZone string | ||
zoneNames []string | ||
}{ | ||
"root": { | ||
desiredZone: ".", | ||
zoneNames: []string{"."}, | ||
}, | ||
"com": { | ||
desiredZone: "com.", | ||
zoneNames: []string{".", "com."}, | ||
}, | ||
"example.com": { | ||
desiredZone: "example.com.", | ||
zoneNames: []string{".", "com.", "example.com."}, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
testCase := testCase | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
zoneNames := desiredZoneToZoneNames(testCase.desiredZone) | ||
assert.Equal(t, testCase.zoneNames, zoneNames) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package dnssec | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/miekg/dns" | ||
"github.com/qdm12/dns/v2/internal/server" | ||
) | ||
|
||
type Client struct { | ||
exchange server.Exchange | ||
} | ||
|
||
func New(exchange server.Exchange) *Client { | ||
return &Client{ | ||
exchange: exchange, | ||
} | ||
} | ||
|
||
func (c *Client) Exchange(ctx context.Context, request *dns.Msg) ( | ||
response *dns.Msg, err error) { | ||
response = new(dns.Msg) | ||
|
||
for _, question := range request.Question { | ||
rrset, err := fetchAndValidateZone(ctx, c.exchange, | ||
question.Name, question.Qclass, question.Qtype) | ||
if err != nil { | ||
return nil, fmt.Errorf("validating %s %s %s: %w", | ||
question.Name, dns.ClassToString[question.Qclass], | ||
dns.TypeToString[question.Qtype], err) | ||
} | ||
|
||
response.Answer = append(response.Answer, rrset...) | ||
} | ||
|
||
return response, nil | ||
} | ||
|
||
func fetchAndValidateZone(ctx context.Context, exchange server.Exchange, | ||
desiredZone string, qClass, qType uint16) (rrset []dns.RR, err error) { | ||
answerRRSets, authorityRRSets, signed, err := queryRRSets(ctx, exchange, | ||
desiredZone, qClass, qType) | ||
if err != nil { | ||
return nil, fmt.Errorf("running desired query: %w", err) | ||
} | ||
|
||
originalDesiredZone := desiredZone | ||
cnameTarget := getCnameTarget(answerRRSets) | ||
if cnameTarget != "" { | ||
desiredZone = cnameTarget | ||
} | ||
|
||
delegationChain, err := buildDelegationChain(ctx, exchange, desiredZone, qClass) | ||
if err != nil { | ||
return nil, fmt.Errorf("building delegation chain for %s: %w", | ||
originalDesiredZone, err) | ||
} | ||
|
||
err = validateWithChain(desiredZone, qType, answerRRSets, | ||
authorityRRSets, signed, delegationChain) | ||
if err != nil { | ||
return nil, fmt.Errorf("for %s: validating answer RRSets"+ | ||
" with delegation chain: %w", | ||
nameClassTypeToString(originalDesiredZone, qClass, qType), err) | ||
} | ||
|
||
if len(answerRRSets) == 0 { | ||
return nil, nil | ||
} | ||
|
||
minRRSetSize := len(answerRRSets) // 1 RR per owner, type and class | ||
rrset = make([]dns.RR, 0, minRRSetSize) | ||
for _, signedRRSet := range answerRRSets { | ||
rrset = append(rrset, signedRRSet.rrSet...) | ||
} | ||
|
||
return rrset, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package dnssec | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
func mustRRToCNAME(rr dns.RR) *dns.CNAME { | ||
cname, ok := rr.(*dns.CNAME) | ||
if !ok { | ||
panic(fmt.Sprintf("RR is of type %T and not of type *dns.CNAME", rr)) | ||
} | ||
return cname | ||
} | ||
|
||
func getCnameTarget(rrSets []dnssecRRSet) (target string) { | ||
for _, rrSet := range rrSets { | ||
if rrSet.qtype() == dns.TypeCNAME { | ||
cname := mustRRToCNAME(rrSet.rrSet[0]) | ||
return cname.Target | ||
} | ||
} | ||
return "" | ||
} |
Oops, something went wrong.