> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateways and routing

> Route tables, internet, NAT and egress-only gateways, and floating IPs — how traffic gets in and out of a VPC.

A floating IP is not reachable because you allocated it. Reaching an instance
from the internet takes four things, and three of them have nothing to do with
the address itself.

<Warning>
  **Allocating a floating IP and attaching it is not enough.** The instance's
  subnet also needs an internet gateway attached to the VPC *and* a `0.0.0.0/0`
  route pointing at that gateway. Without the route the reply traffic has
  nowhere to go, and from outside it looks exactly like the inbound packet being
  dropped — so you spend the afternoon debugging the wrong direction.

  The API refuses the attach for this reason rather than handing you a dead
  address.
</Warning>

## The reachability chain

```mermaid theme={null}
flowchart LR
  NET([Internet]) --> IGW["Internet gateway<br/>attached to the VPC"]
  IGW --> RT{"Subnet's route table<br/>0.0.0.0/0 → that gateway?"}
  RT -- no --> D1["Reply has no way out.<br/>Looks like inbound blackhole."]
  RT -- yes --> FIP["Floating IP<br/>attached to the NIC"]
  FIP --> SG{"Security group<br/>allows the port?"}
  SG -- no --> D2["Dropped at the interface"]
  SG -- yes --> VM([Instance])
```

<Steps>
  <Step title="Attach an internet gateway to the VPC">
    <Tabs>
      <Tab title="Console">
        Go to **Networking → Internet Gateways** and choose **Create Internet
        Gateway**. A **Name** is all it takes, and the gateway is "Created
        detached; attach it to a VPC from the list to start routing traffic
        out."

        Open it and choose **Attach**, pick the **VPC**, and confirm with
        **Attach**. **Status** goes from **Detached** to **Attached** and
        **Attached VPC** fills in.
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        POST /v1/internet-gateways        { "name": "main" }
        POST /v1/internet-gateways/{id}/attach  { "vpc_id": "<vpc>" }
        ```
      </Tab>
    </Tabs>

    Attaching creates no routes. It only makes the gateway available as a
    route target.
  </Step>

  <Step title="Add the default route to the subnet's route table">
    <Tabs>
      <Tab title="Console">
        Go to **Networking → Route Tables** and open the table the subnet
        uses — the subnet's page names it under **Route table**. Choose **Add
        Route**, set **Destination CIDR** to `0.0.0.0/0`, set **Target type**
        to **Internet Gateway**, pick the gateway under **Internet gateway**,
        and confirm with **Add Route**.

        If the picker is empty and says "No IGW attached to this VPC. Attach
        one first.", you are still on the previous step.
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        POST /v1/route-tables/{route_table_id}/routes
        { "destination": "0.0.0.0/0", "target_internet_gateway_id": "<igw>" }
        ```
      </Tab>
    </Tabs>

    This is what makes the subnet public. It is also what makes the *reply*
    path exist.
  </Step>

  <Step title="Attach the floating IP to the interface">
    <Tabs>
      <Tab title="Console">
        Go to **Networking → Floating IPs**, choose **Allocate Floating IP**
        if you do not have a spare one, then open the address and choose
        **Attach**. Pick the NIC under **Interface** and confirm with **Attach
        floating IP**.

        The same attach is on the NIC: open it under **Networking →
        Interfaces** and choose **Attach floating IP**.
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        POST /v1/floating-ips/{floating_ip_id}/attach
        { "interface_id": "<interface>" }
        ```
      </Tab>
    </Tabs>

    If the subnet has no default route to an internet gateway, this fails
    with `400`: "the interface's subnet has no default route (0.0.0.0/0) to an
    internet gateway — attach an internet gateway to the VPC and add a default
    route first". The console shows that same text under **Failed to attach
    floating IP**.
  </Step>

  <Step title="Allow the traffic in a security group">
    <Tabs>
      <Tab title="Console">
        Open the NIC under **Networking → Interfaces** and choose **Attach
        security group**. Its rules "start filtering this interface's traffic
        as soon as it is attached."
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        PUT /v1/interfaces/{interface_id}/security-groups
        { "security_group_ids": ["<sg>"] }
        ```
      </Tab>
    </Tabs>

    An interface in no security group drops everything.
    [Security groups](/networking/security-groups) covers the default posture.
  </Step>
</Steps>

<Tip>
  **Create VPC** in the console does the first two steps for you. Turn on
  **Internet gateway** under **Internet access** and ask for at least one
  public subnet, and the workflow runs **Create internet gateway**, **Attach
  internet gateway** and **Configure public internet route** as part of
  provisioning. The floating IP and the security group are still yours to do
  afterwards.
