Skip to content

[Feat] Networks: private networking via WireGuard, custom, and provider-synced VPCs#1209

Open
RichardAnderson wants to merge 32 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/networks
Open

[Feat] Networks: private networking via WireGuard, custom, and provider-synced VPCs#1209
RichardAnderson wants to merge 32 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/networks

Conversation

@RichardAnderson

@RichardAnderson RichardAnderson commented Jul 24, 2026

Copy link
Copy Markdown
Member

Adds project-scoped private networks that link servers either over Vito-managed WireGuard tunnels (with peer devices for laptops and CI runners), over private IPs you already have, or by discovering and syncing existing VPCs from AWS, DigitalOcean, Hetzner, Linode, and Vultr, each network carrying its own firewall rules that Vito keeps applied across every member.

View networks

image

View network details

image

Summary by CodeRabbit

  • New Features
    • Added full Networks management (WireGuard, Custom, and provider-discovered networks) including servers, peers, firewall rules, and automatic reconciliation/sync with status tracking and retries.
  • Documentation
    • Added/expanded Networks docs for overview, creating networks, peers, servers, firewalls, and provider networks, plus automation notes.
  • Bug Fixes
    • Improved firewall rule application to correctly handle empty protocol/port values, improved managed-network firewall visibility, and enhanced navigation “active” highlighting (including exact-match routes).

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete network management platform covering WireGuard and provider networks, CIDR allocation, firewall rule materialisation, queued synchronisation, peer management, HTTP/Inertia interfaces, reconciliation scheduling, provider discovery, tests, and documentation.

Changes

Network lifecycle and synchronisation

