From e05cb5264dc0d297fc784b7f37f9c45ba7307307 Mon Sep 17 00:00:00 2001 From: Dmitri Dolguikh Date: Thu, 30 Jul 2026 15:34:09 +0200 Subject: [PATCH] support for dns custom zones Signed-off-by: Dmitri Dolguikh --- .../internals/network_map_db/db_store.go | 1 + .../internals/network_map_db/pgsql/dns.go | 84 ++++++++++++++++--- .../network_map_db/pgsql/dns_test.go | 38 +++++++++ .../network_map_db/pgsql/group_test.go | 22 +++++ 4 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 management/internals/network_map_db/pgsql/dns_test.go diff --git a/management/internals/network_map_db/db_store.go b/management/internals/network_map_db/db_store.go index 0598e9bc7..5f6e565cc 100644 --- a/management/internals/network_map_db/db_store.go +++ b/management/internals/network_map_db/db_store.go @@ -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 { diff --git a/management/internals/network_map_db/pgsql/dns.go b/management/internals/network_map_db/pgsql/dns.go index d1074b524..ee4545422 100644 --- a/management/internals/network_map_db/pgsql/dns.go +++ b/management/internals/network_map_db/pgsql/dns.go @@ -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) + } +} diff --git a/management/internals/network_map_db/pgsql/dns_test.go b/management/internals/network_map_db/pgsql/dns_test.go new file mode 100644 index 000000000..62e34e795 --- /dev/null +++ b/management/internals/network_map_db/pgsql/dns_test.go @@ -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) + }) + } +} diff --git a/management/internals/network_map_db/pgsql/group_test.go b/management/internals/network_map_db/pgsql/group_test.go index 39b7c0701..610aaf218 100644 --- a/management/internals/network_map_db/pgsql/group_test.go +++ b/management/internals/network_map_db/pgsql/group_test.go @@ -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), ";")