support for dns custom zones

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
Dmitri Dolguikh
2026-07-30 15:34:09 +02:00
parent 5a10561ca1
commit e05cb5264d
4 changed files with 133 additions and 12 deletions

View File

@@ -26,6 +26,7 @@ type NetworkMapDBStore interface {
GetNetworkResources(ctx context.Context, accountId string) ([]nmdata.NetworkResource, error)
GetNetworkRouters(ctx context.Context, accountId string) ([]nmdata.NetworkRouter, error)
GetNetwork(ctx context.Context, accountId string) (nmdata.Network, error)
GetAccountZones(ctx context.Context, accountId string) ([]nmdata.CustomZone, error)
}
type NetworkMapDBStoreImpl struct {

View File

@@ -2,41 +2,101 @@ package networkmap_pgsql
import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"
"github.com/jackc/pgx/v5"
"github.com/miekg/dns"
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
var DnsUnsupportedRecordTypeError = errors.New("unsupported record type")
const (
GetCustomZonesQuery = `
select id, public_id, name, description, name_servers, groups, "primary", domains, enabled, search_domains_enabled
from name_server_groups
where account_id=$1
GetAccountZonesQuery = `
select zones.id as id, domain, enable_search_domain as search_domain_disabled,
r.name as record_name, r.type as record_type, 'IN' record_class, r.ttl as record_ttl, r.content as record_rdata
from zones
left join records as r on r.zone_id = zones.id
where zones.account_id=$1
`
)
func (pg *PgStore) GetCustomZones(ctx context.Context, accountId string) ([]nmdata.NameServerGroup, error) {
rows, err := pg.pool.Query(ctx, GetNameserversQuery, accountId)
func (pg *PgStore) GetAccountZones(ctx context.Context, accountId string) ([]nmdata.CustomZone, error) {
rows, err := pg.pool.Query(ctx, GetAccountZonesQuery, accountId)
if err != nil {
return nil, err
}
nsgroups, err := pgx.CollectRows(rows, pgx.RowToStructByName[nameserverGroup])
zones, err := pgx.CollectRows(rows, pgx.RowToStructByName[zone])
if err != nil {
return nil, err
}
toret := make([]nmdata.NameServerGroup, 0, len(nsgroups))
for _, nsg := range nsgroups {
group := nmdata.NameServerGroup{}
toret := make([]nmdata.CustomZone, 0, len(zones))
currentZoneId := ""
for _, z := range zones {
zone := nmdata.CustomZone{}
err := networkmapdb.FromSqlTypesToSharedTypes(
reflect.ValueOf(&nsg), reflect.ValueOf(&group))
reflect.ValueOf(&z), reflect.ValueOf(&zone))
if err != nil {
return nil, err
}
toret = append(toret, group)
rtype, rdata, err := recordTypeAndRdata(z.RecordType.String, z.RecordRData.String)
if err != nil {
return nil, err
}
record := nmdata.SimpleRecord{
Name: z.RecordName.String,
Class: z.RecordClass.String,
TTL: int(z.RecordTTL.Int64),
RData: rdata,
Type: rtype,
}
zone.Records = []nmdata.SimpleRecord{record}
if len(toret) == 0 {
toret = append(toret, zone)
currentZoneId = z.Id
continue
}
if z.Id == currentZoneId {
lastZone := &toret[len(toret)-1]
lastZone.Records = append(lastZone.Records, record)
continue
}
toret = append(toret, zone)
currentZoneId = z.Id
}
return toret, nil
}
type zone struct {
Id string `nmap:"skip"`
Domain sql.NullString
SearchDomainDisabled sql.NullBool
RecordName sql.NullString `nmap:"skip"`
RecordType sql.NullString `nmap:"skip"`
RecordClass sql.NullString `nmap:"skip"`
RecordTTL sql.NullInt64 `nmap:"skip"`
RecordRData sql.NullString `nmap:"skip"`
}
func recordTypeAndRdata(t, rdata string) (int, string, error) {
switch t {
case "A":
return int(dns.TypeA), rdata, nil
case "AAAA":
return int(dns.TypeAAAA), rdata, nil
case "CNAME":
return int(dns.TypeCNAME), dns.Fqdn(rdata), nil
default:
return 0, "", fmt.Errorf("record type: %s %w", t, DnsUnsupportedRecordTypeError)
}
}

View File

@@ -0,0 +1,38 @@
package networkmap_pgsql
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRecordTypeAndRdata(t *testing.T) {
var tests = []struct {
recordType string
expectedRecordType int
rdata string
expectedRdata string
expectedErr error
}{
{recordType: "A", expectedRecordType: 1, rdata: "test.com", expectedRdata: "test.com", expectedErr: nil},
{recordType: "AAAA", expectedRecordType: 28, rdata: "test.com", expectedRdata: "test.com", expectedErr: nil},
{recordType: "CNAME", expectedRecordType: 5, rdata: "test.com", expectedRdata: "test.com.", expectedErr: nil},
{recordType: "CNAME", expectedRecordType: 5, rdata: "test.com.", expectedRdata: "test.com.", expectedErr: nil},
{recordType: "TypeMX", expectedErr: DnsUnsupportedRecordTypeError},
}
for _, tt := range tests {
t.Run(tt.recordType, func(t *testing.T) {
recordType, rdata, err := recordTypeAndRdata(tt.recordType, tt.rdata)
if tt.expectedErr != nil {
assert.ErrorIs(t, err, DnsUnsupportedRecordTypeError)
return
}
assert.NoError(t, err)
assert.Equal(t, recordType, tt.expectedRecordType)
assert.Equal(t, rdata, tt.expectedRdata)
})
}
}

View File

@@ -193,6 +193,28 @@ func TestGetNetwork(t *testing.T) {
// )
}
func TestGetAccountZones(t *testing.T) {
ctx := context.TODO()
s, err := NewPostgresqlStore(ctx, "postgresql://root:netbird@localhost:5432/netbird")
assert.NoError(t, err)
// err = loadSQL(ctx, s.pool, initDb)
//assert.NoError(t, err)
zones, err := s.GetAccountZones(ctx, "d4g66rjl0ubs73b2q3b0") //"ckd7ee2fic3c73dtendg")
assert.NoError(t, err)
fmt.Print(zones)
// assert.Contains(t,
// groups,
// nmdata.Group{Name: "test-group-1", PublicID: "public-id-1", Resources: []nmdata.Resource{{ID: "cui7q2jl0ubs73d8qpi0", Type: "host"}}},
// )
// assert.Contains(t,
// groups,
// nmdata.Group{Name: "All", PublicID: "d9aejspvcsu517nkh4a0", Resources: []nmdata.Resource{{ID: "cui7olrl0ubs73d8qpe0", Type: "subnet"}, {ID: "cui7q2jl0ubs73d8qpi0", Type: "host"}}},
// )
}
func loadSQL(ctx context.Context, pool *pgxpool.Pool, initdb string) error {
queries := strings.Split(string(initdb), ";")