External Access — Port Allocation Design¶
Overview¶
Users can open additional ports on their server through the External Access tab. They choose an internal container port and protocol (TCP/UDP); the system assigns a random available port on the same IP already in use by the order's primary ExternalService provision. Users get 3 free allocations per order; beyond that they must purchase more.
Current Infrastructure¶
ip_poolstable +IpPooldomain type: tracks IP addresses with port ranges and exposure kind.port_allocationstable +PortAllocationdomain type: one row per (ip_pool, port, protocol), linked to aprovision_id.ExternalServiceConfigonProvisionConfig::ExternalServicecarriesport_mappings: Vec<ExternalPortMapping>.PortProtocolenum:Tcp | Udp | Both.
1. Domain Changes¶
1.1 Quota tracking — order_port_quotas¶
CREATE TABLE order_port_quotas (
order_id UUID PRIMARY KEY REFERENCES orders(id),
extra_slots INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL
);
Effective quota = 3 + extra_slots.
1.2 PortAllocation additions¶
- Add
order_id UUID NOT NULL REFERENCES orders(id)— enables count-by-order without joins - Add
internal_port: u16— needed for K8s routing
async fn find_by_order(&self, order_id: &OrderId) -> exn::Result<Vec<PortAllocation>, RepositoryError>;
async fn count_by_order(&self, order_id: &OrderId) -> exn::Result<usize, RepositoryError>;
async fn delete(&self, id: &PortAllocationId) -> exn::Result<(), RepositoryError>;
2. Application Service — ExternalAccessService¶
pub trait ExternalAccessService: shaku::Interface {
async fn list_allocations(&self, order_id: &OrderId) -> exn::Result<Vec<PortAllocation>, ExternalAccessError>;
async fn request_allocation(
&self,
order_id: &OrderId,
internal_port: u16,
protocol: PortProtocol,
) -> exn::Result<PortAllocation, ExternalAccessError>;
async fn release_allocation(
&self,
order_id: &OrderId,
allocation_id: &PortAllocationId,
) -> exn::Result<(), ExternalAccessError>;
}
pub enum ExternalAccessError {
OrderNotFound,
OrderNotProvisioned,
QuotaExceeded,
PortProtocolConflict,
NoPoolAvailable,
AllocationNotFound,
Repository(RepositoryError),
}
request_allocation logic¶
- Load order, verify it's
Active - Find the order's
ExternalServiceprovision — use itsip_pool_id - Count existing
PortAllocationrows for this order; check against quota - Reject if
internal_port + protocolis already allocated for this order - Load
find_used_ports(ip_pool_id)and the pool's port range; build the free set; pick a random port - Persist a new
PortAllocationrow - Patch the K8s ExternalService to add the new port mapping via
ClusterService
Port collision: retry up to 3 times, relying on the DB unique constraint on (ip_pool_id, port, protocol).
3. ClusterService changes¶
async fn patch_external_service_ports(
&self,
provision: &Provision,
allocations: &[PortAllocation],
) -> exn::Result<(), ClusterServiceError>;
Reconstructs the full port list (primary game port + all user allocations) and patches the existing K8s Service.
4. API Endpoints¶
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/orders/{order_id}/external-access |
List allocations + quota info |
POST |
/api/v1/orders/{order_id}/external-access |
Request a new allocation |
DELETE |
/api/v1/orders/{order_id}/external-access/{allocation_id} |
Release an allocation |
POST |
/api/v1/admin/orders/{order_id}/external-access/quota |
Grant extra slots (admin) |
GET response¶
{
"free_slots": 3,
"extra_slots": 0,
"used": 1,
"allocations": [
{
"id": "...",
"external_ip": "1.2.3.4",
"external_port": 32145,
"internal_port": 25565,
"protocol": "Tcp",
"allocated_at": "..."
}
]
}
POST request¶
Returns: 409 QuotaExceeded, 409 PortProtocolConflict, or 503 NoPoolAvailable on failure.
Admin quota endpoint¶
Sets extra_slots absolutely (not incrementally).
5. Quota System — Purchasing More¶
Intentionally out of scope for the initial implementation. Future options:
- Catalog add-on: a PortSlotPack catalog item; on invoice paid, increment extra_slots
- Manual: admin grants slots via the admin endpoint or direct DB update
6. Frontend — External Access UI¶
ExternalAccessTab (dashboard/components/orders/external-access-tab.tsx):
- Fetches GET .../external-access on mount
- Shows quota bar: {used} / {free_slots + extra_slots} allocations used
- Lists allocations: external IP:port | internal port | protocol | delete button
- "Open Port" button → OpenPortDialog (internal port input + protocol select)
7. Implementation Order¶
| Phase | What |
|---|---|
| 1 | DB & Domain (migrations, OrderPortQuota, PortAllocation updates) |
| 2 | ExternalAccessServiceImpl (list, request, release) |
| 3 | patch_external_service_ports in KubernetesClusterService |
| 4 | HTTP endpoints |
| 5 | Frontend (ExternalAccessTab, OpenPortDialog, PortAllocationRow) |
| 6 | Billing hook (deferred) |