</Tip>

<Note>
  The guard runs in both directions. Deleting the `0.0.0.0/0` route while
  floating IPs still depend on it is refused too: "cannot delete the default
  route: *N* floating IP(s) in this route table's subnets depend on it for
  internet reachability — detach them first". Any other route deletes normally,
  and so does the default route once nothing is attached.
</Note>

## Route tables

Every VPC gets a table called `main` when it is created. Subnets land there
unless they name a `route_table_id`. Table names are unique per VPC, and `main`
is reserved — you cannot create a second table with that name.

<Tabs>
  <Tab title="Console">
    Go to **Networking → Route Tables** and choose **Create Route Table**.
    Under **Route table details**, pick the **VPC** and give it a **Name**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/route-tables
    { "vpc_id": "<vpc>", "name": "private" }
    ```
  </Tab>
</Tabs>

Deleting a table refuses in two cases: the main table cannot be deleted at all,
and a table still associated with subnets is refused with "reassociate them
first". Routes on a table go away with it.

## Routes

<Tabs>
  <Tab title="Console">
    Open the table under **Networking → Route Tables** and choose **Add
    Route**. Fill in **Destination CIDR**, then pick a **Target type** — **IP
    address**, **Internet Gateway** or **NAT Gateway** — and the field below
    it changes to match: **Next-hop IP**, **Internet gateway** or **NAT
    gateway**. Confirm with **Add Route**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/route-tables/{route_table_id}/routes
    { "destination": "10.1.0.0/16", "target_ip": "10.0.1.9" }
    ```
  </Tab>
</Tabs>

`destination` is a CIDR, and **exactly one** target field must be set:

<ResponseField name="target_ip" type="string">
  A unicast next hop **inside this VPC's CIDR**, same family as the
  destination. Loopback, link-local, multicast and unspecified addresses are
  rejected, and so is anything outside the VPC. This is for appliances and NICs
  you run yourself — internet egress is not expressible this way, because a
  gateway target is what pins the route to your own VPC's uplink.
</ResponseField>

<ResponseField name="target_internet_gateway_id" type="uuid">
  The gateway must be attached, and attached to **this** VPC. Otherwise the
  create fails with "target internet gateway is not attached to a VPC" or
  "target internet gateway is attached to a different VPC".
</ResponseField>

<ResponseField name="target_nat_gateway_id" type="uuid">
  IPv4 destinations only. A v6 destination is refused with "nat gateway targets
  do not support IPv6 destinations" — v6 needs no address translation.
</ResponseField>

<ResponseField name="target_egress_only_gateway_id" type="uuid">
  IPv6 destinations only, and refused the other way round.
</ResponseField>

