Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Jan 2, 2024
1 parent 09e170e commit 9a9fcac
Show file tree
Hide file tree
Showing 38 changed files with 2,824 additions and 2 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
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.0-rc4 h1:t4cZW/63EngMCzddQ+Je7aXKcPC7Poh/4IqxcbCBzgw=
github.com/qdm12/gosettings v0.4.0-rc4/go.mod h1:kZtFrO3sfWmMGG3scQB3GcawGe41LKo/4dR4h0dG104=
github.com/qdm12/gosettings v0.4.0-rc5 h1:DZ4PjfF/Xtx0QGMbinNPr7xRf0rtLfkIg4zZNXUoypY=
github.com/qdm12/gosettings v0.4.0-rc5/go.mod h1:kZtFrO3sfWmMGG3scQB3GcawGe41LKo/4dR4h0dG104=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
Expand Down
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())
}
}
155 changes: 155 additions & 0 deletions pkg/dnssec/chain.go
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
}
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)
})
}
}
79 changes: 79 additions & 0 deletions pkg/dnssec/client.go
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
}
25 changes: 25 additions & 0 deletions pkg/dnssec/cname.go
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 ""
}
Loading

0 comments on commit 9a9fcac

Please sign in to comment.