Layer / File(s) Summary
Network domain and persistence
app/Actions/Network/*, app/Models/Network*.php, app/Enums/Network*.php, database/migrations/*network*
Adds network types, statuses, persistence models, CIDR allocation, creation, updates, membership changes, deletion, and status recomputation.
WireGuard execution and peer lifecycle
app/Services/VPN/*, app/Jobs/Network/*, resources/views/ssh/wireguard/*, resources/views/wireguard/*
Adds WireGuard installation, configuration, teardown, peer keys, peer configuration, handshake polling, queued synchronisation, and failure handling.
Provider private-network discovery
app/ServerProviders/*, app/DTOs/PrivateNetwork*.php, app/Exceptions/PrivateNetworkSyncError.php
Adds provider contracts and private-network discovery/mapping for AWS, DigitalOcean, Hetzner, Linode, and Vultr, including pagination and provider error handling.
Firewall rule orchestration
app/Actions/Network/MaterializeServerNetworkRules.php, app/Actions/Network/ApplyNetworkFirewall.php, app/Services/Firewall/Ufw.php, app/Models/ServerNetworkRule.php
Materialises network rules, applies them through UFW, tracks provisioning states, and exposes managed rules on server firewall pages.
HTTP and Inertia surfaces
app/Http/Controllers/Network*.php, resources/js/pages/networks/*, resources/js/components/dialogs/registry.ts, resources/js/types/network.d.ts
Adds network, server-member, firewall-rule, and peer routes, pages, dialogs, resources, tables, layouts, forms, and typed client models.
Reconciliation and validation coverage
app/Console/Commands/ReconcileNetworksCommand.php, app/Traits/UniqueQueue.php, tests/Feature/Network*Test.php, tests/Feature/ProviderPrivateNetworkTest.php
Adds scheduled recovery for pending, failed, stale, and leaving members, transient queue retries, and feature coverage for lifecycle, provider, firewall, peer, and WireGuard behaviour.
Documentation and supporting updates
docs/4.x/networks/*, docs/4.x/automation.md, app/Actions/Service/Install.php, resources/js/components/app-sidebar.tsx
Documents network operation and updates navigation, service installation filtering, path handling, formatting, and ignore rules.

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

Possibly related PRs

Suggested reviewers: saeedvaziry

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding private networking via WireGuard, custom networks, and provider-synced VPCs.
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 unit tests (beta)
  • Create PR with unit tests

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.

Copilot AI 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.

Pull request overview

Adds first-class, project-scoped “Networks” to Vito, supporting Vito-managed WireGuard tunnels (with peers), custom private-IP networks, and provider-synced VPCs, plus network-scoped firewall rules applied consistently across members.

Changes:

  • Introduces Network/NetworkServer/NetworkPeer/NetworkFirewallRule domain models, migrations, factories, policies, controllers, jobs, and Inertia pages.
  • Adds provider capability to discover/sync private networks (AWS/DO/Hetzner/Linode/Vultr) and reconciles Vito state to match.
  • Integrates network-managed firewall rules into per-server UFW application and surfaces network rules read-only on the server firewall page.

Reviewed changes

Copilot reviewed 149 out of 155 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Unit/Actions/Service/UninstallTest.php Adds WireGuard uninstall guard coverage for network members.
tests/Feature/UniqueQueueTest.php Adds test coverage for transient DB lock retry behavior in UniqueQueue.
resources/views/wireguard/peer-conf.blade.php WireGuard peer config template for “peer device” downloads.
resources/views/ssh/wireguard/uninstall.blade.php SSH script to uninstall WireGuard packages.
resources/views/ssh/wireguard/remove-network.blade.php SSH script to stop/disable a per-network wg-quick interface and remove config.
resources/views/ssh/wireguard/latest-handshakes.blade.php SSH script to read WireGuard latest handshakes.
resources/views/ssh/wireguard/install.blade.php SSH script to install WireGuard packages and validate availability.
resources/views/ssh/wireguard/configure.blade.php SSH script to install config, enable unit, and validate interface start.
resources/views/ssh/wireguard/conf.blade.php WireGuard server interface config template.
resources/views/ssh/services/firewall/ufw/apply-rules.blade.php Allows UFW rules without port/proto for “allow all” network rules.
resources/js/types/server-log.d.ts Adds optional server_name and network_id to ServerLog type for network log UIs.
resources/js/types/network.d.ts Adds frontend type definitions for networks, members, peers, rules, and stats.
resources/js/pages/site-tooling/index.tsx Updates docs link.
resources/js/pages/services/components/install.tsx Hides VPN services from the generic “install service” UI flow.
resources/js/pages/server-network/index.tsx Updates docs link.
resources/js/pages/redirects/components/redirect-form-fields.tsx Formatting-only change to Checkbox usage.
resources/js/pages/redirects/components/edit-redirect.tsx Formatting-only prop type inline change.
resources/js/pages/networks/show.tsx Adds network overview page (stats + recent activity).
resources/js/pages/networks/servers.tsx Adds network servers listing + actions (sync/add/edit/remove) via VitoTable + dialogs.
resources/js/pages/networks/peers.tsx Adds WireGuard peer device listing + actions via VitoTable + dialogs.
resources/js/pages/networks/logs.tsx Adds dedicated network logs page.
resources/js/pages/networks/index.tsx Adds top-level networks index listing + create/sync actions.
resources/js/pages/networks/firewall.tsx Adds network firewall rules listing + create/edit/delete actions.
resources/js/pages/networks/components/private-ip-select.tsx Adds reusable private-IP picker with refresh-on-demand.
resources/js/pages/networks/components/log-columns.tsx Adds network-specific server column for logs (with truncation helper).
resources/js/pages/networks/components/firewall-rule-form.tsx Adds network firewall rule dialog form (registry-compatible).
resources/js/pages/networks/components/edit-network-server.tsx Adds custom-network member IP edit dialog.
resources/js/pages/networks/components/add-server.tsx Adds “add server to network” dialog (custom network supports picking private IP).
resources/js/pages/networks/components/add-peer.tsx Adds “add peer device” dialog for WireGuard networks.
resources/js/pages/firewall/index.tsx Surfaces network-managed rules read-only on server firewall page.
resources/js/pages/backups/index.tsx Formatting-only change to VitoTable props layout.
resources/js/pages/backups/files.tsx Formatting-only change to ternary layout.
resources/js/pages/backups/components/restore-backup.tsx Minor text punctuation change.
resources/js/lib/utils.ts Adds currentPath() and truncate() helpers for nav active-state and UI truncation.
resources/js/layouts/network/layout.tsx Adds dedicated network layout + secondary nav tabs.
resources/js/layouts/database/layout.tsx Uses currentPath() to avoid query/hash affecting active nav state.
resources/js/components/ui/combobox.tsx Extends Combobox items with optional search keywords.
resources/js/components/dialogs/registry.ts Registers new network dialogs in the centralized dialog registry.
resources/js/components/app-sidebar.tsx Adds “Networks” entry to main sidebar + improves active detection.
resources/js/components/app-sidebar-nested.tsx Improves active detection via currentPath().
package-lock.json Updates lockfile contents; project name changed (needs correction).
docs/4.x/settings/server-providers.md Documents provider connections being used for provider network discovery.
docs/4.x/servers/network.md Clarifies server network page vs project networks feature.
docs/4.x/servers/firewall.md Documents network rules shown on server firewall page.
docs/4.x/README.md Adds Networks section and cross-links.
docs/4.x/networks/servers.md New documentation for network servers tab.
docs/4.x/networks/provider-networks.md New documentation for provider network discovery/sync semantics.
docs/4.x/networks/overview.md New documentation for network concept, types, statuses, and tabs.
docs/4.x/networks/firewall.md New documentation for network firewall rules and application behavior.
docs/4.x/networks/create.md New documentation for creating WireGuard/custom networks.
docs/4.x/automation.md Documents scheduled network reconciliation and notes provider sync is manual.
database/migrations/2026_07_24_195725_add_network_id_to_server_logs.php Adds network_id foreign key to server_logs for network-scoped activity.
database/migrations/2026_07_24_195724_create_network_peers_table.php Creates network_peers table for peer devices.
database/migrations/2026_07_24_195723_create_server_network_rules_table.php Creates server_network_rules materialized rules table for network-driven firewall.
database/migrations/2026_07_24_195722_create_network_firewall_rules_table.php Creates network_firewall_rules table.
database/migrations/2026_07_24_195721_create_network_servers_table.php Creates network_servers membership table.
database/migrations/2026_07_24_195720_create_networks_table.php Creates networks table with provider sync fields.
database/factories/NetworkServerFactory.php Factory for network server membership.
database/factories/NetworkPeerFactory.php Factory for network peer devices.
database/factories/NetworkFirewallRuleFactory.php Factory for network firewall rules.
database/factories/NetworkFactory.php Factory for networks.
app/ValidationRules/WireGuardPublicKeyRule.php Validates WireGuard public keys and uniqueness within a network.
app/Traits/UniqueQueue.php Adds transient DB error detection/retry logic (needs lock release cleanup).
app/Tables/Servers/ServerNetworkRuleTable.php InertiaTable table for server network-managed rules listing.
app/Tables/NetworkTable.php InertiaTable table for networks listing.
app/Tables/Networks/NetworkServerTable.php InertiaTable table for network members listing.
app/Tables/Networks/NetworkPeerTable.php InertiaTable table for network peers listing.
app/Tables/Networks/NetworkFirewallRuleTable.php InertiaTable table for network firewall rules listing.
app/Support/Testing/SSHFake.php Uses ServerLog::newLog() flow to support new network log context tagging.
app/Support/Cidr.php Adds IPv4 CIDR utilities (canonicalization, overlap, host allocation).
app/Services/VPN/VPN.php Defines VPN service interface for network configuration hooks.
app/Services/Firewall/Ufw.php Applies both network and server firewall rules; finalizes server network rule statuses.
app/ServerProviders/Vultr.php Adds ProvidesPrivateNetworks support for Vultr VPC discovery.
app/ServerProviders/ProvidesPrivateNetworks.php Adds provider capability interface for private network discovery.
app/ServerProviders/Linode.php Adds ProvidesPrivateNetworks support for Linode VPC discovery.
app/ServerProviders/Hetzner.php Adds ProvidesPrivateNetworks support for Hetzner network discovery.
app/ServerProviders/DigitalOcean.php Adds ProvidesPrivateNetworks support for DigitalOcean VPC discovery.
app/ServerProviders/AbstractProvider.php Adds credential-safe PrivateNetworkSyncError wrapper helper.
app/Providers/ServiceTypeServiceProvider.php Registers WireGuard as a service type (VPN).
app/Policies/NetworkPolicy.php Adds authorization rules for network CRUD, incl. provider network delete semantics.
app/Models/ServerNetworkRule.php Adds model for materialized per-server network firewall/handshake rules.
app/Models/ServerLog.php Adds network context tagging to ServerLog creation + network relationship.
app/Models/ServerIpAddress.php Triggers custom-network firewall re-apply when a referenced private IP is deleted.
app/Models/Server.php Adds networkRules relationship + WireGuard sibling resync on server deletion.
app/Models/Project.php Adds networks relationship.
app/Models/NetworkServer.php Adds membership model for network servers (incl. encrypted private key).
app/Models/NetworkPeer.php Adds peer device model (incl. concealment semantics).
app/Models/NetworkFirewallRule.php Adds network firewall rule model.
app/Models/Network.php Adds network model with relationships and enum casting.
app/Jobs/Network/SyncProviderNetworksJob.php Adds background job for provider network reconciliation (with click de-dupe).
app/Jobs/Network/PollPeerHandshakesJob.php Adds handshake polling job for WireGuard peers.
app/Jobs/Network/ApplyNetworkFirewallJob.php Adds job to apply network firewall rules to a single member.
app/Http/Resources/ServerLogResource.php Adds server_name and network_id to log resource.
app/Http/Resources/NetworkServerResource.php Adds API resource for network members.
app/Http/Resources/NetworkResource.php Adds API resource for networks.
app/Http/Resources/NetworkPeerResource.php Adds API resource for network peers.
app/Http/Resources/NetworkFirewallRuleResource.php Adds API resource for network firewall rules.
app/Http/Middleware/HandleInertiaRequests.php Shares network context in Inertia props when routed under a network.
app/Http/Controllers/NetworkServerController.php Adds endpoints for managing network server membership and sync.
app/Http/Controllers/NetworkPeerController.php Adds endpoints for peer device lifecycle and config reveal.
app/Http/Controllers/NetworkFirewallRuleController.php Adds endpoints for network firewall rules CRUD.
app/Http/Controllers/FirewallController.php Adds networkRules table data to server firewall page.
app/Exceptions/PrivateNetworkSyncError.php Adds safe exception type to avoid leaking credentials via failed_jobs.
app/Enums/ServerNetworkRuleKind.php Adds enum for handshake vs rule materialization.
app/Enums/NetworkType.php Adds enum for network type (provider/custom/wireguard).
app/Enums/NetworkStatus.php Adds enum for network status (creating/syncing/active/failed/deleting).
app/Enums/NetworkServerStatus.php Adds enum for member status (pending/updating/active/failed/leaving).
app/Enums/NetworkPeerStatus.php Adds enum for peer status (pending/active/disabled).
app/Enums/NetworkAddressingPool.php Adds enum for WireGuard address pool selection.
app/DTOs/PrivateNetworkMemberDTO.php Adds DTO for provider-reported member + sanitizes IPv4 addresses.
app/DTOs/PrivateNetworkDTO.php Adds DTO for provider networks + normalizes CIDRs to IPv4 canonical form.
app/Console/Kernel.php Schedules network reconciliation command every 3 minutes.
app/Actions/Service/Install.php Prevents installing VPN services via generic service install action.
app/Actions/Server/EditServer.php Resyncs WireGuard peers when a server’s public IP changes.
app/Actions/Network/UpdateNetworkServerIp.php Updates custom-network member IP selection and reapplies firewall.
app/Actions/Network/UpdateNetworkPeer.php Updates peer enable/disable + triggers member resync and recompute.
app/Actions/Network/UpdateNetwork.php Allows renaming a network with per-project uniqueness.
app/Actions/Network/SyncNetwork.php Syncs a network (provider uses provider sync job; others resync members).
app/Actions/Network/RemoveServerFromNetwork.php Removes member and triggers network-wide resync/firewall updates.
app/Actions/Network/RegenerateNetworkPeerKeys.php Regenerates peer keys (BYO or generated), resyncs members, recomputes status.
app/Actions/Network/RecomputeNetworkStatus.php Computes network status from member statuses + activates pending peers when stable.
app/Actions/Network/ManageNetworkFirewallRule.php CRUD for network firewall rules + applies firewall + broadcasts.
app/Actions/Network/GetNetworkPeerConfig.php Generates peer config and conditionally returns private key.
app/Actions/Network/GenerateWireGuardKeys.php Generates Curve25519 WireGuard keypairs in wg-compatible format.
app/Actions/Network/FinalizeServerNetworkRules.php Finalizes materialized server network rule statuses based on apply outcome.
app/Actions/Network/DispatchNetworkServerSync.php Dispatches per-member sync/teardown to SSH queue based on readiness.
app/Actions/Network/DeleteNetworkPeer.php Deletes peer device and triggers member resync + recompute.
app/Actions/Network/DeleteNetwork.php Marks network deleting and tears down members.
app/Actions/Network/CreateNetworkPeer.php Creates a peer device with allocated IP and keys (BYO optional) + resync.
app/Actions/Network/ConcealNetworkPeerKey.php Removes stored peer private key after reveal.
app/Actions/Network/ApplyNetworkFirewall.php Materializes and applies network firewall rules to members (defers unreachable).
app/Actions/Network/AllocateNetworkBlock.php Allocates WireGuard address blocks avoiding conflicts and blocklisted ranges.
.prettierignore Excludes wireguard view templates from Prettier formatting.

Comment thread app/Traits/UniqueQueue.php
Comment thread app/Actions/Network/CreateNetwork.php

@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: 51

🤖 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 `@app/Actions/Network/AddServersToNetwork.php`:
- Around line 127-131: The servers.* validation rules accept duplicate or
non-integer IDs. In app/Actions/Network/AddServersToNetwork.php lines 127-131
and app/Actions/Network/CreateNetwork.php lines 210-212, add integer and
distinct validation rules alongside the existing exists and unique rules,
preserving the current project and network constraints.
- Around line 67-97: The tunnel-address allocation logic is duplicated across
the network actions. Create a shared AllocateNetworkAddress action that locks
the network, collects member and peer IPs, allocates the next Cidr::nextHost
address, tracks allocations, and raises the existing exhaustion validation
error; update AddServersToNetwork, CreateNetworkPeer, and
CreateNetwork::buildWireGuard to use it instead of their inline or usedIps()
logic. Affected sites: app/Actions/Network/AddServersToNetwork.php lines 67-97,
replace the used-IP collection and per-server allocation;
app/Actions/Network/CreateNetworkPeer.php lines 83-90, replace usedIps() with
the shared action; app/Actions/Network/CreateNetwork.php lines 95-113, replace
the inline allocation loop with the shared action.

In `@app/Actions/Network/CreateNetwork.php`:
- Around line 125-144: Normalize the CIDR input once before the duplicate check
and creation in the network action, treating a missing or empty `cidr` as null.
Reuse that normalized value for the duplicate lookup and both `cidr` and
`cidr_canonical` fields in the `Network::create` call, ensuring empty input
cannot be passed to `Cidr::canonical` or persisted as a bogus CIDR.

In `@app/Actions/Network/MaterializeServerNetworkRules.php`:
- Around line 235-242: Extract the duplicated server-network-rule.updated
dispatch from MaterializeServerNetworkRules::broadcast into a shared injectable
broadcaster or trait, preserving the project_id, event type, and server_id
payload. Update FinalizeServerNetworkRules to use the same shared helper instead
of maintaining its own broadcast implementation; apply the change at
app/Actions/Network/MaterializeServerNetworkRules.php#L235-L242 and
app/Actions/Network/FinalizeServerNetworkRules.php#L51-L58.
- Around line 40-103: Reduce the complexity of forServer() by extracting the
self-contained reconciliation branches into private helpers, such as
createMissing(), updateChanged(), and pruneStale(), passing the existing,
desired, and applied state they require. Keep the transaction boundary, changed
tracking, status transitions, deletion behavior, and broadcast($server) behavior
unchanged.

In `@app/Actions/Network/RecomputeNetworkStatus.php`:
- Around line 24-38: Update the DELETING branch in RecomputeNetworkStatus so
that when servers remain, it dispatches a network.updated SocketEvent before
returning, using the network’s project and identifier data; preserve the
existing network.deleted event and deletion behavior when no servers remain.

In `@app/Actions/Network/SyncProviderNetworks.php`:
- Around line 50-62: Update SyncProviderNetworks::reconcile/prune flow so every
provider-reported network is excluded from pruning when reconciliation fails,
including failures caused by transient QueryException; track the discovered
external ID independently of reconcile’s return value, while preserving pruning
for persisted networks not reported by the provider.
- Around line 191-226: Replace the isset() check in the existing-member
departure loop with array_key_exists() so desired members remain attached even
when their desired IP value is null. Keep the surrounding LEAVING handling and
removal flow unchanged.

In `@app/Actions/Network/UpdateNetworkPeer.php`:
- Around line 43-56: Make the dependent network mutations atomic: in
app/Actions/Network/UpdateNetworkPeer.php, wrap the peer save, conditional
resyncMembers(), and recompute->handle() calls in DB::transaction(), dispatch
the socket event after the transaction commits, and move
RecomputeNetworkStatus::activatePendingPeers() socket dispatches to the same
commit boundary; in app/Actions/Network/UpdateNetworkServerIp.php, wrap
member->update() and firewall rule materialisation in DB::transaction(), then
dispatch the SSH firewall job only after commit.

In `@app/Console/Commands/ReconcileNetworksCommand.php`:
- Around line 142-162: Update forceConverge() to wrap the membership deletion
and RecomputeNetworkStatus::handle() call in a DB::transaction(), then dispatch
the deletion event only after the transaction commits. After commit, invoke the
existing RemoveServerFromNetwork, DispatchNetworkServerSync::resyncMembers(),
and ApplyNetworkFirewall::handle() convergence flow so remaining members and
firewall rules are synchronized.

In `@app/Console/Kernel.php`:
- Line 24: Update the networks:reconcile scheduling chain to pass a short
expiration, such as 10 minutes, to withoutOverlapping(), matching the command’s
three-minute cadence and preventing stale locks from suppressing reconciliation
for 24 hours.

In `@app/DTOs/PrivateNetworkDTO.php`:
- Around line 32-55: The IPv4 CIDR validation logic is duplicated across two
locations and should use the shared helper. In app/DTOs/PrivateNetworkDTO.php
lines 32-55, simplify normalizeCidr() to validate with
Cidr::isValidIpv4Cidr($cidr) before canonicalizing, removing the manual parsing
and unreachable prefix check; in app/Actions/Network/CreateNetwork.php lines
241-256, update the closure to call the same helper instead of re-parsing the
CIDR.

In `@app/Enums/ServerNetworkRuleKind.php`:
- Around line 5-9: Update ServerNetworkRuleKind to implement
App\Contracts\VitoEnum and add getColor(): string and getText(): string methods,
providing the required color and display text for both HANDSHAKE and RULE cases.

In `@app/Http/Controllers/NetworkPeerController.php`:
- Around line 53-60: Update NetworkPeerController::config to wrap the
GetNetworkPeerConfig::config result in the appropriate JsonResource and return
that resource directly, replacing response()->json() while preserving the
existing authorization and network-membership checks.

In `@app/Http/Middleware/HandleInertiaRequests.php`:
- Around line 86-92: Update the project-switching branch in
HandleInertiaRequests so that after assigning and saving the new
current_project_id, the shared currentProject value is refreshed from the
updated user/project relationship before building the response data. Ensure the
first network response exposes auth.currentProject from the new project while
preserving the existing NetworkResource behavior.

In `@app/Models/NetworkFirewallRule.php`:
- Around line 7-19: Document the NetworkFirewallRule model’s serverRules
relationship in its PHPDoc with a Collection<int, ServerNetworkRule> property
annotation, and import Illuminate\Database\Eloquent\Collection alongside the
existing Eloquent imports.

In `@app/Models/NetworkServer.php`:
- Around line 41-48: Update the private_key entry in NetworkServer::$casts from
the plain encrypted cast to the required encrypted:json cast, and verify the
related factory/action values remain JSON-compatible with this cast.

In `@app/Models/Server.php`:
- Around line 130-168: Replace the static server-ID-keyed
$networkSiblingsToResync array with a WeakMap keyed by Server instances, and
update the deleting/deleted listeners in boot() to store, retrieve, and unset
entries using the $server object. Preserve the existing sibling collection and
resync behavior while ensuring aborted deletions cannot retain stale state.

In `@app/ServerProviders/AWS.php`:
- Around line 45-57: Update privateNetworks() to consume every page returned by
both describeInstances() and describeVpcs(), using the AWS SDK paginator or
equivalent NextToken iteration. Aggregate all reservations before passing them
to vpcIdsFrom(), and aggregate all Vpcs before building the network result;
preserve the existing empty-result behavior.
- Around line 123-148: Update placementOf to select the NetworkInterfaces entry
whose Attachment.DeviceIndex is 0, using its VpcId and PrivateIpAddress; if no
primary interface exists, fall back to the first entry before using top-level
instance fields.

In `@app/ServerProviders/Vultr.php`:
- Around line 40-49: Replace the inline comments in the PrivateNetworkSyncError
catch block with a DocBlock placed immediately before the 404 handling,
preserving the rationale that stale upstream server IDs should be skipped so one
404 does not abort synchronization or suppress pruning for other networks. Keep
the existing continue and rethrow behavior unchanged.

In `@app/Services/VPN/WireGuard.php`:
- Around line 62-72: Update the WireGuard SSH script references used by
install(), the indicated related methods, and their corresponding view lookups
from ssh.wireguard.* to ssh.services.wireguard.*; relocate the templates into
resources/views/ssh/services/wireguard/ while preserving the existing script
names and execution behavior.
- Around line 207-218: Update WireGuard::uploadConf to ensure the temporary
configuration file is created with 0600 permissions before SSH upload, rather
than relying on Storage::disk('local')->put() defaults; preserve the existing
upload flow and finally cleanup.

In `@app/Support/Cidr.php`:
- Around line 7-36: Harden Cidr::split() and Cidr::mask() for invalid prefixes:
make missing, empty, or non-digit suffixes default to 32, and clamp numeric
prefixes above 32 to 32 before bit shifting. Preserve valid prefix values and
the existing zero-prefix behavior so canonicalization and allocation remain
correct.

In `@app/Traits/UniqueQueue.php`:
- Around line 49-61: Update UniqueQueue::isTransientDatabaseError to use
Laravel’s structured concurrency detection via
Illuminate\Database\Concerns\DetectsConcurrencyErrors, or inspect QueryException
SQLSTATE/driver error codes, instead of matching exception message fragments.
Preserve the boolean result so only recognized transient database concurrency
failures are retried.

In `@app/ValidationRules/WireGuardPublicKeyRule.php`:
- Around line 13-19: Update validate in WireGuardPublicKeyRule to require
canonical Base64 encoding by decoding the string and rejecting it when
base64_encode($decoded) does not exactly equal the original value, in addition
to the existing 32-byte decoded-length check. Perform this validation before any
peer-key storage or collision checks.

In `@database/factories/NetworkServerFactory.php`:
- Line 24: Update the IP generation in NetworkServerFactory to derive unique
host addresses from the 100.64.0.0/24 network instead of using the fixed
two-digit numerify pattern. Exclude the subnet address .0, preserve valid IPv4
formatting, and support creating more than 100 records without exhausting the
generator.
- Around line 25-26: Update the NetworkServerFactory key generation to create a
valid WireGuard key pair: generate one private key, then derive its matching
public key through the same production key-generation path, or use an
established valid fixture pair. Ensure the factory stores the derived public key
alongside the private key instead of generating both independently.

In `@database/migrations/2026_07_24_195723_create_server_network_rules_table.php`:
- Around line 15-18: Update the migration defining the server network rules
table to enforce consistency between server_id, network_id, network_server_id,
and network_firewall_rule_id. Prefer storing only network_server_id and deriving
the parent IDs; otherwise replace the independent foreign keys with
database-level composite constraints that require the referenced network server
and firewall rule to belong to the specified server and network.

In `@docs/4.x/networks/servers.md`:
- Line 33: Update the networking server retry description to remove the fixed
three-minute recovery guarantee or qualify it as the reconcile interval rather
than a completion deadline. Ensure the wording reflects that processing depends
on retries of stalled leaf/teardown attempts and may take longer than one
interval.

In `@resources/js/layouts/network/layout.tsx`:
- Around line 10-11: Update the network layout’s realtime update flow so the
live record from useRealtimeRecord is propagated to every child page as the
network value, rather than leaving pages to read page.props.network directly.
Ensure downstream Head titles, badges, and isCustom/isWireGuard/isManaged checks
consume the updated network while preserving the existing layout behavior.

In `@resources/js/pages/firewall/index.tsx`:
- Line 26: Update the hasNetworkRules logic used by the firewall page to keep
VitoTable mounted when a search query is active, even if networkRules.data is
empty; base visibility on persistent rule availability or the active query state
so users can clear searches with no matches.

In `@resources/js/pages/networks/components/add-peer.tsx`:
- Line 1: Remove the redundant post-submit resets from the onSuccess handlers in
the add-peer, add-server, and create-network dialog components: retain only
onOpenChange(false), deleting form.reset() everywhere and setByo(false) in
add-peer. Rely on the DialogHost instanceId remount behavior and existing
mount-time initialization for fresh form state on each open.

In `@resources/js/pages/networks/firewall.tsx`:
- Line 21: Update the network reference used by the page metadata and type-based
gating to consume the realtime network record rather than static
page.props.network. Locate the existing realtime network source used by
NetworkLayout and reuse that symbol in the page’s Head title and conditional
logic, while preserving the current behavior for other page content.
- Line 34: Update the external documentation anchor in the firewall page to
include rel="noreferrer" alongside target="_blank", preventing the opened page
from accessing window.opener while preserving the existing link destination and
behavior.

In `@resources/js/pages/networks/index.tsx`:
- Line 28: Add rel="noopener noreferrer" to the Docs anchors in
resources/js/pages/networks/index.tsx lines 28-28 and
resources/js/pages/networks/show.tsx lines 69-69, preserving their existing
target="_blank" behavior.
- Around line 61-65: Update the network row action around the Link and Button
components to use Button with asChild, making the Link its rendered interactive
element instead of nesting controls. Add a descriptive aria-label to the
icon-only Link while preserving its existing route, styling, and EyeIcon.

In `@resources/js/pages/networks/logs.tsx`:
- Line 17: Update the network reference in the logs page to use the realtime
network record, matching the approach used by the network layout, instead of
reading the static page.props.network value. Locate the existing realtime record
source and use it wherever the logs page currently relies on network.
- Line 27: Update the external documentation anchor in the networks logs page to
include rel="noreferrer" alongside target="_blank", preserving the existing href
and link behavior.

In `@resources/js/pages/networks/peers.tsx`:
- Line 21: Update the network data reference in the peers page to use the
realtime network record provided by the network layout, rather than the static
page.props.network value. Follow the existing pattern in the network layout and
other network pages, preserving the current consumers while ensuring they
receive live network updates.
- Line 31: Add rel="noreferrer" to the external documentation anchor in the
peers page, alongside its existing target="_blank" attribute.

In `@resources/js/pages/networks/servers.tsx`:
- Around line 23-26: Update the network type checks near isCustom, isWireGuard,
and isManaged to derive from the realtime network record used by the network
layout, rather than the initial page.props.network value. Ensure row-action
gating reacts when realtime network data changes, while preserving the existing
type comparisons.
- Line 36: Update the external documentation anchor in the servers page to
include rel="noreferrer" alongside target="_blank", preserving the existing href
and link behavior.

In `@resources/js/pages/networks/settings.tsx`:
- Line 20: Update the network source in the settings page from static page props
to the realtime network record used by the network layout, and ensure the Type
and Status Badge values derive from that realtime record as well. Locate the
change around the network declaration and preserve the existing rendering logic.
- Line 51: Update the external documentation anchor in the networks settings
page to include rel="noreferrer" alongside target="_blank", preserving the
existing href and link behavior.
- Around line 26-107: Render the validation message from form.errors.name
alongside the editable name field, using the existing InputError component. Keep
it tied to the name input so failed updates display the field-level error
without changing the current submit or focus behavior.

In `@resources/js/pages/server-network/index.tsx`:
- Line 59: Update the external anchor in the server network page to include
rel="noopener noreferrer" alongside target="_blank", preserving the existing
href and new-tab behavior.

In `@resources/js/pages/site-tooling/index.tsx`:
- Line 66: Add rel="noreferrer" to the external target="_blank" Docs links in
resources/js/pages/site-tooling/index.tsx:66-66 and
resources/js/pages/backups/index.tsx:36-36, preserving their existing href and
target attributes.

In `@resources/views/ssh/wireguard/configure.blade.php`:
- Around line 7-13: Update the WireGuard setup script around `systemctl restart`
to use `wg syncconf` for live configuration changes, preserving existing tunnels
when peers are added. Retain `systemctl restart` for cold starts and membership
IP changes, since `wg syncconf` does not apply `Address` or `MTU` updates; keep
the existing interface validation unchanged.

In `@tests/Feature/NetworkProviderSyncTest.php`:
- Around line 514-529: Strengthen test_sync_ignores_networks_of_another_project
by recording the other network’s relevant attributes before invoking
SyncProviderNetworks::forProject, then assert those attributes remain unchanged
afterward in addition to asserting the record still exists. Keep the test
focused on preventing writes across the project boundary.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 4a59d81b-f557-4b64-824f-3861dcaa60ab

📥 Commits

Reviewing files that changed from the base of the PR and between 956ca41 and 344abd1.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (154)
  • .prettierignore
  • app/Actions/Network/AddServersToNetwork.php
  • app/Actions/Network/AllocateNetworkBlock.php
  • app/Actions/Network/ApplyNetworkFirewall.php
  • app/Actions/Network/ConcealNetworkPeerKey.php
  • app/Actions/Network/CreateNetwork.php
  • app/Actions/Network/CreateNetworkPeer.php
  • app/Actions/Network/DeleteNetwork.php
  • app/Actions/Network/DeleteNetworkPeer.php
  • app/Actions/Network/DispatchNetworkServerSync.php
  • app/Actions/Network/FinalizeServerNetworkRules.php
  • app/Actions/Network/GenerateWireGuardKeys.php
  • app/Actions/Network/GetNetworkPeerConfig.php
  • app/Actions/Network/ManageNetworkFirewallRule.php
  • app/Actions/Network/MaterializeServerNetworkRules.php
  • app/Actions/Network/RecomputeNetworkStatus.php
  • app/Actions/Network/RegenerateNetworkPeerKeys.php
  • app/Actions/Network/RemoveServerFromNetwork.php
  • app/Actions/Network/SyncNetwork.php
  • app/Actions/Network/SyncProviderNetworks.php
  • app/Actions/Network/UpdateNetwork.php
  • app/Actions/Network/UpdateNetworkPeer.php
  • app/Actions/Network/UpdateNetworkServerIp.php
  • app/Actions/Server/EditServer.php
  • app/Actions/Service/Install.php
  • app/Console/Commands/ReconcileNetworksCommand.php
  • app/Console/Kernel.php
  • app/DTOs/PrivateNetworkDTO.php
  • app/DTOs/PrivateNetworkMemberDTO.php
  • app/Enums/NetworkAddressingPool.php
  • app/Enums/NetworkPeerStatus.php
  • app/Enums/NetworkServerStatus.php
  • app/Enums/NetworkStatus.php
  • app/Enums/NetworkType.php
  • app/Enums/ServerNetworkRuleKind.php
  • app/Exceptions/PrivateNetworkSyncError.php
  • app/Http/Controllers/FirewallController.php
  • app/Http/Controllers/NetworkController.php
  • app/Http/Controllers/NetworkFirewallRuleController.php
  • app/Http/Controllers/NetworkPeerController.php
  • app/Http/Controllers/NetworkServerController.php
  • app/Http/Middleware/HandleInertiaRequests.php
  • app/Http/Resources/NetworkFirewallRuleResource.php
  • app/Http/Resources/NetworkPeerResource.php
  • app/Http/Resources/NetworkResource.php
  • app/Http/Resources/NetworkServerResource.php
  • app/Http/Resources/ServerLogResource.php
  • app/Jobs/Network/ApplyNetworkFirewallJob.php
  • app/Jobs/Network/PollPeerHandshakesJob.php
  • app/Jobs/Network/SyncNetworkServerJob.php
  • app/Jobs/Network/SyncProviderNetworksJob.php
  • app/Models/Network.php
  • app/Models/NetworkFirewallRule.php
  • app/Models/NetworkPeer.php
  • app/Models/NetworkServer.php
  • app/Models/Project.php
  • app/Models/Server.php
  • app/Models/ServerIpAddress.php
  • app/Models/ServerLog.php
  • app/Models/ServerNetworkRule.php
  • app/Policies/NetworkPolicy.php
  • app/Providers/ServiceTypeServiceProvider.php
  • app/ServerProviders/AWS.php
  • app/ServerProviders/AbstractProvider.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Linode.php
  • app/ServerProviders/ProvidesPrivateNetworks.php
  • app/ServerProviders/Vultr.php
  • app/Services/Firewall/Ufw.php
  • app/Services/VPN/VPN.php
  • app/Services/VPN/WireGuard.php
  • app/Support/Cidr.php
  • app/Support/Testing/SSHFake.php
  • app/Tables/NetworkTable.php
  • app/Tables/Networks/NetworkFirewallRuleTable.php
  • app/Tables/Networks/NetworkPeerTable.php
  • app/Tables/Networks/NetworkServerTable.php
  • app/Tables/Servers/ServerNetworkRuleTable.php
  • app/Traits/UniqueQueue.php
  • app/ValidationRules/WireGuardPublicKeyRule.php
  • database/factories/NetworkFactory.php
  • database/factories/NetworkFirewallRuleFactory.php
  • database/factories/NetworkPeerFactory.php
  • database/factories/NetworkServerFactory.php
  • database/migrations/2026_07_24_195720_create_networks_table.php
  • database/migrations/2026_07_24_195721_create_network_servers_table.php
  • database/migrations/2026_07_24_195722_create_network_firewall_rules_table.php
  • database/migrations/2026_07_24_195723_create_server_network_rules_table.php
  • database/migrations/2026_07_24_195724_create_network_peers_table.php
  • database/migrations/2026_07_24_195725_add_network_id_to_server_logs.php
  • docs/4.x/README.md
  • docs/4.x/automation.md
  • docs/4.x/networks/create.md
  • docs/4.x/networks/firewall.md
  • docs/4.x/networks/overview.md
  • docs/4.x/networks/peers.md
  • docs/4.x/networks/provider-networks.md
  • docs/4.x/networks/servers.md
  • docs/4.x/servers/firewall.md
  • docs/4.x/servers/network.md
  • docs/4.x/settings/server-providers.md
  • resources/js/components/app-sidebar-nested.tsx
  • resources/js/components/app-sidebar.tsx
  • resources/js/components/dialogs/registry.ts
  • resources/js/components/ui/combobox.tsx
  • resources/js/layouts/database/layout.tsx
  • resources/js/layouts/network/layout.tsx
  • resources/js/lib/utils.ts
  • resources/js/pages/backups/components/restore-backup.tsx
  • resources/js/pages/backups/files.tsx
  • resources/js/pages/backups/index.tsx
  • resources/js/pages/firewall/index.tsx
  • resources/js/pages/networks/components/add-peer.tsx
  • resources/js/pages/networks/components/add-server.tsx
  • resources/js/pages/networks/components/create-network.tsx
  • resources/js/pages/networks/components/edit-network-server.tsx
  • resources/js/pages/networks/components/firewall-rule-form.tsx
  • resources/js/pages/networks/components/log-columns.tsx
  • resources/js/pages/networks/components/peer-config.tsx
  • resources/js/pages/networks/components/private-ip-select.tsx
  • resources/js/pages/networks/firewall.tsx
  • resources/js/pages/networks/index.tsx
  • resources/js/pages/networks/logs.tsx
  • resources/js/pages/networks/peers.tsx
  • resources/js/pages/networks/servers.tsx
  • resources/js/pages/networks/settings.tsx
  • resources/js/pages/networks/show.tsx
  • resources/js/pages/redirects/components/edit-redirect.tsx
  • resources/js/pages/redirects/components/redirect-form-fields.tsx
  • resources/js/pages/server-network/index.tsx
  • resources/js/pages/services/components/install.tsx
  • resources/js/pages/site-tooling/index.tsx
  • resources/js/types/network.d.ts
  • resources/js/types/server-log.d.ts
  • resources/views/ssh/services/firewall/ufw/apply-rules.blade.php
  • resources/views/ssh/wireguard/conf.blade.php
  • resources/views/ssh/wireguard/configure.blade.php
  • resources/views/ssh/wireguard/install.blade.php
  • resources/views/ssh/wireguard/latest-handshakes.blade.php
  • resources/views/ssh/wireguard/remove-network.blade.php
  • resources/views/ssh/wireguard/uninstall.blade.php
  • resources/views/wireguard/peer-conf.blade.php
  • storage/database.sqlite.bak-20260724-195719
  • storage/database.sqlite.bak-20260724-203839
  • storage/database.sqlite.bak-20260724-204013-prefkfix
  • tests/Feature/NetworkFirewallTest.php
  • tests/Feature/NetworkPeerTest.php
  • tests/Feature/NetworkProviderSyncTest.php
  • tests/Feature/NetworkSyncTest.php
  • tests/Feature/NetworkTest.php
  • tests/Feature/ProviderPrivateNetworkTest.php
  • tests/Feature/UniqueQueueTest.php
  • tests/Unit/Actions/Service/UninstallTest.php

Comment thread app/Actions/Network/AddServersToNetwork.php
Comment thread app/Actions/Network/SyncProviderNetworks.php Outdated
Comment thread app/Actions/Network/SyncProviderNetworks.php
Comment thread app/Actions/Network/UpdateNetworkPeer.php
Comment thread app/Console/Commands/ReconcileNetworksCommand.php
Comment thread resources/js/pages/networks/settings.tsx Outdated
Comment thread resources/js/pages/server-network/index.tsx
Comment thread resources/js/pages/site-tooling/index.tsx
Comment thread resources/views/ssh/services/wireguard/configure.blade.php
Comment thread tests/Feature/NetworkProviderSyncTest.php

@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: 27

Caution

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

⚠️ Outside diff range comments (1)
resources/js/components/app-sidebar.tsx (1)

202-212: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compare currentPath() against childItem.href, not childItem.onlyActivePath.

onlyActivePath is used as the selection flag; elsewhere in this component the exact active route is compared with .href. Comparing the current path to the flag prevents groupActive from becoming true for children such as the server-side servers.show, databases, or sites links, so the parent collapsible can fail to open when those child routes are active.

🐛 Proposed fix
                          childItem.hidden
                            ? false
                            : childItem.onlyActivePath
-                              ? currentPath() === childItem.onlyActivePath
+                              ? currentPath() === childItem.href
                               : window.location.href.startsWith(childItem.href),
🤖 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 `@resources/js/components/app-sidebar.tsx` around lines 202 - 212, Update the
groupActive calculation in the childItem.some callback to compare currentPath()
with childItem.href when childItem.onlyActivePath is set, rather than comparing
against the onlyActivePath flag. Preserve the existing hidden-child exclusion
and startsWith behavior for other children.
♻️ Duplicate comments (4)
app/Services/VPN/WireGuard.php (1)

207-219: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Temp config file is still briefly world-readable before chmod.

Storage::disk('local')->put() creates the file with the disk's default permissions; @chmod(..., 0600) only tightens it afterwards, leaving a race window where the WireGuard private key is exposed. The @ also silently swallows any chmod failure. Create the file restricted before writing content instead of chmod-after-write.

🔒 Proposed fix
     private function uploadConf(SSH|SSHFake $ssh, string $remote, string $content): void
     {
         $tmpName = 'wg-'.Str::random(20);
         $disk = Storage::disk('local');
-        $disk->put($tmpName, $content);
-        `@chmod`($disk->path($tmpName), 0600);
+        $path = $disk->path($tmpName);
+        touch($path);
+        chmod($path, 0600);
+        $disk->put($tmpName, $content);
 
         try {
             $ssh->upload($disk->path($tmpName), $remote, 'root');
         } finally {
             $disk->delete($tmpName);
         }
     }
🤖 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 `@app/Services/VPN/WireGuard.php` around lines 207 - 219, Update uploadConf so
the temporary configuration file is created with 0600 permissions before writing
content, rather than calling Storage::put followed by chmod. Remove the
suppressed chmod call and preserve the existing SSH upload and finally-block
cleanup behavior.
app/Console/Commands/ReconcileNetworksCommand.php (1)

143-165: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Still unresolved: forceConverge() isn't transactional and skips peer-config convergence.

Deleting the membership, broadcasting, and applying the firewall/status recompute happen outside a DB::transaction(), and remaining members' WireGuard peer configs are never resynced (no DispatchNetworkServerSync::resyncMembers() call) after this forced removal — a prior review already flagged this exact gap on these lines.

🤖 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 `@app/Console/Commands/ReconcileNetworksCommand.php` around lines 143 - 165,
Update forceConverge() to perform the membership deletion and related network
state updates within a DB::transaction(), and invoke
DispatchNetworkServerSync::resyncMembers() for the remaining members after the
forced removal. Preserve the existing logging, socket event, firewall
application, and network status recomputation while ensuring peer configurations
are resynchronized.
app/Models/NetworkServer.php (1)

41-48: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

private_key still uses the plain encrypted cast, not encrypted:json.

Same concern as flagged on a previous commit — this WireGuard credential should follow the repo-wide encrypted:json convention (as used for Server::$authentication).

🔒 Proposed fix
-        'private_key' => 'encrypted',
+        'private_key' => 'encrypted: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 `@app/Models/NetworkServer.php` around lines 41 - 48, Update the private_key
entry in NetworkServer’s casts to use the repository-standard encrypted:json
cast, matching Server::$authentication, while leaving the other casts unchanged.

Source: Coding guidelines

database/factories/NetworkServerFactory.php (1)

25-26: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Public/private keys still aren't a matching WireGuard pair.

Same concern as before: generating both independently with random_bytes means they don't represent one identity, which will break any test relying on real key correspondence.

🔑 Proposed fix
-            'public_key' => base64_encode(random_bytes(32)),
-            'private_key' => base64_encode(random_bytes(32)),
+            'private_key' => $keys = app(\App\Actions\Network\GenerateWireGuardKeys::class)->generate(),

Or reuse the same generator used in production (GenerateWireGuardKeys::generate()), storing both public_key/private_key from its return value instead of generating them independently.

🤖 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 `@database/factories/NetworkServerFactory.php` around lines 25 - 26, Update the
network server factory key generation to use the production WireGuard key-pair
generator, GenerateWireGuardKeys::generate(), and assign its corresponding
private and public values to private_key and public_key instead of generating
each with random_bytes.
🤖 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 `@app/Actions/Network/AllocateNetworkBlock.php`:
- Around line 122-131: Update memberSubnets() to avoid calling
$server->ipAddresses()->get() for each member server; fetch all ServerIpAddress
records for the member server IDs in one query, then apply the existing IPv4
filtering, CIDR canonicalization, uniqueness, and value normalization. Add the
ServerIpAddress import and preserve the current returned array behavior.

In `@app/Actions/Network/CreateNetwork.php`:
- Line 1: Extract the duplicated per-server WireGuard member creation sequence
from CreateNetwork::buildWireGuard() into a shared private method or
collaborator that allocates the next host IP, generates keys, and creates the
network_servers row. Update both CreateNetwork::buildWireGuard() and
AddServersToNetwork::addWireGuard() to call this shared implementation,
preserving their existing inputs and behavior.

In `@app/Actions/Network/FinalizeServerNetworkRules.php`:
- Around line 20-34: Wrap the two database mutations in
ServerNetworkRule::success—updating emitted rules and deleting deletingIds—in a
DB::transaction() callback, while preserving the existing conditional queries
and broadcasting behavior after the transaction completes.

In `@app/Actions/Network/MaterializeServerNetworkRules.php`:
- Around line 108-176: Refactor desiredFor() and its handshakes()/sources() flow
to memoise each network’s peer collection by network_id for the duration of the
call, reusing it for handshake generation, source generation, and the
devices-exist check instead of querying peers per membership. Also eager-load or
cache network firewallRules once per network and reuse that collection,
preserving the existing non-deleting filter and ordering while eliminating
repeated membership queries during forNetwork().
- Around line 56-83: Update the existing-rule handling in the materialization
loop so a matching row with status FirewallRuleStatus::DELETING is revived to
the desired lifecycle status, using CREATING when applied and READY otherwise.
Ensure this status-only transition marks changed and is persisted, while
retaining the existing field-change update behavior for other statuses.
- Around line 183-212: Guard nullable relationship dereferences in peers() and
sources(). In the peers mapping, safely handle missing peer->server before
reading ip or name, and in sources() safely handle a missing
peer->serverIpAddress when server_ip_address_id is set; skip entries without a
valid IP while preserving the existing output shape and reconciliation flow.

In `@app/Actions/Network/RegenerateNetworkPeerKeys.php`:
- Around line 41-52: Make topology side effects commit-consistent across
app/Actions/Network/RegenerateNetworkPeerKeys.php lines 41-52,
app/Actions/Network/ApplyNetworkFirewall.php lines 25-50, and
app/Actions/Network/RecomputeNetworkStatus.php lines 30-34, 59-63, and 74-78:
defer resync/recompute work and SocketEvent dispatches until the surrounding
database transaction commits, using the existing commit-callback mechanism where
available, so events expose only final durable state.

In `@app/Actions/Network/RemoveServerFromNetwork.php`:
- Around line 23-33: Centralize WireGuard member re-sync traversal in
DispatchNetworkServerSync::resyncMembers() by adding an optional exclusion
parameter and applying the existing ACTIVE/UPDATING filtering there. In
app/Actions/Network/RemoveServerFromNetwork.php lines 23-33, replace the manual
loop with resyncMembers($network) and remove the unused NetworkServerStatus
import. In app/Actions/Server/EditServer.php lines 67-82, replace the nested
query and loop with resyncMembers() for each WireGuard membership, passing the
edited server’s membership ID as the exclusion.

In `@app/Actions/Network/SyncProviderNetworks.php`:
- Around line 179-231: Update reconcileMembers to track whether any existing
member transitions to LEAVING during this pass, and skip the final removal loop
when such a transition occurs. Preserve normal pruning when no member was newly
marked LEAVING, while continuing to reactivate desired LEAVING members.

In `@app/Actions/Network/UpdateNetworkServerIp.php`:
- Around line 34-40: Scope the unique validation rule for server_ip_address_id
in the UpdateNetworkServerIp validation to the current network by adding the
appropriate network_id constraint, while retaining the existing member ignore
behavior and private-IP/server existence constraints.

In `@app/Http/Controllers/NetworkController.php`:
- Around line 109-126: Extract the shared private IP mapping from
memberIpsPayload() and serversPayload() into a private helper, such as
mapPrivateIps(), preserving the existing id, ip, and is_primary fields and
values() behavior. Replace both inline mappings with calls to the helper so the
two payload methods use one implementation.

In `@app/Http/Controllers/NetworkPeerController.php`:
- Around line 53-60: Update the config method in NetworkPeerController to add
the Cache-Control: no-store header to the JSON response returned by
GetNetworkPeerConfig, while preserving the existing authorization, ownership
validation, and response payload.

In `@app/Jobs/Network/ApplyNetworkFirewallJob.php`:
- Around line 41-44: Update the status filter in the network member query within
ApplyNetworkFirewallJob to target ACTIVE, PENDING, and UPDATING members, while
continuing to exclude members being removed. Keep the existing bulk update
behavior and mark all eligible synchronising members as FAILED.

In `@app/Jobs/Network/SyncNetworkServerJob.php`:
- Around line 46-64: Update failed() in
app/Jobs/Network/SyncNetworkServerJob.php (lines 46-64) and
app/Jobs/Network/PollPeerHandshakesJob.php (lines 49-58) to accept \Throwable
instead of Exception, preserving the existing logging and status-update behavior
for all throwable failures.

In `@app/Models/Network.php`:
- Around line 31-33: Update the Network model’s relationship property
annotations to document the remaining collections: add Collection<int,
ServerNetworkRule> $serverRules and Collection<int, ServerLog> $serverLogs
alongside the existing $servers, $firewallRules, and $peers annotations.

In `@app/Models/NetworkServer.php`:
- Around line 30-48: Add `private_key` to the NetworkServer model’s hidden
attributes via its `$hidden` configuration, matching the existing protection
pattern used by `Server::$authentication`; leave the current casts and fillable
attributes unchanged.

In `@app/Models/Server.php`:
- Around line 130-167: Update the Server deletion callbacks and the
networkSiblingsToResync flow so sibling re-sync dispatch is deferred until the
surrounding database transaction commits, using the framework’s after-commit
queue mechanism rather than immediate toPresent execution. Preserve the
collected sibling IDs and cleanup behavior, and ensure
DispatchNetworkServerSync/toPresent cannot queue SyncNetworkServerJob before the
Server deletion is committed.

In `@app/Models/ServerIpAddress.php`:
- Around line 50-72: Update the ServerIpAddress deletion flow in booted,
specifically the deleted callback, to remove each affected CUSTOM network
membership after the IP is deleted instead of reapplying firewall rules for the
incomplete membership. Reuse RemoveServerFromNetwork or the established
membership-removal mechanism, using reapplyNetworkIds to identify the affected
networks, and eliminate the direct ApplyNetworkFirewall call for these orphaned
memberships.

In `@app/ServerProviders/DigitalOcean.php`:
- Around line 102-134: Bound pagination in fetchAll for
app/ServerProviders/DigitalOcean.php (lines 102-134) by adding a maximum page
count to the links.pages.next continuation check. In
app/ServerProviders/Hetzner.php (lines 107-139), only advance when
meta.pagination.next_page is strictly greater than the current page. In
app/ServerProviders/Vultr.php (lines 116-148), stop pagination when
meta.links.next repeats the previous cursor.

In `@app/Services/Firewall/Ufw.php`:
- Around line 60-89: Update the serverRules query in the Ufw rule-application
flow to apply a deterministic ordering, matching the ordering used for
networkRules or the intended firewall precedence, before concatenating and
applying the collections. Keep the existing status filtering and finalize
success/failure handling unchanged.

In `@app/Tables/Servers/ServerNetworkRuleTable.php`:
- Around line 18-27: Update the query() method to replace the duplicated
orderByRaw/orderBy ordering chain with the ServerNetworkRule::scopeOrdered()
scope. Preserve the existing filtering, eager loading, and pagination behavior
while reusing the centralized ordering definition.

In `@database/factories/NetworkFactory.php`:
- Line 21: Update the network factory’s block generation so a single unique
block number provides enough range for both middle CIDR octets, rather than
limiting uniqueness to 0–255. Preserve the existing Faker uniqueness and ensure
the derived octets remain valid network-address values.

In `@resources/js/pages/networks/components/add-server.tsx`:
- Line 1: Extract the duplicated server-selection and private-IP refresh
behavior from AddServer and CreateNetwork into a shared useServerWithPrivateIp
hook. Move each component’s servers state, selection handler
(selectServer/selectPrimaryServer), applyRefreshedIps callback, and dynamic
ip_addresses.<id> error lookup into the hook, then update both dialogs to
consume its API while preserving their existing form behavior.

In `@resources/js/pages/networks/settings.tsx`:
- Around line 28-30: Update the form synchronization near the useForm
initialization so form.data.name follows realtime network.name changes when the
form is not dirty, while preserving any locally edited name. Ensure the non-edit
display and edit prefill use the latest record name without overwriting a dirty
local edit.

In `@tests/Feature/NetworkPeerTest.php`:
- Around line 157-158: Replace the explanatory inline // comments near the
WireGuard configuration assertions in the relevant test methods with PHPDoc
block comments, including the additional comment around the second referenced
location; preserve their wording and keep the comments outside executable
statements as required by the repository’s PHP style.

In `@tests/Feature/NetworkProviderSyncTest.php`:
- Around line 46-48: Replace the inline rationale comments with PHPDoc blocks:
in tests/Feature/NetworkProviderSyncTest.php#L46-L48, place the Http::fake()
merge-semantics explanation above setUp(); at `#L232`, document
test_already_leaving_member_is_not_torn_down_again; and in
tests/Feature/NetworkTest.php#L307-L308, document
test_empty_cidr_is_stored_as_null_not_canonicalised. Remove the corresponding
inline comments while preserving their explanations.

In `@tests/Feature/ProviderPrivateNetworkTest.php`:
- Around line 411-447: Extend the injection test coverage from member addresses
to WireGuard public keys by adding a matching untrusted-public-key data provider
and test in tests/Feature/ProviderPrivateNetworkTest.php. Exercise shell
metacharacters, embedded whitespace, and other invalid values through the
private-network member public_key path, then assert the resulting public key is
dropped. Tighten WireGuardPublicKeyRule so embedded whitespace and these unsafe
inputs are rejected before interpolation into the WireGuard template.

---

Outside diff comments:
In `@resources/js/components/app-sidebar.tsx`:
- Around line 202-212: Update the groupActive calculation in the childItem.some
callback to compare currentPath() with childItem.href when
childItem.onlyActivePath is set, rather than comparing against the
onlyActivePath flag. Preserve the existing hidden-child exclusion and startsWith
behavior for other children.

---

Duplicate comments:
In `@app/Console/Commands/ReconcileNetworksCommand.php`:
- Around line 143-165: Update forceConverge() to perform the membership deletion
and related network state updates within a DB::transaction(), and invoke
DispatchNetworkServerSync::resyncMembers() for the remaining members after the
forced removal. Preserve the existing logging, socket event, firewall
application, and network status recomputation while ensuring peer configurations
are resynchronized.

In `@app/Models/NetworkServer.php`:
- Around line 41-48: Update the private_key entry in NetworkServer’s casts to
use the repository-standard encrypted:json cast, matching
Server::$authentication, while leaving the other casts unchanged.

In `@app/Services/VPN/WireGuard.php`:
- Around line 207-219: Update uploadConf so the temporary configuration file is
created with 0600 permissions before writing content, rather than calling
Storage::put followed by chmod. Remove the suppressed chmod call and preserve
the existing SSH upload and finally-block cleanup behavior.

In `@database/factories/NetworkServerFactory.php`:
- Around line 25-26: Update the network server factory key generation to use the
production WireGuard key-pair generator, GenerateWireGuardKeys::generate(), and
assign its corresponding private and public values to private_key and public_key
instead of generating each with random_bytes.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 6cbb0338-b6c0-4cdb-8d2a-d18d999d1362

📥 Commits

Reviewing files that changed from the base of the PR and between 956ca41 and 09b1bc9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (153)
  • .gitignore
  • .prettierignore
  • app/Actions/Network/AddServersToNetwork.php
  • app/Actions/Network/AllocateNetworkBlock.php
  • app/Actions/Network/ApplyNetworkFirewall.php
  • app/Actions/Network/ConcealNetworkPeerKey.php
  • app/Actions/Network/CreateNetwork.php
  • app/Actions/Network/CreateNetworkPeer.php
  • app/Actions/Network/DeleteNetwork.php
  • app/Actions/Network/DeleteNetworkPeer.php
  • app/Actions/Network/DispatchNetworkServerSync.php
  • app/Actions/Network/FinalizeServerNetworkRules.php
  • app/Actions/Network/GenerateWireGuardKeys.php
  • app/Actions/Network/GetNetworkPeerConfig.php
  • app/Actions/Network/ManageNetworkFirewallRule.php
  • app/Actions/Network/MaterializeServerNetworkRules.php
  • app/Actions/Network/RecomputeNetworkStatus.php
  • app/Actions/Network/RegenerateNetworkPeerKeys.php
  • app/Actions/Network/RemoveServerFromNetwork.php
  • app/Actions/Network/SyncNetwork.php
  • app/Actions/Network/SyncProviderNetworks.php
  • app/Actions/Network/UpdateNetwork.php
  • app/Actions/Network/UpdateNetworkPeer.php
  • app/Actions/Network/UpdateNetworkServerIp.php
  • app/Actions/Server/EditServer.php
  • app/Actions/Service/Install.php
  • app/Console/Commands/ReconcileNetworksCommand.php
  • app/Console/Kernel.php
  • app/DTOs/PrivateNetworkDTO.php
  • app/DTOs/PrivateNetworkMemberDTO.php
  • app/Enums/NetworkAddressingPool.php
  • app/Enums/NetworkPeerStatus.php
  • app/Enums/NetworkServerStatus.php
  • app/Enums/NetworkStatus.php
  • app/Enums/NetworkType.php
  • app/Enums/ServerNetworkRuleKind.php
  • app/Exceptions/PrivateNetworkSyncError.php
  • app/Http/Controllers/FirewallController.php
  • app/Http/Controllers/NetworkController.php
  • app/Http/Controllers/NetworkFirewallRuleController.php
  • app/Http/Controllers/NetworkPeerController.php
  • app/Http/Controllers/NetworkServerController.php
  • app/Http/Middleware/HandleInertiaRequests.php
  • app/Http/Resources/NetworkFirewallRuleResource.php
  • app/Http/Resources/NetworkPeerResource.php
  • app/Http/Resources/NetworkResource.php
  • app/Http/Resources/NetworkServerResource.php
  • app/Http/Resources/ServerLogResource.php
  • app/Jobs/Network/ApplyNetworkFirewallJob.php
  • app/Jobs/Network/PollPeerHandshakesJob.php
  • app/Jobs/Network/SyncNetworkServerJob.php
  • app/Jobs/Network/SyncProviderNetworksJob.php
  • app/Models/Network.php
  • app/Models/NetworkFirewallRule.php
  • app/Models/NetworkPeer.php
  • app/Models/NetworkServer.php
  • app/Models/Project.php
  • app/Models/Server.php
  • app/Models/ServerIpAddress.php
  • app/Models/ServerLog.php
  • app/Models/ServerNetworkRule.php
  • app/Policies/NetworkPolicy.php
  • app/Providers/ServiceTypeServiceProvider.php
  • app/ServerProviders/AWS.php
  • app/ServerProviders/AbstractProvider.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Linode.php
  • app/ServerProviders/ProvidesPrivateNetworks.php
  • app/ServerProviders/Vultr.php
  • app/Services/Firewall/Ufw.php
  • app/Services/VPN/VPN.php
  • app/Services/VPN/WireGuard.php
  • app/Support/Cidr.php
  • app/Support/Testing/SSHFake.php
  • app/Tables/NetworkTable.php
  • app/Tables/Networks/NetworkFirewallRuleTable.php
  • app/Tables/Networks/NetworkPeerTable.php
  • app/Tables/Networks/NetworkServerTable.php
  • app/Tables/Servers/ServerNetworkRuleTable.php
  • app/Traits/UniqueQueue.php
  • app/ValidationRules/WireGuardPublicKeyRule.php
  • database/factories/NetworkFactory.php
  • database/factories/NetworkFirewallRuleFactory.php
  • database/factories/NetworkPeerFactory.php
  • database/factories/NetworkServerFactory.php
  • database/migrations/2026_07_24_195720_create_networks_table.php
  • database/migrations/2026_07_24_195721_create_network_servers_table.php
  • database/migrations/2026_07_24_195722_create_network_firewall_rules_table.php
  • database/migrations/2026_07_24_195723_create_server_network_rules_table.php
  • database/migrations/2026_07_24_195724_create_network_peers_table.php
  • database/migrations/2026_07_24_195725_add_network_id_to_server_logs.php
  • docs/4.x/README.md
  • docs/4.x/automation.md
  • docs/4.x/networks/create.md
  • docs/4.x/networks/firewall.md
  • docs/4.x/networks/overview.md
  • docs/4.x/networks/peers.md
  • docs/4.x/networks/provider-networks.md
  • docs/4.x/networks/servers.md
  • docs/4.x/servers/firewall.md
  • docs/4.x/servers/network.md
  • docs/4.x/settings/server-providers.md
  • eslint.config.js
  • resources/js/components/app-sidebar-nested.tsx
  • resources/js/components/app-sidebar.tsx
  • resources/js/components/dialogs/registry.ts
  • resources/js/components/ui/combobox.tsx
  • resources/js/layouts/database/layout.tsx
  • resources/js/layouts/network/layout.tsx
  • resources/js/lib/utils.ts
  • resources/js/pages/backups/components/restore-backup.tsx
  • resources/js/pages/backups/files.tsx
  • resources/js/pages/backups/index.tsx
  • resources/js/pages/firewall/index.tsx
  • resources/js/pages/networks/components/add-peer.tsx
  • resources/js/pages/networks/components/add-server.tsx
  • resources/js/pages/networks/components/create-network.tsx
  • resources/js/pages/networks/components/edit-network-server.tsx
  • resources/js/pages/networks/components/firewall-rule-form.tsx
  • resources/js/pages/networks/components/log-columns.tsx
  • resources/js/pages/networks/components/peer-config.tsx
  • resources/js/pages/networks/components/private-ip-select.tsx
  • resources/js/pages/networks/firewall.tsx
  • resources/js/pages/networks/index.tsx
  • resources/js/pages/networks/logs.tsx
  • resources/js/pages/networks/peers.tsx
  • resources/js/pages/networks/servers.tsx
  • resources/js/pages/networks/settings.tsx
  • resources/js/pages/networks/show.tsx
  • resources/js/pages/redirects/components/edit-redirect.tsx
  • resources/js/pages/redirects/components/redirect-form-fields.tsx
  • resources/js/pages/server-network/index.tsx
  • resources/js/pages/services/components/install.tsx
  • resources/js/pages/site-tooling/index.tsx
  • resources/js/types/network.d.ts
  • resources/js/types/server-log.d.ts
  • resources/views/ssh/services/firewall/ufw/apply-rules.blade.php
  • resources/views/ssh/wireguard/conf.blade.php
  • resources/views/ssh/wireguard/configure.blade.php
  • resources/views/ssh/wireguard/install.blade.php
  • resources/views/ssh/wireguard/latest-handshakes.blade.php
  • resources/views/ssh/wireguard/remove-network.blade.php
  • resources/views/ssh/wireguard/uninstall.blade.php
  • resources/views/wireguard/peer-conf.blade.php
  • tests/Feature/NetworkFirewallTest.php
  • tests/Feature/NetworkPeerTest.php
  • tests/Feature/NetworkProviderSyncTest.php
  • tests/Feature/NetworkSyncTest.php
  • tests/Feature/NetworkTest.php
  • tests/Feature/ProviderPrivateNetworkTest.php
  • tests/Feature/UniqueQueueTest.php
  • tests/Unit/Actions/Service/UninstallTest.php

Comment thread app/Actions/Network/AllocateNetworkBlock.php
Comment thread app/Actions/Network/CreateNetwork.php
Comment thread app/Actions/Network/FinalizeServerNetworkRules.php
Comment thread app/Actions/Network/MaterializeServerNetworkRules.php
Comment thread app/Actions/Network/MaterializeServerNetworkRules.php
Comment thread resources/js/pages/networks/components/add-server.tsx
Comment thread resources/js/pages/networks/settings.tsx
Comment thread tests/Feature/NetworkPeerTest.php Outdated
Comment thread tests/Feature/NetworkProviderSyncTest.php Outdated
Comment thread tests/Feature/ProviderPrivateNetworkTest.php

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
app/Jobs/Network/SyncNetworkServerJob.php (1)

46-63: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the failure transition against concurrent removal.

A removal can set the member to LEAVING while this sync is in flight; $this->member->save() then overwrites it as FAILED, preventing the teardown path from proceeding. Update only synchronising states and broadcast only if that conditional update succeeds.

Proposed fix
-            $this->member->status = NetworkServerStatus::FAILED;
-            $this->member->save();
-            $this->broadcastMember();
+            $updated = NetworkServer::query()
+                ->whereKey($this->member->id)
+                ->whereIn('status', [
+                    NetworkServerStatus::PENDING,
+                    NetworkServerStatus::UPDATING,
+                ])
+                ->update(['status' => NetworkServerStatus::FAILED]);
+
+            if ($updated === 1) {
+                $this->broadcastMember();
+            }

As per path instructions, the required UniqueQueue pattern does not protect this database state transition from concurrent removal requests.

🤖 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 `@app/Jobs/Network/SyncNetworkServerJob.php` around lines 46 - 63, Update the
failure transition in SyncNetworkServerJob::failed so it conditionally changes
the member from the synchronising state to FAILED at the database level,
preventing an existing LEAVING state from being overwritten. Broadcast the
member only when that conditional update succeeds; preserve the teardown branch
and status recomputation behavior.

Source: Path instructions

🤖 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 `@app/Models/Server.php`:
- Around line 164-172: Update the after-commit query in the callback around
DispatchNetworkServer::toPresent() to retain a predicate excluding NetworkServer
records whose status is LEAVING when reloading the IDs. Keep the existing ID
filtering, relationships, and synchronization behavior unchanged for non-LEAVING
siblings.

In `@app/Models/ServerIpAddress.php`:
- Around line 71-75: Move the custom network membership cleanup from the IP
address before-commit/deleted handling into a DB::afterCommit callback scheduled
after the DELETING status save completes. Keep the existing NetworkServer query
and RemoveServerFromNetwork invocation unchanged within that deferred callback,
ensuring cleanup runs only after the transaction commits.

In `@app/ServerProviders/DigitalOcean.php`:
- Around line 131-132: Update the pagination flow around $hasNext and $page so
that a response with a next page when self::MAX_PAGES is reached throws a sync
error instead of ending successfully. Preserve normal page advancement below the
cap, and ensure the exception propagates through the sync path so pruning is
skipped.

---

Outside diff comments:
In `@app/Jobs/Network/SyncNetworkServerJob.php`:
- Around line 46-63: Update the failure transition in
SyncNetworkServerJob::failed so it conditionally changes the member from the
synchronising state to FAILED at the database level, preventing an existing
LEAVING state from being overwritten. Broadcast the member only when that
conditional update succeeds; preserve the teardown branch and status
recomputation behavior.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 3b4ec84a-8a1c-4b41-bca4-4760dea33228

📥 Commits

Reviewing files that changed from the base of the PR and between 09b1bc9 and 71f42cf.

📒 Files selected for processing (41)
  • app/Actions/Network/AddServersToNetwork.php
  • app/Actions/Network/AllocateNetworkBlock.php
  • app/Actions/Network/CreateNetwork.php
  • app/Actions/Network/CreateWireGuardMembers.php
  • app/Actions/Network/DispatchNetworkServerSync.php
  • app/Actions/Network/FinalizeServerNetworkRules.php
  • app/Actions/Network/MaterializeServerNetworkRules.php
  • app/Actions/Network/RemoveServerFromNetwork.php
  • app/Actions/Server/EditServer.php
  • app/Console/Commands/ReconcileNetworksCommand.php
  • app/Http/Controllers/NetworkController.php
  • app/Http/Controllers/NetworkPeerController.php
  • app/Jobs/Network/ApplyNetworkFirewallJob.php
  • app/Jobs/Network/PollPeerHandshakesJob.php
  • app/Jobs/Network/SyncNetworkServerJob.php
  • app/Models/Network.php
  • app/Models/NetworkPeer.php
  • app/Models/NetworkServer.php
  • app/Models/Server.php
  • app/Models/ServerIpAddress.php
  • app/Models/ServerNetworkRule.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Vultr.php
  • app/Services/Firewall/Ufw.php
  • app/Services/VPN/WireGuard.php
  • app/Tables/Servers/ServerNetworkRuleTable.php
  • app/ValidationRules/WireGuardPublicKeyRule.php
  • database/factories/NetworkFactory.php
  • database/factories/NetworkServerFactory.php
  • resources/js/components/app-sidebar.tsx
  • resources/js/hooks/use-server-with-private-ip.ts
  • resources/js/pages/firewall/index.tsx
  • resources/js/pages/networks/components/add-server.tsx
  • resources/js/pages/networks/components/create-network.tsx
  • resources/js/pages/networks/components/peer-config.tsx
  • resources/js/pages/networks/settings.tsx
  • resources/js/types/network.d.ts
  • tests/Feature/NetworkPeerTest.php
  • tests/Feature/NetworkProviderSyncTest.php
  • tests/Feature/NetworkTest.php

Comment thread app/Models/Server.php Outdated
Comment thread app/Models/ServerIpAddress.php Outdated
Comment thread app/ServerProviders/DigitalOcean.php Outdated

@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: 3

Caution

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

⚠️ Outside diff range comments (5)
app/Services/VPN/WireGuard.php (1)

207-220: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Clean up the temporary secret on every failure path.

The try block starts after the second put(). If writing, chmod(), or the upload preparation fails, the temporary file containing the WireGuard private key is not deleted. Move the complete create/write/chmod/upload sequence inside try/finally and fail closed when permission or write operations fail.

Suggested fix
         $tmpName = 'wg-'.Str::random(20);
         $disk = Storage::disk('local');

-        $disk->put($tmpName, '');
-        $path = $disk->path($tmpName);
-        chmod($path, 0600);
-        $disk->put($tmpName, $content);
-
         try {
+            $disk->put($tmpName, '');
+            $path = $disk->path($tmpName);
+            if (! chmod($path, 0600)) {
+                throw new \RuntimeException('Unable to restrict WireGuard temporary file permissions.');
+            }
+            $disk->put($tmpName, $content);
             $ssh->upload($path, $remote, 'root');
         } finally {
             $disk->delete($tmpName);
🤖 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 `@app/Services/VPN/WireGuard.php` around lines 207 - 220, Update uploadConf so
the entire temporary-file creation, write, permission-setting, and SSH upload
sequence is inside the existing try/finally, ensuring the generated temporary
secret is deleted on any failure. Validate the initial and content writes plus
chmod succeed, and fail closed by throwing or propagating an error when any
operation fails before cleanup.
app/Jobs/Network/PollPeerHandshakesJob.php (1)

31-45: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Select a member with a ready WireGuard service.

reachableMember() only filters membership and server status. If the first ACTIVE member has no READY WireGuard service, handle() returns without trying another eligible member, even when one is available. This silently disables handshake polling for the entire network.

Select or iterate through reachable members until a READY WireGuard service is found, and cover this fallback case with a feature test.

Also applies to: 87-94

🤖 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 `@app/Jobs/Network/PollPeerHandshakesJob.php` around lines 31 - 45, Update
PollPeerHandshakesJob::handle and its member-selection flow so it iterates
through reachable members until finding one whose WireGuard service is a READY
Service, rather than returning after the first ineligible member; then apply
latestHandshakes using that handler and add a feature test covering fallback to
a later eligible member.
app/Jobs/Network/SyncNetworkServerJob.php (1)

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

Guard queued jobs against stale membership state.

The job performs synchronisation without reloading or atomically claiming the current NetworkServer status. A queued sync can therefore run after removal changes the member to LEAVING, while failed() can later overwrite a newer state with FAILED.

Refresh and atomically claim the expected state before SSH work (UPDATING for sync and LEAVING for teardown), and only update/broadcast failure when that transition still succeeds.

Also applies to: 46-63

🤖 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 `@app/Jobs/Network/SyncNetworkServerJob.php` around lines 31 - 43, Update
SyncNetworkServerJob::handle and its teardown/synchronization flow to refresh
the current NetworkServer and atomically claim the expected status before any
SSH work: UPDATING for sync and LEAVING for teardown. Abort when the claim fails
because membership state is stale or already changed. In failed(), only
transition to FAILED and broadcast the failure when the atomic state transition
still succeeds, preventing stale jobs from overwriting newer membership state.
app/Actions/Network/AddServersToNetwork.php (1)

37-41: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Serialise the WireGuard port-conflict check.

The conflict query runs before any shared lock. Concurrent adds—or an add racing network creation—can both pass validation and assign the same UDP port to one server, leaving one interface unable to bind. Lock the project row and run validateNoPortConflict() inside this transaction.

Proposed fix
-        if ($network->type === NetworkType::WIREGUARD) {
-            $this->validateNoPortConflict($network, $input['servers'] ?? []);
-        }
         $newMemberIds = DB::transaction(function () use ($network, $input): array {
+            if ($network->type === NetworkType::WIREGUARD) {
+                Project::query()->whereKey($network->project_id)->lockForUpdate()->firstOrFail();
+                $this->validateNoPortConflict($network, $input['servers']);
+            }
+
             return $network->type === NetworkType::WIREGUARD
                 ? $this->addWireGuard($network, $input)
                 : $this->addCustom($network, $input);
         });
🤖 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 `@app/Actions/Network/AddServersToNetwork.php` around lines 37 - 41, Update the
transaction in AddServersToNetwork so it first acquires the project row’s shared
lock, then runs validateNoPortConflict() and the WireGuard add flow while that
lock is held. Ensure network creation and concurrent server additions serialize
through the same project lock, while leaving the addCustom path unchanged.
app/Actions/Network/RemoveServerFromNetwork.php (1)

20-28: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the removal lifecycle atomic before dispatching remote work.

teardown() persists LEAVING and queues a job before sibling synchronisation/firewall handling and status recomputation complete. A later failure leaves partial database state and already-dispatched work. Transactionally persist the lifecycle changes, then defer queue/firewall side effects until commit.

As per coding guidelines and path instructions, “Wrap multi-step data mutations in database transactions to prevent partial state.”

🤖 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 `@app/Actions/Network/RemoveServerFromNetwork.php` around lines 20 - 28, Update
the removal flow around RemoveServerFromNetwork and its teardown, resyncMembers,
firewall handle, and recompute handle calls to execute database lifecycle
mutations within a transaction, ensuring all persistence and status
recomputation succeeds atomically. Defer teardown’s queued job and the
subsequent synchronization/firewall side effects until the transaction commits,
so failures roll back state without dispatching remote work.

Sources: Coding guidelines, Path instructions

🤖 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 `@app/ServerProviders/DigitalOcean.php`:
- Around line 131-132: Make pagination failures abort synchronization instead of
returning partial provider state: in app/ServerProviders/DigitalOcean.php lines
131-132, throw PrivateNetworkSyncError when a next link exists at the MAX_PAGES
limit; in app/ServerProviders/Hetzner.php lines 134-135, throw when next_page is
not greater than the current page; and in app/ServerProviders/Vultr.php lines
143-144, throw when the pagination cursor repeats. Preserve normal advancement
and completion behavior for valid pagination.

In `@database/factories/NetworkServerFactory.php`:
- Around line 24-27: Update NetworkServerFactory so the value assigned by
hostAddress() is generated within the CIDR produced for the related
Network::factory() record. Coordinate network and member IP generation, ensuring
factory-created memberships always use an address inside the generated network’s
/24 rather than independently selecting from the broader range.

In `@resources/js/pages/networks/settings.tsx`:
- Around line 34-40: Update the successful save flow around form.put so it
resets the form baseline and clears its dirty state after the request succeeds.
Ensure the existing isDirty guard in the useEffect can accept subsequent
realtime network.name updates and the Save button is hidden after a successful
save, without changing unsaved-edit behavior.

---

Outside diff comments:
In `@app/Actions/Network/AddServersToNetwork.php`:
- Around line 37-41: Update the transaction in AddServersToNetwork so it first
acquires the project row’s shared lock, then runs validateNoPortConflict() and
the WireGuard add flow while that lock is held. Ensure network creation and
concurrent server additions serialize through the same project lock, while
leaving the addCustom path unchanged.

In `@app/Actions/Network/RemoveServerFromNetwork.php`:
- Around line 20-28: Update the removal flow around RemoveServerFromNetwork and
its teardown, resyncMembers, firewall handle, and recompute handle calls to
execute database lifecycle mutations within a transaction, ensuring all
persistence and status recomputation succeeds atomically. Defer teardown’s
queued job and the subsequent synchronization/firewall side effects until the
transaction commits, so failures roll back state without dispatching remote
work.

In `@app/Jobs/Network/PollPeerHandshakesJob.php`:
- Around line 31-45: Update PollPeerHandshakesJob::handle and its
member-selection flow so it iterates through reachable members until finding one
whose WireGuard service is a READY Service, rather than returning after the
first ineligible member; then apply latestHandshakes using that handler and add
a feature test covering fallback to a later eligible member.

In `@app/Jobs/Network/SyncNetworkServerJob.php`:
- Around line 31-43: Update SyncNetworkServerJob::handle and its
teardown/synchronization flow to refresh the current NetworkServer and
atomically claim the expected status before any SSH work: UPDATING for sync and
LEAVING for teardown. Abort when the claim fails because membership state is
stale or already changed. In failed(), only transition to FAILED and broadcast
the failure when the atomic state transition still succeeds, preventing stale
jobs from overwriting newer membership state.

In `@app/Services/VPN/WireGuard.php`:
- Around line 207-220: Update uploadConf so the entire temporary-file creation,
write, permission-setting, and SSH upload sequence is inside the existing
try/finally, ensuring the generated temporary secret is deleted on any failure.
Validate the initial and content writes plus chmod succeed, and fail closed by
throwing or propagating an error when any operation fails before cleanup.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 3b4ec84a-8a1c-4b41-bca4-4760dea33228

📥 Commits

Reviewing files that changed from the base of the PR and between 09b1bc9 and 71f42cf.

📒 Files selected for processing (41)
  • app/Actions/Network/AddServersToNetwork.php
  • app/Actions/Network/AllocateNetworkBlock.php
  • app/Actions/Network/CreateNetwork.php
  • app/Actions/Network/CreateWireGuardMembers.php
  • app/Actions/Network/DispatchNetworkServerSync.php
  • app/Actions/Network/FinalizeServerNetworkRules.php
  • app/Actions/Network/MaterializeServerNetworkRules.php
  • app/Actions/Network/RemoveServerFromNetwork.php
  • app/Actions/Server/EditServer.php
  • app/Console/Commands/ReconcileNetworksCommand.php
  • app/Http/Controllers/NetworkController.php
  • app/Http/Controllers/NetworkPeerController.php
  • app/Jobs/Network/ApplyNetworkFirewallJob.php
  • app/Jobs/Network/PollPeerHandshakesJob.php
  • app/Jobs/Network/SyncNetworkServerJob.php
  • app/Models/Network.php
  • app/Models/NetworkPeer.php
  • app/Models/NetworkServer.php
  • app/Models/Server.php
  • app/Models/ServerIpAddress.php
  • app/Models/ServerNetworkRule.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Vultr.php
  • app/Services/Firewall/Ufw.php
  • app/Services/VPN/WireGuard.php
  • app/Tables/Servers/ServerNetworkRuleTable.php
  • app/ValidationRules/WireGuardPublicKeyRule.php
  • database/factories/NetworkFactory.php
  • database/factories/NetworkServerFactory.php
  • resources/js/components/app-sidebar.tsx
  • resources/js/hooks/use-server-with-private-ip.ts
  • resources/js/pages/firewall/index.tsx
  • resources/js/pages/networks/components/add-server.tsx
  • resources/js/pages/networks/components/create-network.tsx
  • resources/js/pages/networks/components/peer-config.tsx
  • resources/js/pages/networks/settings.tsx
  • resources/js/types/network.d.ts
  • tests/Feature/NetworkPeerTest.php
  • tests/Feature/NetworkProviderSyncTest.php
  • tests/Feature/NetworkTest.php

Comment thread app/ServerProviders/DigitalOcean.php Outdated
Comment thread database/factories/NetworkServerFactory.php
Comment thread resources/js/pages/networks/settings.tsx
@RichardAnderson
RichardAnderson marked this pull request as ready for review July 25, 2026 10:09

@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.

Caution

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

⚠️ Outside diff range comments (2)
app/Models/Server.php (1)

130-175: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Static sibling-resync accumulator can leak entries if deletion fails.

$networkSiblingsToResync is a class-level static array populated in the first deleting hook, and only cleared in deleted. The second deleting hook (unchanged, below) wraps the rest of the teardown in a transaction and rethrows on failure, which aborts the delete pipeline before deleted ever fires — leaving that server's entry in the static array permanently. In a long-running queue worker process this grows unbounded across failed deletions.

ServerIpAddress::$reapplyNetworkIds (same PR) uses a non-static, per-instance property for the exact same producer/consumer pattern across deletingdeleted — the same instance is passed through both hooks, so there's no need for a static keyed array here either.

🐛 Proposed fix
-    /** `@var` array<int, array<int, int>> */
-    protected static array $networkSiblingsToResync = [];
+    /** `@var` array<int, int> */
+    public array $networkSiblingsToResync = [];

     public static function boot(): void
     {
         parent::boot();

         static::deleting(function (Server $server): void {
             $siblings = [];
             ...
-            static::$networkSiblingsToResync[$server->id] = array_keys($siblings);
+            $server->networkSiblingsToResync = array_keys($siblings);
         });

         static::deleted(function (Server $server): void {
-            $ids = static::$networkSiblingsToResync[$server->id] ?? [];
-            unset(static::$networkSiblingsToResync[$server->id]);
+            $ids = $server->networkSiblingsToResync;
             ...
🤖 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 `@app/Models/Server.php` around lines 130 - 175, Replace the static keyed
$networkSiblingsToResync accumulator with a non-static per-instance property on
Server, and update the deleting and deleted hooks to write and consume that
property directly. Remove the static declaration and cleanup logic, while
preserving the existing sibling collection and after-commit resynchronization
behavior.
app/Actions/Network/AddServersToNetwork.php (1)

63-82: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard WireGuard port conflicts with a port-level lock.

validateNoPortConflict() reads other WireGuard networks by port, but this action only locks the current Project and Network rows. Concurrent adds to different WireGuard networks with the same port can both pass the check before either commits, so the second config creation can fail later on the host with a duplicate UDP port bind. Wrap the conflict check and membership creation inside a named lock keyed by the port.

🤖 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 `@app/Actions/Network/AddServersToNetwork.php` around lines 63 - 82, The
addWireGuard method must serialize WireGuard operations sharing the same port.
Acquire a named lock keyed by the configured port before calling
validateNoPortConflict, and keep both that validation and members->create within
the lock’s callback/scope so concurrent networks cannot pass the check
simultaneously.
🤖 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.

Outside diff comments:
In `@app/Actions/Network/AddServersToNetwork.php`:
- Around line 63-82: The addWireGuard method must serialize WireGuard operations
sharing the same port. Acquire a named lock keyed by the configured port before
calling validateNoPortConflict, and keep both that validation and
members->create within the lock’s callback/scope so concurrent networks cannot
pass the check simultaneously.

In `@app/Models/Server.php`:
- Around line 130-175: Replace the static keyed $networkSiblingsToResync
accumulator with a non-static per-instance property on Server, and update the
deleting and deleted hooks to write and consume that property directly. Remove
the static declaration and cleanup logic, while preserving the existing sibling
collection and after-commit resynchronization behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fc3a2f69-a3f8-47f5-a521-78bb43eb6d47

📥 Commits

Reviewing files that changed from the base of the PR and between 71f42cf and 13d3b12.

📒 Files selected for processing (11)
  • app/Actions/Network/AddServersToNetwork.php
  • app/Jobs/Network/PollPeerHandshakesJob.php
  • app/Jobs/Network/SyncNetworkServerJob.php
  • app/Models/Server.php
  • app/Models/ServerIpAddress.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Vultr.php
  • app/Services/VPN/WireGuard.php
  • resources/js/pages/networks/settings.tsx
  • tests/Feature/NetworkPeerTest.php

@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.

Caution

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

⚠️ Outside diff range comments (1)
app/Models/Server.php (1)

137-173: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move network resynchronisation into an Action.

Lines 137-173 place network-specific discovery and lifecycle orchestration inside Server::boot(). Delegate this work to an Action while preserving the existing after-commit callback and LEAVING safeguards.

🤖 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 `@app/Models/Server.php` around lines 137 - 173, Extract the network sibling
discovery and post-deletion resynchronisation logic from Server::boot() into a
dedicated Action, and invoke that Action from the deleting/deleted lifecycle
flow. Preserve the existing WireGuard filtering, sibling deduplication, LEAVING
exclusions, and DB::afterCommit dispatch behavior.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@app/Models/Server.php`:
- Around line 137-173: Extract the network sibling discovery and post-deletion
resynchronisation logic from Server::boot() into a dedicated Action, and invoke
that Action from the deleting/deleted lifecycle flow. Preserve the existing
WireGuard filtering, sibling deduplication, LEAVING exclusions, and
DB::afterCommit dispatch behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6c40cb08-b6f9-42cd-9d91-51e1882dea31

📥 Commits

Reviewing files that changed from the base of the PR and between 13d3b12 and a9f5917.

📒 Files selected for processing (1)
  • app/Models/Server.php

@saeedvaziry

Copy link
Copy Markdown
Member

• ## Review result: request changes

Specs

  • P1 — IPv6 endpoints generate invalid WireGuard configs. app/Services/VPN/WireGuard.php:166 concatenates addresses as $ip:$port; IPv6 requires
    [address]:port. Downloaded peer configs have the same issue in app/Actions/Network/GetNetworkPeerConfig.php:56. Networks containing an IPv6-addressed
    server will fail synchronization.

  • P2 — Removing the last provider-backed server strands the network. app/Actions/Network/SyncProviderNetworks.php:347 discards provider contexts without
    current servers. After the sole managed server is deleted, sync never prunes the empty network, while its provider association prevents manual
    deletion.

  • P2 — Firewall protocol and port cannot be independently optional. app/Actions/Network/ManageNetworkFirewallRule.php:93 uses reciprocal required_with
    rules. This rejects “all protocols on port 3306” and “UDP on all ports,” although both the form and documentation describe protocol and port as
    independently optional.

  • P2 — Custom-network addresses need CIDR containment validation. app/Actions/Network/CreateNetwork.php:216 verifies ownership, type, and uniqueness,
    but not that the chosen address belongs to the declared CIDR. Add and update have the same omission. A mismatched address breaks connectivity while
    potentially opening the firewall to an unrelated range.

  • P3 — Provider name is absent from network settings. app/Http/Middleware/HandleInertiaRequests.php:94 does not load serverProvider, while
    NetworkResource only emits provider when that relation is loaded. Managed network settings therefore display -.

Standards and security

  • P1 — Unvalidated server IP reaches an SSH shell template. app/Actions/Server/EditServer.php:49 resynchronizes peers before checking the new address,
    but validation only requires a string. The value becomes an unquoted UFW source through app/Actions/Network/MaterializeServerNetworkRules.php:209,
    permitting shell injection on ready sibling servers. Validate it as an IP address before saving or dispatching synchronization.

  • P1 — Server deletion leaves stale allow rules on remaining members. app/Models/Server.php:141 only reconciles WireGuard networks. For custom networks
    without a CIDR—and provider networks with per-member rules—the remaining servers retain /32 allow rules for the deleted server’s address. That address
    may later be reassigned. The reconciliation should cover every affected network type and live in an Action rather than model callbacks.

  • P2 — Provider failures are swallowed. app/Actions/Network/SyncProviderNetworks.php:48 logs provider failures and continues, so the job reports success
    and users receive no failure state. This conflicts with the project rule that provider errors must bubble. Multiple failures can be collected and
    rethrown after processing unaffected connections.

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