Skip to content

offload client config generation to the client#6711

Open
dmitri-netbird wants to merge 36 commits into
mainfrom
components-impl-drop-indexes-use-xids
Open

offload client config generation to the client#6711
dmitri-netbird wants to merge 36 commits into
mainfrom
components-impl-drop-indexes-use-xids

Conversation

@dmitri-netbird

@dmitri-netbird dmitri-netbird commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Describe your changes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md.

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features
    • Added component-based network map sync for capable clients, with per-peer payload selection and automatic fallback to the legacy format.
    • Introduced consistent PublicID generation for multiple management objects (accounts, groups, networks, routers, resources, routes, policies, DNS name server groups, and posture checks).
  • Bug Fixes
    • Preserved PublicID across create/update flows and tightened database upsert behavior to avoid overwriting it.
    • Improved SSH activation behavior for component-format peers and corrected route/firewall conversions.
  • Tests
    • Added comprehensive encode/decode, envelope, and SSH decision test coverage, plus wire-size benchmarks/diagnostics.
  • Chores
    • Increased Go lint workflow timeouts and improved SQLite build handling.

crn4 and others added 30 commits May 19, 2026 14:42
# Conflicts:
#	management/server/posture/checks.go
# Conflicts:
#	client/internal/engine.go
#	management/server/store/sql_store.go
#	shared/management/proto/management.pb.go
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7745b149-0202-4aef-b0fd-e3c12e40b8ba

📥 Commits

Reviewing files that changed from the base of the PR and between 5941b26 and fc40eb9.

📒 Files selected for processing (1)
  • shared/management/networkmap/envelope_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shared/management/networkmap/envelope_test.go

📝 Walkthrough

Walkthrough

This PR adds component-based NetworkMap synchronization with server encoding and client decoding, introduces PublicID persistence across management entities, updates shared conversion helpers and generated mocks, and adjusts lint timeout and Dex SQLite build handling.

Changes

Component NetworkMap Format

