Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Dec 31, 2023
1 parent 46fc699 commit f387bd7
Show file tree
Hide file tree
Showing 32 changed files with 2,171 additions and 698 deletions.
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
FROM qmcgaw/godevcontainer
RUN apk add bind-tools
67 changes: 67 additions & 0 deletions internal/setup/block_test.go
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())
}
}
182 changes: 105 additions & 77 deletions pkg/dnssec/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dnssec

import (
"context"
"errors"
"fmt"
"strings"

Expand All @@ -10,115 +11,142 @@ import (
)

// buildDelegationChain queries the RRs required for the zone validation.
// It begins the queries at the desired zone and then go
// up the delegation tree until it reaches the root zone.
// It returns a new delegation chain of signed zones where the
// first signed zone (index 0) is the child zone and the last signed
// zone is the root zone.
// 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 []signedZone, err error) {
zoneParts := strings.Split(desiredZone, ".")

delegationChain = make([]signedZone, len(zoneParts))
for i := range zoneParts {
zoneName := dns.Fqdn(strings.Join(zoneParts[i:], "."))
delegationChain[i], err = queryDelegation(ctx, exchange, zoneName, qClass)
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
}

// queryDelegation obtains the DNSKEY records and the DS
// records for a given zone, and creates a signed zone with
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.
// DS record for the root zone, which is the trust root anchor.
func queryDelegation(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (sz signedZone, err error) {
sz.zone = zone
sz.class = qClass
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 != "." {
sz.dsRRSet, sz.nsecRRSet, err = queryDS(ctx,
exchange, zone, qClass)
data.dsRRSet, data.dsAuthorityRRSets, err = queryDS(ctx, exchange, zone, qClass)
if err != nil {
return signedZone{}, fmt.Errorf("querying DS record: %w", err)
return signedData{}, false, fmt.Errorf("querying DS record: %w", err)
}
}

sz.dnsKeyRRSet, err = queryDNSKey(ctx,
exchange, zone, qClass)
if err != nil {
return signedZone{}, fmt.Errorf("querying DNSKEY 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
}
}
sz.keyTagToDNSKey = makeKeyTagToDNSKey(sz.dnsKeyRRSet.rrset)

return sz, nil
}

func queryDS(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (signedDSRRSet,
signedNSRRSet signedRRSet, err error) {
signedAnswerRRSets, signedAuthorityRRSets, err :=
fetchSignedRRSets(ctx, exchange, zone, qClass, dns.TypeDS)
data.dnsKeyRRSet, err = queryDNSKeys(ctx, exchange, zone, qClass)
if err != nil {
return signedRRSet{}, signedRRSet{}, err
return signedData{}, true, fmt.Errorf("querying DNSKEY record: %w", err)
}

signedDSRRSet, err = findRRSet(signedAnswerRRSets, dns.TypeDS)
if err != nil {
return signedRRSet{}, signedRRSet{}, fmt.Errorf("for %s: %w",
queryParamsToString(zone, qClass, dns.TypeDS), err)
}
return data, true, nil
}

if len(signedDSRRSet.rrset) > 0 {
return signedDSRRSet, signedRRSet{}, nil
}
var (
ErrDSAndNSECAbsent = errors.New("zone has no DS record and no NSEC record")
)

signedNSRRSet, err = findRRSet(signedAuthorityRRSets,
dns.TypeNSEC3, dns.TypeNSEC)
if err != nil {
return signedRRSet{}, signedRRSet{}, fmt.Errorf("for %s: %w",
queryParamsToString(zone, qClass, dns.TypeDNSKEY), err)
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 len(answerRRSets) == 0:
// there is one or more NSEC/NSEC3 authority RRSets.
return dnssecRRSet{}, authorityRRSets, nil
default:
// signed DS 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
}

return signedDSRRSet, signedNSRRSet, nil
}

func queryDNSKey(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (signedDNSKeyRRSet signedRRSet, err error) {
signedAnswerRRSets, _, err := fetchSignedRRSets(ctx,
exchange, zone, qClass, dns.TypeDNSKEY)
if err != nil {
return signedRRSet{}, err
// 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)
}

signedDNSKeyRRSet, err = findRRSet(signedAnswerRRSets, dns.TypeDNSKEY)
err = rrSetsIsSingleOfType(answerRRSets, dns.TypeDNSKEY)
if err != nil {
return signedRRSet{}, fmt.Errorf("for %s: %w",
queryParamsToString(zone, qClass, dns.TypeDNSKEY), err)
return dnssecRRSet{},
wrapError(qname, qClass, dns.TypeDNSKEY, err)
}
dnsKeyRRSet = answerRRSets[0]

return signedDNSKeyRRSet, nil
}

func makeKeyTagToDNSKey(rrset []dns.RR) (keyTagToDNSKey map[uint16]*dns.DNSKEY) {
keyTagToDNSKey = make(map[uint16]*dns.DNSKEY, len(rrset))
for _, rr := range rrset {
if rr.Header().Rrtype != dns.TypeDNSKEY {
continue
}
dnsKey, ok := rr.(*dns.DNSKEY)
if !ok {
panic(fmt.Sprintf("RR is of type %T and not of type *dns.DNSKEY", rr))
}

keyTagToDNSKey[dnsKey.KeyTag()] = dnsKey
}
return keyTagToDNSKey
return dnsKeyRRSet, nil
}
39 changes: 39 additions & 0 deletions pkg/dnssec/chain_test.go
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)
})
}
}
27 changes: 16 additions & 11 deletions pkg/dnssec/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,34 @@ func (c *Client) Exchange(ctx context.Context, request *dns.Msg) (
}

func fetchAndValidateZone(ctx context.Context, exchange server.Exchange,
zone string, qClass, qType uint16) (rrset []dns.RR, err error) {
signedAnswerRRSets, signedAuthorityRRSets, err := fetchSignedRRSets(
ctx, exchange, zone, qClass, qType)
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("fetching desired zone RRSet with RRSig: %w", err)
return nil, fmt.Errorf("running desired query: %w", err)
}

delegationChain, err := buildDelegationChain(ctx, exchange, zone, qClass)
delegationChain, err := buildDelegationChain(ctx, exchange, desiredZone, qClass)
if err != nil {
return nil, fmt.Errorf("building delegation chain: %w", err)
}

err = verifyWithChain(signedAnswerRRSets, delegationChain)
err = validateWithChain(desiredZone, qType, answerRRSets,
authorityRRSets, signed, delegationChain)
if err != nil {
return nil, fmt.Errorf("for %s: verifying answer RRSets"+
return nil, fmt.Errorf("for %s: validating answer RRSets"+
" with delegation chain: %w",
queryParamsToString(zone, qClass, qType), err)
nameClassTypeToString(desiredZone, qClass, qType), err)
}

minRRSetSize := len(signedAnswerRRSets) // 1 RR per RRSig
if len(answerRRSets) == 0 {
return nil, nil
}

minRRSetSize := len(answerRRSets) // 1 RR per RRSig
rrset = make([]dns.RR, 0, minRRSetSize)
for _, signedRRSet := range signedAnswerRRSets {
rrset = append(rrset, signedRRSet.rrset...)
for _, signedRRSet := range answerRRSets {
rrset = append(rrset, signedRRSet.rrSet...)
}

return rrset, nil
Expand Down
Loading

0 comments on commit f387bd7

Please sign in to comment.