<Note>
  This is the one target the console cannot write. **Add Route** offers only
  **IP address**, **Internet Gateway** and **NAT Gateway**, so a `::/0` route
  at an egress-only gateway is **API only** — as is the gateway itself, see
  [egress-only gateways](#egress-only-gateways-the-ipv6-case).
</Note>

### One route per destination

A table holds at most one route to a given destination. A second one is refused
with `409`.

<Warning>
  The error code on that `409` reads `ROUTER_NAME_EXISTS`, which is misleading —
  read the message, which says "destination CIDR already routed in this table".
  There is no equal-cost pair and no tie-break between two routes to the same
  prefix, because the second one never gets created.
</Warning>

A route's `destination` and target are immutable; `PATCH` only takes
`description` and `tags`. To repoint a route, delete it and create the
replacement.

## Internet gateways

A gateway is created detached and attached to a VPC in a separate call. Both
sides are one-to-one:

* A VPC can have at most one gateway attached ("target VPC already has an
  attached gateway").
* A gateway can be attached to at most one VPC ("already attached — detach
  first").

Attaching connects the VPC to the region's external network. It does **not**
create a route — you still add `0.0.0.0/0` to each route table that should be
public, which is what lets one VPC hold public and private subnets side by side.

Detach refuses while any route still targets the gateway, and delete refuses
while it is either attached or referenced by a route. Rewrite the routes first.

<Note>
  In the console the one-per-VPC rule shows up as a disabled control rather
  than an error: **Attach** on the gateway's page is greyed out when there is
  nothing left to attach to, and the dialog says "Every VPC already has an
  internet gateway attached." The **Detach** button and the **Delete internet
  gateway** action sit on the same page, and refuse for the reasons above.
</Note>

## NAT gateways

A NAT gateway gives private IPv4 subnets outbound access without making them
reachable from outside.

<Tabs>
  <Tab title="Console">
    Go to **Networking → NAT Gateways** and choose **Create NAT Gateway**.
    Give it a **Name** under **NAT gateway details**, then pick the **VPC**
    and **Subnet** under **Placement**. The summary shows **Public IP** as
    **Auto-allocated** — you do not choose the address.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/nat-gateways
    { "name": "main", "subnet_id": "<subnet>" }
    ```
  </Tab>
</Tabs>

There is **one per VPC** — a second is refused with "target VPC already has a
NAT gateway" — and the VPC needs an internet gateway attached first, because
the NAT gateway reuses that uplink. Without one the create fails with "target
VPC has no internet gateway attached — attach one first".

<ResponseField name="external_ip" type="string, stable">
  Allocated from the regional public pool and fixed for the gateway's lifetime.
  This is the address your outbound traffic appears from, so it is the one you
  give a third party to allowlist.
</ResponseField>

<Warning>
  **Creating a NAT gateway translates nothing.** Which subnets it serves follows
  from the route tables that point at it. Add
  `0.0.0.0/0 → target_nat_gateway_id` to the private subnets' route table, and
  they join the moment the route exists. In the console that is **Add Route**
  on that table with **Target type** set to **NAT Gateway**.
</Warning>

Its address draws on the same public IPv4 quota as a floating IP — the
allowance is one number, not one per feature. `name`, `subnet_id` and
`external_ip` are immutable; the subnet it lives in cannot be deleted while it
is there; and the gateway cannot be deleted while a route references it.

## Egress-only gateways: the IPv6 case

IPv6 has no address translation, so an instance in a dual-stack subnet reaches
the internet as its own global address. "Outbound only" cannot be built from a
NAT gateway the way it can for IPv4 — an egress-only gateway is how you express
it.

```bash theme={null}
POST /v1/egress-only-gateways
{ "name": "main", "vpc_id": "<vpc>" }
```

<Note>
  Egress-only gateways are **API only**. The console has no page for them and
  **Add Route** cannot target one, so both the gateway and the `::/0` route
  that activates it are calls you make yourself.
</Note>

One per VPC, and the VPC must have an IPv6 CIDR ("target VPC has no IPv6 CIDR —
an egress-only gateway only routes v6").

It owns no address and does nothing on its own. It becomes effective when a
route table points `::/0` at it. Subnets on that table then get outbound IPv6
and the replies to their own connections, while connections the internet
initiates are dropped.

The internet gateway requirement bites at that route, not at the create: an
egress-only gateway uses the VPC's internet gateway for its uplink, so the
`::/0` route is refused with "egress-only gateway requires an internet gateway
attached to its VPC" if there is none.

<Warning>
  That inbound drop is enforced by the platform and is **not** something your
  security groups can open. If you want inbound IPv6 governed by your own rules,
  route `::/0` at the internet gateway instead.
</Warning>

Delete refuses while a route still references it.

### IPv6 reachability is the route, not the address

| `::/0` route on the subnet's table | What the subnet's IPv6 does                                     |
| ---------------------------------- | --------------------------------------------------------------- |
| None                               | Private. Unreachable in both directions.                        |
| → internet gateway                 | Public both ways. Your security groups govern inbound.          |
| → egress-only gateway              | Outbound and its replies only. Inbound refused by the platform. |

<Note>
  Adding, changing or removing that route flips the posture of **every instance
  already running** on that route table, not only ones created afterwards.
</Note>

## Floating IPs

A floating IP is a public IPv4 address from the region's pool. The create takes
only `description` and `tags` — the address is assigned to you:

<Tabs>
  <Tab title="Console">
    Go to **Networking → Floating IPs** and choose **Allocate Floating IP**.
    There is nothing to configure but a **Description** and **Tags** —
    **Address** reads **Auto-assigned from pool**. Confirm with **Allocate
    IP**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/floating-ips
    { "description": "web front door" }
    ```
  </Tab>
</Tabs>

It counts against the same public IPv4 quota as a NAT gateway address.

<AccordionGroup>
  <Accordion title="Attach" icon="link">
    <Tabs>
      <Tab title="Console">
        Open the address and choose **Attach** — or **Attach interface** on
        its **Interfaces** tab — then pick the NIC under **Interface** and
        confirm with **Attach floating IP**. The same action is on the NIC
        itself, as **Attach floating IP** under **Networking → Interfaces**.
      </Tab>

      <Tab title="API">
        `POST /v1/floating-ips/{floating_ip_id}/attach` with `interface_id`.
      </Tab>
    </Tabs>

    Re-attaching to a NIC that already holds it is a no-op success.

    It is refused when:

    * the interface's subnet has no `0.0.0.0/0` route to an internet gateway
      (`400`);
    * the interface already carries a **different** floating IP (`409`,
      "interface is already attached to floating IP *address*") — a NIC has one
      public identity;
    * the address already has a member, so a second one would make it an
      anycast address (`409`, see below);
    * the address is bound to a resource that owns its own bindings, such as a
      load balancer or an instance pool — attach and detach it there. The
      console does not offer **Attach** on such an address at all: its
      **Attached interface** reads **Bound by another service**, or **No
      replica is serving it yet** for a pool.
  </Accordion>

  <Accordion title="Detach" icon="unlink">
    <Tabs>
      <Tab title="Console">
        **Detach** on the address's page, **Remove interface** on its
        **Interfaces** tab, or **Detach floating IP** from the NIC's page —
        all three do the same thing. **Remove interface** is blunt about what
        that means: "The address stops reaching any instance."
      </Tab>

      <Tab title="API">
        `POST /v1/floating-ips/{floating_ip_id}/detach`. The body is optional:
        an ordinary floating IP has at most one member, so naming
        `interface_id` or omitting it detaches the same address.
      </Tab>
    </Tabs>

    It is idempotent. Detaching an already-detached address, or naming a NIC
    that is not a member, returns `200` with the row unchanged.
  </Accordion>

  <Accordion title="Release" icon="trash-2">
    <Tabs>
      <Tab title="Console">
        **Release floating IP**, on the address's **Settings** tab.
      </Tab>

      <Tab title="API">
        `DELETE /v1/floating-ips/{floating_ip_id}`
      </Tab>
    </Tabs>

    Either way the address goes back to the pool. Refused with `409` while it
    is still attached, or while it belongs to an instance pool. Detach first.
  </Accordion>
</AccordionGroup>

### Reading the bindings

`members` is authoritative. `attached_to_interface_id` is a legacy
single-binding field and is null both when the address is unattached *and* when
it has more than one member, so a client that reads only that field cannot tell
those two states apart.

<Note>
  A member's `health` reads `unknown` for instance NICs. It is not a health
  check and nothing sets it to anything else on this path — do not build alerting
  on it.
</Note>

### One address in front of several instances

An address with more than one member is an anycast address, and only an
[instance pool](/compute) can own one. Attaching a second interface by hand is
refused with `409`.

The reason is worth understanding before you try to work around it. The members
have to land on different hosts. Two members on the same host means one of them
answers and the other receives nothing at all — and nothing reports it: the
`members` array lists two, the console shows two, and half your capacity is
silently dark. Nothing on the floating-IP path places instances, so nothing
there can promise the spread. A pool does: it spreads its replicas and keeps the
address's members in step as it scales.

<Tabs>
  <Tab title="Console">
    Open the pool under **Compute → Instance Pools**, go to its **Floating
    IPs** tab and choose **Attach floating IP**. Detaching is the row's
    **Detach floating IP** action on that same tab.

    The address's own page will not let you do this — an address a pool holds
    offers no **Attach** button at all, and its **Attached interface** reads
    **No replica is serving it yet** until a replica picks it up.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST   /v1/instance-pools/{pool_id}/floating-ips
    DELETE /v1/instance-pools/{pool_id}/floating-ips/{floating_ip_id}
    ```
  </Tab>
</Tabs>

With several members the region's edge picks one per connection by hashing the
connection's addresses and ports, and every packet of that connection goes to
the same one. That spreads connections and survives the loss of a host.

<Warning>
  This is **not** a load balancer. Nothing health-checks what runs inside the
  instance, and connections in flight to a member that goes away end rather than
  move. A pool can also carry more replicas than the address has members — a
  replica sharing a host with an existing member is left out of the set instead
  of being added and starved.
</Warning>

## Tearing it down in the right order

Each of these refusals exists because the step before it was skipped. Working
inwards:

<Steps>
  <Step title="Detach floating IPs from their interfaces">
    Release refuses while attached, and the default-route delete refuses while
    any floating IP in the table's subnets is live.
  </Step>

  <Step title="Delete the routes that target a gateway">
    Every gateway type refuses detach and delete while a route still references
    it.
  </Step>

  <Step title="Delete NAT and egress-only gateways">
    A NAT gateway also blocks the delete of the subnet it lives in.
  </Step>

  <Step title="Detach the internet gateway, then delete it">
    An attached gateway also blocks the VPC delete.
  </Step>

  <Step title="Interfaces, then subnets, then the VPC">
    Detach each interface from its instance first — see
    [interfaces](/networking#interfaces).
  </Step>
</Steps>