Layer / File(s) Summary
Component schema and shared helpers
shared/management/proto/management.proto, shared/management/types/*, management/server/types/aliases.go, shared/management/networkmap/encode.go
Adds component wire messages, capability markers, exported firewall helpers, type aliases, and shared NetworkMap protobuf conversions.
Server component envelope generation
management/internals/shared/grpc/components_encoder.go, components_envelope_response.go, *_test.go
Encodes component snapshots, builds component sync responses, evaluates SSH activation, and tests indexing, policies, routes, DNS, proxy patches, and malformed inputs.
Account and controller dispatch
management/server/types/account_components.go, peer_networkmap_result.go, management/internals/controllers/network_map/*, management/internals/shared/grpc/server.go
Selects component or legacy results by peer capability and kill switch, validates component peers, and branches sync responses.
Client envelope pipeline
shared/management/networkmap/decode.go, envelope.go, envelope_test.go, client/internal/engine.go, shared/management/client/*
Decodes envelopes, reconstructs NetworkMaps, caches components, advertises capability support, and handles both sync wire shapes.

PublicID sequence-ID migration

Layer / File(s) Summary
PublicID domain fields
dns/nameserver.go, route/route.go, management/server/networks/**/types/*, shared/management/types/*, management/server/posture/checks.go
Adds PublicID fields and Copy() propagation across management entities.
Create/update flows
management/server/{account,group,nameserver,route,policy,posture_checks}.go, management/server/networks/**/manager.go
Generates identifiers during creation and preserves them during updates.
SQL persistence and validation
management/server/store/sql_store.go, management/server/*_test.go, management/server/types/*_test.go
Reads identifiers from SQL, excludes them from selected updates, tightens group upserts, and validates allocation and preservation.

Generated store mock regeneration

Layer / File(s) Summary
MockStore API regeneration
management/server/store/store_mock.go
Updates generated signatures and adds agent-network store operations.

CI timeout and Dex SQLite build tags

Layer / File(s) Summary
Lint and SQLite build support
.github/workflows/golangci-lint.yml, idp/dex/*
Increases lint timeouts and uses cgo/non-cgo SQLite constructors.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is mostly template placeholders and lacks the required change summary, issue link, and stack details. Fill in the Describe your changes section, add the issue ticket/link and stack info, and explain why docs are not needed.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: moving client config/network-map generation to the client side.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch components-impl-drop-indexes-use-xids

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…indexes-use-xids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Release artifacts

Built for PR head fc40eb9 in workflow run #16601.

Artifact Link
All release artifacts Download
Linux packages Download
Windows packages Download
macOS packages Download
UI artifacts Download
UI macOS artifacts Download

GHCR images (amd64)

This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
management/server/policy.go (1)

61-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unchanged-policy early return skips PublicID assignment.

When policy.Equal(existingPolicy) short-circuits the update, policy.PublicID never gets set from existingPolicy.PublicID, so SavePolicy returns a Policy with an empty PublicID for no-op updates, while an actual update correctly returns the preserved one.

🐛 Proposed fix
 		if isUpdate {
 			if policy.Equal(existingPolicy) {
 				log.WithContext(ctx).Tracef("policy update skipped because equal to stored one - policy id %s", policy.ID)
 				unchanged = true
+				policy.PublicID = existingPolicy.PublicID
 				return nil
 			}

Also applies to: 99-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/policy.go` around lines 61 - 76, The no-op update path in
policy save skips copying the stored PublicID, so the returned policy can have
an empty identifier even when nothing changed. In the policy update flow inside
the save logic for policy persistence, move the PublicID assignment from the
non-equal update branch so it runs before the policy.Equal(existingPolicy) early
return, ensuring the unchanged policy result preserves existingPolicy.PublicID
just like the normal update path.
dns/nameserver.go (1)

51-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

NameServerGroup.Copy() drops PublicID.

Unlike Group.Copy() and Policy.Copy() in this same PR, NameServerGroup.Copy() does not copy the new PublicID field. Since CreateNameServerGroup returns newNSGroup.Copy() to its caller, the freshly-generated PublicID will be silently lost from the response even though it was correctly persisted.

🐛 Proposed fix
 func (g *NameServerGroup) Copy() *NameServerGroup {
 	nsGroup := &NameServerGroup{
 		ID:                   g.ID,
+		AccountID:            g.AccountID,
+		PublicID:             g.PublicID,
 		Name:                 g.Name,
 		Description:          g.Description,
 		NameServers:          make([]NameServer, len(g.NameServers)),
 		Groups:               make([]string, len(g.Groups)),
 		Enabled:              g.Enabled,
 		Primary:              g.Primary,
 		Domains:              make([]string, len(g.Domains)),
 		SearchDomainsEnabled: g.SearchDomainsEnabled,
 	}

Also applies to: 142-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dns/nameserver.go` around lines 51 - 73, NameServerGroup.Copy() is not
preserving the new PublicID field, so the response from CreateNameServerGroup
loses the generated public identifier even though it was saved. Update the
NameServerGroup.Copy method to include PublicID when cloning the struct,
matching the behavior of Group.Copy and Policy.Copy, and ensure
CreateNameServerGroup still returns the copied object with PublicID intact.
management/server/store/sql_store.go (2)

2394-2409: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Map account_seq_id to a struct field or stop using RowToStructByName here getNetworks will fail on the account_seq_id column because networkTypes.Network has no matching field/tag (PublicID is untagged). Use explicit Scan, or add a pgx db:"account_seq_id" tag on PublicID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/store/sql_store.go` around lines 2394 - 2409, The
getNetworks function is using pgx.RowToStructByName against the networks query,
but networkTypes.Network has no matching field/tag for account_seq_id, so row
mapping will fail. Fix this either by adding the appropriate db tag to the
PublicID field in networkTypes.Network or by replacing RowToStructByName in
getNetworks with explicit row scanning so the account_seq_id column is handled
correctly.

2206-2225: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Map PublicID to account_seq_id in these models

getPostureChecks, getNetworkRouters, and getNetworkResources scan account_seq_id into .PublicID, but management/server/posture/checks.go, management/server/networks/routers/types/router.go, and management/server/networks/resources/types/resource.go leave PublicID unmapped (json:"-" only). With the current Save/Create/Updates calls, GORM will target public_id, so the reads and writes are using different columns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/store/sql_store.go` around lines 2206 - 2225, The reads and
writes for PublicID are mismatched because getPostureChecks, getNetworkRouters,
and getNetworkResources scan account_seq_id into PublicID while the related GORM
models leave PublicID unmapped. Update the affected model structs
(posture.Checks, router types, and resource types) so PublicID is bound to
account_seq_id instead of the default public_id, and keep the SQL scan logic
aligned with that field. Verify the Save/Create/Updates paths now target the
same column that the getters populate.
management/server/networks/resources/types/resource.go (1)

31-44: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Map NetworkResource.PublicID to account_seq_id getNetworkResources reads account_seq_id into PublicID, but SaveNetworkResource will persist the field to public_id by default. That leaves the value out of sync on round-trip; add the same column tag as Network.PublicID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/networks/resources/types/resource.go` around lines 31 - 44,
`NetworkResource.PublicID` is being read from account_seq_id but saved to the
default public_id column, causing round-trip mismatch. Update the
`NetworkResource` struct to use the same GORM column mapping as
`Network.PublicID`, and verify `SaveNetworkResource` and `getNetworkResources`
both target the same database field via `NetworkResource.PublicID`.
management/server/posture/checks.go (1)

39-56: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Map PublicID to account_seq_id

SavePostureChecks uses GORM Save, while getPostureChecks scans account_seq_id into PublicID. Without an explicit gorm:"column:account_seq_id" tag, GORM will target public_id instead, so this value won’t round-trip.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/posture/checks.go` around lines 39 - 56, The Checks model’s
PublicID field is not mapped to the database column that getPostureChecks reads
from, so SavePostureChecks will persist to the wrong column. Update the PublicID
field in Checks to explicitly use gorm:"column:account_seq_id" (keeping the
existing JSON behavior if needed) so GORM round-trips the value consistently
between SavePostureChecks and getPostureChecks.
management/server/networks/routers/types/router.go (1)

12-22: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Map NetworkRouter.PublicID to account_seq_id
CreateNetworkRouter and UpdateNetworkRouter use plain GORM, while the router read paths load account_seq_id. Without a column tag, PublicID is written to public_id instead.

Suggested change
-	PublicID   string `json:"-"`
+	PublicID   string `gorm:"column:account_seq_id" db:"account_seq_id" json:"-"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/networks/routers/types/router.go` around lines 12 - 22, The
NetworkRouter.PublicID field is currently being persisted to the default
public_id column instead of account_seq_id. Update the NetworkRouter struct so
the PublicID field in router.go is explicitly mapped with the correct GORM
column tag, and verify CreateNetworkRouter and UpdateNetworkRouter continue
using that struct so reads and writes stay consistent with account_seq_id.
🧹 Nitpick comments (2)
shared/management/proto/management.proto (1)

939-943: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Don’t reserve future delta field numbers here. reserved 1 to 100 permanently blocks those numbers from being added later; keep the message empty until the delta schema is defined, or reserve only deleted field numbers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/management/proto/management.proto` around lines 939 - 943, The
NetworkMapComponentsDelta message is unnecessarily reserving future field
numbers, which prevents those delta fields from being added later. Update the
message definition in management.proto so NetworkMapComponentsDelta remains
empty until the schema is defined, or only use reserved numbers for fields that
have been removed; keep the change focused on the message itself and its
reserved field declaration.
management/server/store/sql_store.go (1)

645-710: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CreateGroups and UpdateGroups are now identical — consider consolidating.

Both functions perform the exact same OnConflict/DoUpdates(groupUpsertColumns())/Omit(clause.Associations)/Create sequence. Extracting a shared private helper (e.g. upsertGroups) would remove the duplication without changing behavior.

♻️ Proposed refactor
+func (s *SqlStore) upsertGroups(ctx context.Context, accountID string, groups []*types.Group) error {
+	if len(groups) == 0 {
+		return nil
+	}
+	return s.db.Transaction(func(tx *gorm.DB) error {
+		result := tx.
+			Clauses(
+				clause.OnConflict{
+					Columns:   []clause.Column{{Name: "id"}},
+					Where:     clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
+					DoUpdates: groupUpsertColumns(),
+				},
+			).
+			Omit(clause.Associations).
+			Create(&groups)
+		if result.Error != nil {
+			log.WithContext(ctx).Errorf("failed to save groups to store: %v", result.Error)
+			return status.Errorf(status.Internal, "failed to save groups to store")
+		}
+		return nil
+	})
+}
+
+func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {
+	return s.upsertGroups(ctx, accountID, groups)
+}
+
+func (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error {
+	return s.upsertGroups(ctx, accountID, groups)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/store/sql_store.go` around lines 645 - 710, CreateGroups
and UpdateGroups contain the same upsert flow, so consolidate the duplicated
logic into a shared private helper such as upsertGroups in SqlStore. Move the
common transaction, OnConflict with groupUpsertColumns,
Omit(clause.Associations), and Create call into that helper, then have both
CreateGroups and UpdateGroups delegate to it while preserving the existing error
logging and status return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@management/internals/controllers/network_map/controller/controller.go`:
- Around line 603-607: The initial sync path in `GetPeerNetworkMapResult` can
return nil components when `account.GetPeerNetworkMapComponents` yields nil,
which causes `ToComponentSyncResponse` to fall back to a zero network instead of
the account network identifier. Mirror the nil-components fallback used in the
update dispatch path by replacing nil `components` with a component set that
preserves the account network before returning from `GetPeerNetworkMapResult`,
so `peer`, `components`, and the resulting sync response always carry the
correct account network context.
- Line 570: The initial-sync path in the network map controller is bypassing the
same proxy-policy injection flow used elsewhere, so update the component setup
to follow the existing controller-level path. In the controller logic around
account setup, replace the direct account.InjectProxyPolicies call with the same
injectAllProxyPolicies path used by streaming updates and legacy initial sync,
so controller-level proxy policy injection happens before building components.

In `@management/internals/shared/grpc/components_envelope_response.go`:
- Around line 66-70: The component sync response in SyncResponse is missing
SessionExpiresAt, unlike the legacy path, so peers can miss login-expiration
updates. Update the response construction in the component envelope flow to
populate SessionExpiresAt whenever settings are available, matching the behavior
used in the legacy response path. Use the existing SyncResponse assembly and the
shared settings data source so the component-capable path preserves the same
session expiry information.

In `@management/internals/shared/grpc/conversion_test.go`:
- Around line 110-114: The “WithoutCache” benchmark is still exercising cache
allocation and population on every iteration instead of the true no-cache path.
Update the benchmark case in the `b.Run("WithoutCache...")` block that calls
`networkmap.ToProtocolDNSConfig` so it passes a nil cache rather than
constructing `cache.DNSConfigCache` inside the loop, keeping the benchmark
focused on the actual no-cache behavior.

In `@management/internals/shared/grpc/server.go`:
- Around line 1019-1043: sendInitialSync is rebuilding the response with data
from GetValidatedPeerWithComponents but still passes the stale postureChecks
snapshot into ToComponentSyncResponse. Update the initial-sync branch in
sendInitialSync to use the checks returned by GetValidatedPeerWithComponents
when constructing the component response, and keep the fallback ToSyncResponse
path unchanged so both components and checks come from the same validated read.

In `@management/server/networks/types/network.go`:
- Around line 9-17: The Network.PublicID field is missing the explicit column
mapping needed for both GORM persistence and pgx row scanning, so it currently
resolves to public_id instead of account_seq_id. Update the Network struct to
add the appropriate db/gorm tag on PublicID so sql_store.go’s getNetworks and
pgx.RowToStructByName[networkTypes.Network] can match the selected
account_seq_id column, and ensure SaveNetwork round-trips the same field name
used by the database schema.

In `@management/server/types/account_components.go`:
- Around line 330-383: The route advertiser lookup in the relevant-route
assembly logic is using the WireGuard key field instead of the peer ID, so the
peer map lookup can miss valid advertisers. Update the `Route` handling in
`account_components.go` to use `Route.PeerID` consistently when checking
`validatedPeersMap` and calling `a.GetPeer`, while keeping the existing
relevance filtering and `relevantPeerIDs` population behavior intact.

In `@route/route.go`:
- Line 98: The Route.PublicID field is currently not mapped to the
account_seq_id column, so SaveRoute will persist it to the default public_id
field instead. Update the Route model in route.Route to add the proper GORM
column override for PublicID, or adjust SaveRoute to avoid saving that field
through s.db.Save(route) if it should be excluded. Use the Route struct and
SaveRoute method to locate the write path and ensure PublicID consistently maps
to account_seq_id.

In `@shared/management/client/client_test.go`:
- Around line 371-382: The helper is reading NetworkMapEnvelope.Full.Peers
directly, which tests the component peer universe instead of the actual remote
peer list. Update the test/helper around the existing envelope handling to
decode the envelope with EnvelopeToNetworkMap and assert against
result.NetworkMap.RemotePeers, using the current peer/key filtering logic in the
same function to keep the comparison aligned with the real NetworkMap view.

In `@shared/management/networkmap/decode.go`:
- Around line 110-112: The peer index handling in decode.go is appending empty
peer IDs whenever `peerIDByIndex[idx]` was never resolved, so update the lookup
logic used by `GroupCodec.Decode`, `PeerIndexes`, and the later map/resource
builders to skip both out-of-bounds indexes and empty IDs. Add a small shared
resolver/helper around `peerIDByIndex` access, then use it at the affected
append sites so groups, posture failures, router maps, routes, and resources
never store unresolved peer IDs.
- Around line 486-496: The decode helpers are currently unused, causing lint
failures: either remove the dead helpers or make them part of the decode flow.
Update the `synthID` function in `decode.go` and the unused `lookupAgentVersion`
symbol so they are either referenced from the relevant decode path or deleted if
no longer needed, keeping the naming and call sites consistent with the existing
network map decoding logic.

In `@shared/management/networkmap/envelope.go`:
- Around line 51-54: Handle the empty decoded peer set in the envelope decoding
flow before looking up localPeer in the NetworkMap/Envelope handling path.
Update the logic around components.Peers and localPeer so that when no peers are
present, full.PeerConfig still returns a minimal proto.NetworkMap populated from
the envelope’s DNS/proxy fields instead of erroring, and keep the current error
only for non-empty snapshots where the receiver is genuinely missing.

In `@shared/management/proto/management.proto`:
- Around line 1160-1170: The wire-type comment on NetworkResourceRaw is stale
and does not match the actual schema. Update the generated contract comment
above the NetworkResourceRaw message so it describes field 2 as string
network_seq, keeping the rest of the compatibility note intact and aligned with
the current proto definition.

In `@shared/management/types/firewall_helpers.go`:
- Around line 113-116: Replace the string-substring check in
peerSupportedFirewallFeatures with version.IsDevelopmentVersion() so development
builds are detected consistently instead of matching arbitrary “dev” substrings.
Update the peerVer handling in that function to use the version helper and keep
the existing supportedFeatures return behavior unchanged for development
versions.

---

Outside diff comments:
In `@dns/nameserver.go`:
- Around line 51-73: NameServerGroup.Copy() is not preserving the new PublicID
field, so the response from CreateNameServerGroup loses the generated public
identifier even though it was saved. Update the NameServerGroup.Copy method to
include PublicID when cloning the struct, matching the behavior of Group.Copy
and Policy.Copy, and ensure CreateNameServerGroup still returns the copied
object with PublicID intact.

In `@management/server/networks/resources/types/resource.go`:
- Around line 31-44: `NetworkResource.PublicID` is being read from
account_seq_id but saved to the default public_id column, causing round-trip
mismatch. Update the `NetworkResource` struct to use the same GORM column
mapping as `Network.PublicID`, and verify `SaveNetworkResource` and
`getNetworkResources` both target the same database field via
`NetworkResource.PublicID`.

In `@management/server/networks/routers/types/router.go`:
- Around line 12-22: The NetworkRouter.PublicID field is currently being
persisted to the default public_id column instead of account_seq_id. Update the
NetworkRouter struct so the PublicID field in router.go is explicitly mapped
with the correct GORM column tag, and verify CreateNetworkRouter and
UpdateNetworkRouter continue using that struct so reads and writes stay
consistent with account_seq_id.

In `@management/server/policy.go`:
- Around line 61-76: The no-op update path in policy save skips copying the
stored PublicID, so the returned policy can have an empty identifier even when
nothing changed. In the policy update flow inside the save logic for policy
persistence, move the PublicID assignment from the non-equal update branch so it
runs before the policy.Equal(existingPolicy) early return, ensuring the
unchanged policy result preserves existingPolicy.PublicID just like the normal
update path.

In `@management/server/posture/checks.go`:
- Around line 39-56: The Checks model’s PublicID field is not mapped to the
database column that getPostureChecks reads from, so SavePostureChecks will
persist to the wrong column. Update the PublicID field in Checks to explicitly
use gorm:"column:account_seq_id" (keeping the existing JSON behavior if needed)
so GORM round-trips the value consistently between SavePostureChecks and
getPostureChecks.

In `@management/server/store/sql_store.go`:
- Around line 2394-2409: The getNetworks function is using pgx.RowToStructByName
against the networks query, but networkTypes.Network has no matching field/tag
for account_seq_id, so row mapping will fail. Fix this either by adding the
appropriate db tag to the PublicID field in networkTypes.Network or by replacing
RowToStructByName in getNetworks with explicit row scanning so the
account_seq_id column is handled correctly.
- Around line 2206-2225: The reads and writes for PublicID are mismatched
because getPostureChecks, getNetworkRouters, and getNetworkResources scan
account_seq_id into PublicID while the related GORM models leave PublicID
unmapped. Update the affected model structs (posture.Checks, router types, and
resource types) so PublicID is bound to account_seq_id instead of the default
public_id, and keep the SQL scan logic aligned with that field. Verify the
Save/Create/Updates paths now target the same column that the getters populate.

---

Nitpick comments:
In `@management/server/store/sql_store.go`:
- Around line 645-710: CreateGroups and UpdateGroups contain the same upsert
flow, so consolidate the duplicated logic into a shared private helper such as
upsertGroups in SqlStore. Move the common transaction, OnConflict with
groupUpsertColumns, Omit(clause.Associations), and Create call into that helper,
then have both CreateGroups and UpdateGroups delegate to it while preserving the
existing error logging and status return behavior.

In `@shared/management/proto/management.proto`:
- Around line 939-943: The NetworkMapComponentsDelta message is unnecessarily
reserving future field numbers, which prevents those delta fields from being
added later. Update the message definition in management.proto so
NetworkMapComponentsDelta remains empty until the schema is defined, or only use
reserved numbers for fields that have been removed; keep the change focused on
the message itself and its reserved field declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 161524be-8ea6-4562-8f7e-e22e045958f5

📥 Commits

Reviewing files that changed from the base of the PR and between 2560c6b and ee7806b.

⛔ Files ignored due to path filters (1)
  • shared/management/proto/management.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (67)
  • .github/workflows/golangci-lint.yml
  • client/internal/engine.go
  • dns/nameserver.go
  • idp/dex/config.go
  • idp/dex/provider.go
  • idp/dex/sqlite_cgo.go
  • idp/dex/sqlite_nocgo.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/controllers/network_map/interface.go
  • management/internals/controllers/network_map/interface_mock.go
  • management/internals/shared/grpc/components_encoder.go
  • management/internals/shared/grpc/components_encoder_test.go
  • management/internals/shared/grpc/components_envelope_response.go
  • management/internals/shared/grpc/components_envelope_response_test.go
  • management/internals/shared/grpc/conversion.go
  • management/internals/shared/grpc/conversion_test.go
  • management/internals/shared/grpc/server.go
  • management/server/account.go
  • management/server/account_test.go
  • management/server/group.go
  • management/server/nameserver.go
  • management/server/networks/manager.go
  • management/server/networks/manager_test.go
  • management/server/networks/resources/manager.go
  • management/server/networks/resources/types/resource.go
  • management/server/networks/routers/manager.go
  • management/server/networks/routers/types/router.go
  • management/server/networks/types/network.go
  • management/server/peer/peer.go
  • management/server/policy.go
  • management/server/posture/checks.go
  • management/server/posture_checks.go
  • management/server/posture_checks_test.go
  • management/server/route.go
  • management/server/store/sql_store.go
  • management/server/store/store_mock.go
  • management/server/store/store_mock_agentnetwork.go
  • management/server/types/account.go
  • management/server/types/account_components.go
  • management/server/types/account_test.go
  • management/server/types/aliases.go
  • management/server/types/networkmap_components_correctness_test.go
  • management/server/types/networkmap_wire_benchmark_test.go
  • management/server/types/networkmap_wire_breakdown_test.go
  • management/server/types/peer_networkmap_result.go
  • management/server/types/peer_networkmap_result_test.go
  • route/route.go
  • shared/management/client/client_test.go
  • shared/management/client/grpc.go
  • shared/management/networkmap/decode.go
  • shared/management/networkmap/encode.go
  • shared/management/networkmap/envelope.go
  • shared/management/networkmap/envelope_test.go
  • shared/management/proto/management.proto
  • shared/management/types/dns_settings.go
  • shared/management/types/firewall_helpers.go
  • shared/management/types/firewall_rule.go
  • shared/management/types/firewall_rule_test.go
  • shared/management/types/group.go
  • shared/management/types/network.go
  • shared/management/types/network_test.go
  • shared/management/types/networkmap_components.go
  • shared/management/types/networkmap_components_compact.go
  • shared/management/types/policy.go
  • shared/management/types/policyrule.go
  • shared/management/types/resource.go
  • shared/management/types/route_firewall_rule.go
💤 Files with no reviewable changes (1)
  • management/server/store/store_mock_agentnetwork.go

return nil, nil, nil, nil, 0, err
}

account.InjectProxyPolicies(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match the existing proxy-policy injection path.

Streaming updates and legacy initial sync call c.injectAllProxyPolicies; this component initial-sync path only calls account.InjectProxyPolicies, which can omit controller-level proxy policy injection before building components.

Proposed fix
-	account.InjectProxyPolicies(ctx)
+	c.injectAllProxyPolicies(ctx, account)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
account.InjectProxyPolicies(ctx)
c.injectAllProxyPolicies(ctx, account)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/internals/controllers/network_map/controller/controller.go` at
line 570, The initial-sync path in the network map controller is bypassing the
same proxy-policy injection flow used elsewhere, so update the component setup
to follow the existing controller-level path. In the controller logic around
account setup, replace the direct account.InjectProxyPolicies call with the same
injectAllProxyPolicies path used by streaming updates and legacy initial sync,
so controller-level proxy policy injection happens before building components.

Comment on lines +603 to +607
components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)

dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)

return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the same nil-components fallback used by update dispatch.

If GetPeerNetworkMapComponents returns nil, this path returns nil components and ToComponentSyncResponse falls back to a zero network. Mirror GetPeerNetworkMapResult so initial sync still carries the account network identifier.

Proposed fix
 	components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
+	if components == nil {
+		components = &types.NetworkMapComponents{
+			PeerID:  peer.ID,
+			Network: account.Network.Copy(),
+		}
+	}
 
 	dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
if components == nil {
components = &types.NetworkMapComponents{
PeerID: peer.ID,
Network: account.Network.Copy(),
}
}
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/internals/controllers/network_map/controller/controller.go` around
lines 603 - 607, The initial sync path in `GetPeerNetworkMapResult` can return
nil components when `account.GetPeerNetworkMapComponents` yields nil, which
causes `ToComponentSyncResponse` to fall back to a zero network instead of the
account network identifier. Mirror the nil-components fallback used in the
update dispatch path by replacing nil `components` with a component set that
preserves the account network before returning from `GetPeerNetworkMapResult`,
so `peer`, `components`, and the resulting sync response always carry the
correct account network context.

Comment thread management/internals/shared/grpc/components_envelope_response.go
Comment on lines 110 to +114
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
cache := &cache.DNSConfigCache{}
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Benchmark the actual no-cache path.

ToProtocolDNSConfig supports nil for no caching, but this “WithoutCache” case allocates and populates a cache on every iteration.

Suggested fix
 	b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
-			cache := &cache.DNSConfigCache{}
-			networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
+			networkmap.ToProtocolDNSConfig(testData, nil, int64(network_map.DnsForwarderPort))
 		}
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
cache := &cache.DNSConfigCache{}
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
networkmap.ToProtocolDNSConfig(testData, nil, int64(network_map.DnsForwarderPort))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/internals/shared/grpc/conversion_test.go` around lines 110 - 114,
The “WithoutCache” benchmark is still exercising cache allocation and population
on every iteration instead of the true no-cache path. Update the benchmark case
in the `b.Run("WithoutCache...")` block that calls
`networkmap.ToProtocolDNSConfig` so it passes a nil cache rather than
constructing `cache.DNSConfigCache` inside the loop, keeping the benchmark
focused on the actual no-cache behavior.

Comment thread management/internals/shared/grpc/server.go
Comment on lines +110 to +112
for _, idx := range gc.PeerIndexes {
if int(idx) < len(peerIDByIndex) {
peerIDs = append(peerIDs, peerIDByIndex[idx])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Skip unresolved peer indexes instead of emitting empty peer IDs.

Malformed peers are skipped by leaving peerIDByIndex[idx] == "", but later phases still append/store that empty ID when the index is in range. This can leak empty peer IDs into groups, posture failures, router maps, routes, and resources. Add a small resolver that checks both bounds and non-empty IDs before use.

Suggested direction
+func peerIDAt(peerIDByIndex []string, idx uint32) (string, bool) {
+	if uint64(idx) >= uint64(len(peerIDByIndex)) {
+		return "", false
+	}
+	peerID := peerIDByIndex[int(idx)]
+	return peerID, peerID != ""
+}

Also applies to: 169-174, 220-223, 235-238, 362-364, 408-410

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/management/networkmap/decode.go` around lines 110 - 112, The peer
index handling in decode.go is appending empty peer IDs whenever
`peerIDByIndex[idx]` was never resolved, so update the lookup logic used by
`GroupCodec.Decode`, `PeerIndexes`, and the later map/resource builders to skip
both out-of-bounds indexes and empty IDs. Add a small shared resolver/helper
around `peerIDByIndex` access, then use it at the affected append sites so
groups, posture failures, router maps, routes, and resources never store
unresolved peer IDs.

Comment thread shared/management/networkmap/decode.go Outdated
Comment on lines +51 to +54
localPeer := components.Peers[canonicalKey]
if localPeer == nil {
return nil, fmt.Errorf("receiving peer (wg_key prefix %q) not found among %d decoded peers — components have no PeerID, Calculate would return empty", trimKey(localPeerKey), len(components.Peers))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle minimal envelopes before requiring a local peer.

The server encoder can intentionally send a Full envelope with no components for missing/unvalidated peers, but this path returns an error when components.Peers is empty. That makes the graceful-degrade envelope fail sync instead of yielding the minimal NetworkMap.

Return a minimal proto.NetworkMap from full.PeerConfig/DNS/proxy fields when the decoded component set has no peers, and reserve this error for non-empty snapshots that genuinely omit the receiver.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/management/networkmap/envelope.go` around lines 51 - 54, Handle the
empty decoded peer set in the envelope decoding flow before looking up localPeer
in the NetworkMap/Envelope handling path. Update the logic around
components.Peers and localPeer so that when no peers are present,
full.PeerConfig still returns a minimal proto.NetworkMap populated from the
envelope’s DNS/proxy fields instead of erroring, and keep the current error only
for non-empty snapshots where the receiver is genuinely missing.

Comment on lines +1160 to +1170
// NetworkResourceRaw mirrors *resourceTypes.NetworkResource.
//
// INCOMPATIBLE WIRE CHANGE: field 2 changed from `string network_id` (xid)
// to `uint32 network_seq` without a `reserved` entry. Safe only because
// capability=3 has never been released — every cap=3 producer and consumer
// carries the same regenerated descriptor. Do NOT reuse this pattern once
// cap=3 ships.
message NetworkResourceRaw {
string id = 1; // network_resources.account_seq_id
string network_seq = 2; // networks.account_seq_id (replaces xid)
string name = 3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Correct the stale wire-type comment.

The comment says field 2 changed to uint32 network_seq, but the schema defines string network_seq = 2. This is a generated contract comment, so it should match the actual wire type.

Suggested fix
-// INCOMPATIBLE WIRE CHANGE: field 2 changed from `string network_id` (xid)
-// to `uint32 network_seq` without a `reserved` entry. Safe only because
+// INCOMPATIBLE SEMANTIC CHANGE: field 2 changed from `string network_id` (xid)
+// to `string network_seq` without a `reserved` entry. Safe only because
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// NetworkResourceRaw mirrors *resourceTypes.NetworkResource.
//
// INCOMPATIBLE WIRE CHANGE: field 2 changed from `string network_id` (xid)
// to `uint32 network_seq` without a `reserved` entry. Safe only because
// capability=3 has never been released — every cap=3 producer and consumer
// carries the same regenerated descriptor. Do NOT reuse this pattern once
// cap=3 ships.
message NetworkResourceRaw {
string id = 1; // network_resources.account_seq_id
string network_seq = 2; // networks.account_seq_id (replaces xid)
string name = 3;
// NetworkResourceRaw mirrors *resourceTypes.NetworkResource.
//
// INCOMPATIBLE SEMANTIC CHANGE: field 2 changed from `string network_id` (xid)
// to `string network_seq` without a `reserved` entry. Safe only because
// capability=3 has never been released — every cap=3 producer and consumer
// carries the same regenerated descriptor. Do NOT reuse this pattern once
// cap=3 ships.
message NetworkResourceRaw {
string id = 1; // network_resources.account_seq_id
string network_seq = 2; // networks.account_seq_id (replaces xid)
string name = 3;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/management/proto/management.proto` around lines 1160 - 1170, The
wire-type comment on NetworkResourceRaw is stale and does not match the actual
schema. Update the generated contract comment above the NetworkResourceRaw
message so it describes field 2 as string network_seq, keeping the rest of the
compatibility note intact and aligned with the current proto definition.

Comment on lines +113 to +116
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
if strings.Contains(peerVer, "dev") {
return supportedFeatures{true, true}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the version package and check for potential circular imports
fd -t f "version" --max-depth 2 | head -20
rg -n "func IsDevelopmentVersion" --type=go | head -5
# Check if shared/management/types already imports anything that would cycle with version
rg -n "github.com/netbirdio/netbird/version" shared/management/types/ | head -10

Repository: netbirdio/netbird

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== version/version.go ==\n'
cat -n version/version.go | sed -n '1,220p'

printf '\n== shared/management/types/firewall_helpers.go ==\n'
cat -n shared/management/types/firewall_helpers.go | sed -n '1,220p'

printf '\n== imports of shared/management/types ==\n'
rg -n '^import \(' -A20 shared/management/types/firewall_helpers.go

printf '\n== IsDevelopmentVersion references ==\n'
rg -n 'IsDevelopmentVersion|development' version shared/management/types -g '*.go'

Repository: netbirdio/netbird

Length of output: 9594


Use version.IsDevelopmentVersion() here. It avoids matching arbitrary substrings like 0.50.0-dev and keeps dev/CI build handling consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/management/types/firewall_helpers.go` around lines 113 - 116, Replace
the string-substring check in peerSupportedFirewallFeatures with
version.IsDevelopmentVersion() so development builds are detected consistently
instead of matching arbitrary “dev” substrings. Update the peerVer handling in
that function to use the version helper and keep the existing supportedFeatures
return behavior unchanged for development versions.

Source: Learnings

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 New issue
1 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants