Simple Dedicated IP
Reserving a dedicated IP. Reading the ones a subscription holds, and where it may reserve one, is get_dedicated_ips; this page is the write side, reached through the sdip getter on the SDK Manager.
This is not the Dedicated IP module (dip), which drives the enclave-based protocol. Which one your product uses is decided by the subscription's entitlements.
The flow
- Call
getDedicatedIps.availablelists the locations where the subscription may reserve;reservedwhat it holds. - Call
reservewith theidof anavailablelocation. When it returns, the address is held. - Call
getDedicatedIpsagain and connect through the newreservedentry'sendpoints.
There is no release. Slots are freed by support or billing, never by the app.
Refreshing the locations and connecting to the reserved address
reserve returns only the address and its location id. The endpoints to connect to come from getDedicatedIps, so after a successful reserve call it again for the same subscription. The SDK dropped its cached dataset as part of the reservation, so this call refetches and the new address is in reserved. Match it by ip, and connect through its endpoints exactly as you would with any location's endpoints (see Fetching endpoints for a location).
- Swift
- Kotlin
let assignment = try manager.sdip().reserve(subscriptionId: sub, locationId: location.id)
let refreshed = try manager.instanceDiscovery().getDedicatedIps(subscriptionId: sub)
switch refreshed.dedicatedIps {
case let .ready(available, reserved):
showReservationPicker(available)
showHeldAddresses(reserved)
if let held = reserved.first(where: { $0.ip == assignment.ip }) {
connect(to: held.endpoints)
}
case .failed:
// The address is reserved; the endpoints just could not be read right now. Keep what is on
// screen and call getDedicatedIps again after a minute.
keepPreviousAndRetryLater()
}
val assignment = manager.sdip().reserve(sub, location.id)
when (val ips = manager.instanceDiscovery().getDedicatedIps(sub).dedicatedIps) {
is DedicatedIps.Ready -> {
showReservationPicker(ips.available)
showHeldAddresses(ips.reserved)
ips.reserved.firstOrNull { it.ip == assignment.ip }?.let { connect(it.endpoints) }
}
is DedicatedIps.Failed -> keepPreviousAndRetryLater()
}
Two things to keep straight:
- Re-read
availableas well asreserved. The location you reserved from now has one address fewer, and itsavailableCountmay be zero. - A
Failedanswer here does not undo the reservation. It means the service that serves the endpoints could not be reached; the address is held and will appear once the call succeeds. Do not offer the reserve button again onFailed.
reserve
| Parameter | Description |
|---|---|
| subscription_id | ID of the subscription entitled to Simple Dedicated IP, see Obtaining a VPN Subscription ID |
| location_id | The id of a DedicatedIpLocation from getDedicatedIps().available. Not a ServerLocation id |
Returns SdipReservation, throws SdipException. The server reserves the address at its upstream inside the request, so the call can take longer than most (the SDK allows 60 seconds); run it off the main thread.
Do not retry blindly; see Handling errors.
getAssignments
| Parameter | Description |
|---|---|
| subscription_id | ID of the subscription entitled to Simple Dedicated IP |
Returns an array of SdipAssignment, oldest first, without endpoints. Empty for an entitled subscription with no claim.
SdipAssignment
An enum with two cases:
| Case | Fields | Meaning |
|---|---|---|
Completed | reservation: SdipReservation | The address is held |
Pending | locationId: String | The claim took a slot and the service is still fulfilling the address. Poll getAssignments until it turns Completed, typically within a couple of minutes |
A Pending entry appears when a reserve call failed with TemporarilyUnavailable (a timeout, a lost response, a server error) while the server went on to complete the claim. It counts against the subscription's slots, so do not offer reserve for that location again.
SdipReservation
| Name | Type | Description |
|---|---|---|
| ip | String | The reserved address |
| locationId | String | Matches DedicatedIpLocation.id in getDedicatedIps |
Handling errors
Catch SdipException and branch on its variants; every variant carries the underlying SdkException as source, with the HTTP status and body, for logging. The three cases that need a distinct reaction are NotEntitled, NoCapacity and TemporarilyUnavailable.
- Swift
- Kotlin
do {
let assignment = try manager.sdip().reserve(subscriptionId: sub, locationId: location.id)
showReserved(assignment)
} catch SdipException.NotEntitled {
showUpsell()
} catch SdipException.SlotsExhausted {
showHeldAddressesOnly() // nothing frees a slot from the app
} catch SdipException.NoCapacity {
reloadLocationsAndAskAgain() // getDedicatedIps, then let the user pick another
} catch SdipException.InvalidLocation {
assertionFailure("only DedicatedIpLocation.id values are valid")
} catch SdipException.EntitlementEnded {
showUpsell()
} catch SdipException.TemporarilyUnavailable {
reconcileThenMaybeRetry() // see below
} catch let SdipException.ExceptionDetails(source) {
handleError(error: source)
}
try {
showReserved(manager.sdip().reserve(sub, location.id))
} catch (e: SdipException.NotEntitled) {
showUpsell()
} catch (e: SdipException.SlotsExhausted) {
showHeldAddressesOnly()
} catch (e: SdipException.NoCapacity) {
reloadLocationsAndAskAgain()
} catch (e: SdipException.InvalidLocation) {
error("only DedicatedIpLocation.id values are valid")
} catch (e: SdipException.EntitlementEnded) {
showUpsell()
} catch (e: SdipException.TemporarilyUnavailable) {
reconcileThenMaybeRetry()
} catch (e: SdipException.ExceptionDetails) {
handleError(e.source)
}
TemporarilyUnavailable: reconcile before retrying
TemporarilyUnavailable covers both a server-side error and a transport failure, timeouts included, so it is the one variant to handle here. A claim that failed on the way back may still complete on the server and occupy a slot. So never re-run reserve straight away. Call getAssignments first and look for that location:
Completedfor that location: the reservation went through. Continue as after a success: refetchgetDedicatedIpsand connect. The SDK keeps its cached dataset on a failedreserve, so that refetch may still serve the previous dataset for up to an hour; the address is held either way.Pendingfor that location: the server is still fulfilling it. Show it as in progress and pollgetAssignments; do not callreserveagain.- Nothing for that location: nothing is held, and
reservemay be called again.
suspend fun reconcileThenMaybeRetry() {
val claim = manager.sdip().getAssignments(sub).firstOrNull {
when (it) {
is SdipAssignment.Completed -> it.reservation.locationId == location.id
is SdipAssignment.Pending -> it.locationId == location.id
}
}
when (claim) {
is SdipAssignment.Completed -> refreshAndConnect()
is SdipAssignment.Pending -> showInProgressAndPollLater()
null -> offerRetry()
}
}
getAssignments itself can throw TemporarilyUnavailable. In that case keep whatever is on screen, tell the user the service is unavailable, and let them try again later; do not fall through to another reserve.
Errors
| Variant | Meaning | What to do |
|---|---|---|
NotEntitled | The subscription is not entitled to Simple Dedicated IP | Upsell, or nothing |
SlotsExhausted | Every slot the entitlement grants is taken | Show the held addresses |
NoCapacity | The location sold out meanwhile | Refetch getDedicatedIps, offer another |
InvalidLocation | Not a Simple Dedicated IP location id | A bug: only available[].id values are valid |
EntitlementEnded | Subscription cancelled while in flight | Treat as not entitled |
TemporarilyUnavailable | The service did not complete the call: a server error, a timeout or another transport failure | Reconcile with getAssignments before retrying |
ExceptionDetails | Invalid input, unattributed answer | Inspect source |