Networking

VPCs and Subnets

VPCs are private networks that isolate sets of instances from each other. Instances within a VPC subnet can talk to each other using private IP addresses (if firewall rules allow it) but traffic between VPCs must go through external IPs.

Every project comes with a default VPC. More VPCs can be set up by project administrators and collaborators to isolate the network traffic between different groups of VM instances. Each VPC includes a subnet by default with a RFC1918 IP address range. More subnets can be created for allocating different IP address ranges to different types of instances (e.g., databases versus web servers) and defining firewall rules that govern network traffic based on IP ranges. By default, all subnets within a VPC are able to route to one another.

External Connectivity

Instances that have one or more network interfaces get outbound internet access through NAT service on the rack. There are three classes of address which can be allocated to an instance:

External IP TypeDescriptionAllows Inbound External TrafficDefaultMaximum user-allocated

Source NAT

A shared temporary IP and port range.

No

Yes

N/A

Ephemeral IP

A 1-to-1 temporary IP address.

Yes

No

1

Floating IP

A 1-to-1 permanent IP address.

Yes

No

32

Instances with network interfaces will always have a Source NAT (SNAT) address. These allow them to communicate with external hosts, but do not allow external hosts to initiate a session with an instance.

Ephemeral and floating IP addresses can be optionally attached to VM instances, which allow them to be accessible from hosts outside of their VPCs. These addresses can be attached at instance provisioning time and are detached when instances are terminated. Floating IPs may also be attached and detached from actively-running instances using the floating_ip_attach and floating_ip_detach endpoints. Ephemeral IPs are automatically assigned and released back into an IP pool when attached or detached, while floating IPs are explicitly created with a permanent identity in a project and allow greater control over which IP an instance should have.

An instance can have a maximum of 32 external IPs, and may have a single ephemeral IP.

Instances will receive traffic from external hosts on all of their ephemeral and floating IPs, and will transparently send reply traffic from the original external address. Outbound traffic will be mapped to an external IP by priority:

balanced(floating_ips) > ephemeral_ip

That is, an instance with two floating IPs 192.168.32.32 and 192.168.32.33 and an ephemeral IP 192.168.32.1 will randomly choose .32 or .33 as a source address for outbound traffic. It will never originate traffic on .1 unless the floating IPs are detached.

Firewall Rules

Firewall rules govern what traffic is allowed to be sent or received by an instance. The VPC firewall API allows you to define VPC-wide and per-instance policies for traffic according to its direction, protocol, and source/destination address.

Firewall Basics

Every network interface attached to an instance has a dedicated stateful firewall. By default, this firewall will:

  1. Deny all inbound traffic.

  2. Allow all outbound traffic.

Stateful refers to how the firewall behaves whenever a packet or flow is allowed. A decision to allow traffic will create a temporary exception for return-path traffic on the same network interface (i.e., with swapped source/destination ports and addresses).

Note
When no rules are installed, instances within a VPC cannot reliably communicate with one another. They can establish a working bidirectional flow with any host in the external network or wider Internet.

We alter this behavior by installing firewall rules within the VPC. Each rule comprises:

  • A set of targets. These are specifiers that control which instances' firewalls a rule will be installed on. These include:

    • all instances in a VPC,

    • all instances in a VPC subnet,

    • all instances matching a specific IP address or CIDR,

    • a specific instance.

  • A set of filters. Filters determine which traffic a rule applies to. These include:

    • protocols — TCP, UDP, and ICMP,

    • ports — individual ports and/or ranges,

    • hosts — the IP address of a remote host, using similar specifiers to targets.

  • A direction. Inbound or Outbound.

  • An action. Allow or Deny.

  • A priority value, in the range 0—​65535. 0 is the highest priority.

Each type of filter stores a list of possible values, and a packet matches a rule if it has an entry in every non-empty list of filter entries. For example, a rule which contains the filters {"ports": [80, 443], "hosts": [{"type": "ip", "value": "192.168.1.1"}, {"type": "subnet", "value": "other-subnet"}]} will match all packets satisfying (Port 80 OR Port 443) AND (IP 192.168.1.1 OR Subnet other-subnet).

When a packet arrives at an instance firewall, it will use the highest priority rule which both matches and is enabled. If several rules of identical priority match a packet, then deny will take precedence over any allow actions.

Default Rules

Whenever a VPC is created, it includes a few rules designed to minimize the configuration needed to get started:

  1. Allow inbound SSH (TCP port 22) for all instances in the VPC,

  2. Allow inbound ICMP (ping) for all instances in the VPC,

  3. Allow all inbound traffic from other instances in the VPC.

Configuration

Firewall rules may be set for a VPC using the vpc_firewall_rules_update endpoint. Here is an example of a rule that allows SSH from anywhere to all instances on the rack:

{
  "action": "allow",
  "description": "allow inbound TCP connections on port 22 from anywhere",
  "direction": "inbound",
  "filters": {
    "hosts": null,
    "ports": [
      "22"
    ],
    "protocols": [
      "TCP"
    ]
  },
  "name": "allow-ssh",
  "priority": 65534,
  "status": "enabled",
  "targets": [
    {
      "type": "vpc",
      "value": "default"
    }
  ],
  "vpc_id": "f04c8e14-e4f8-4a1b-94db-cf34e8780738"
}

VPC Subnet Routing

VPC subnet routing controls how packets are forwarded from instances to the external network, and to other instances within the same VPC. The Oxide control plane allows you to configure this by creating routers. All subnets within a VPC share a System router, and may each have an optional Custom router that allows for basic user-controlled routing.

Routes within a router match traffic outbound from an instance, and direct it towards a known target. A route is selected using a longest-prefix match on a flow’s destination IP address, i.e., the 'most specific route' wins.

To offer a worked example, this guide assumes a VPC (subnet-guide) containing three subnets with child instances:

  • kanto (10.0.0.0/24),

  • johto (10.0.1.0/24),

    • goldenrod (10.0.1.5), and ecruteak (10.0.1.6),

  • hoenn (192.168.0.0/24).

    • mossdeep (192.168.0.5)

vpc-subnet-example

Route semantics

Routes consist of two elements:

  • A destination, that controls which outbound packets will be affected by the route. Packets are matched on their destination IP address.

  • A target. This determines where a matched packet will be forwarded. This can resolve to an interface in the VPC, the external network, or an explicit drop.

Destinations and targets can refer to explicit IPs and prefixes, or to named resources like instances and subnets. If a named destination or target does not correspond to any entity in the VPC, the route will still be created. However, it will not be applied until a match can be made — i.e., an instance/subnet is created or renamed. A route will also become a no-op in the event that its named subnet or instance is deleted or renamed.

Table 1. Destinations
TypeDescriptionExample valueSystem-only

ip

Matches an individual IP address.

192.168.1.222

No

ip_net

Matches an explicit IP subnet.

192.168.0.0/16

No

subnet

Matches both the IPv4 and IPv6 prefixes of a named subnet.

johto

No

vpc

Matches all subnets of a named VPC.

subnet-guide

Yes

Table 2. Targets
TypeDescriptionExample valueSystem-only

ip

Forwards packets to a VPC-local interface with a specific IP.

10.0.1.5

No

instance

Forwards packets to the IPv4/IPv6 address of an instance’s primary NIC.

ecruteak

No

internet_gateway

Forwards packets to the external network.

outbound

No

drop

Drops all matched packets at the source.

N/A

No

subnet

Represents a route to every instance within a subnet.

johto

Yes

vpc

Represents a route to every subnet within a VPC.

subnet-guide

Yes

System-only destinations and targets cannot be set or used in user-configured routes.

Note
Packets forwarded to an explicit IP address which does not exist within the VPC are implicitly dropped.
Important
  • outbound is the only valid Internet Gateway in the current product version.

  • vpc destinations and targets are placeholder types, as routes cannot be specified across VPC boundaries in the current product version.

  • Instances do not yet support the use of IPv6.

System Routers

System routers are automatically created and managed by the control plane, and apply to all subnets within a VPC. Each VPC has a single system router, which will always be named "system". This router contains one route per subnet (which is immutable), and IPv4/v6 default outbound routes whose target can be changed.

We can inspect its routes using the vpc_router_route_list endpoint, which will contain routes like the below:

NameTypeDestinationTargetMutable

kanto

VPC Subnet

subnet:kanto

subnet:kanto

No

johto

VPC Subnet

subnet:johto

subnet:johto

No

hoenn

VPC Subnet

subnet:hoenn

subnet:hoenn

No

default-v4

Default

0.0.0.0/0

internet_gateway:outbound

Yes

default-v6

Default

::/0

internet_gateway:outbound

Yes

This means that:

  • All traffic directed to instances in any of the three subnets will have a valid route.

    • These rules match on the IPv4 and IPv6 prefixes of the named subnet.

    • Packets targeting an address in these subnets without a matching instance will be implicitly dropped.

  • Any IPv4 or IPv6 traffic which does not match one of the named subnets will be sent to the upstream network and leave the rack.

The target of the 'default-v4' and 'default-v6' routes may be modified using the vpc_router_route_update endpoint. Example 3: Controlling access to upstream networks shows how you can do so — and instead prevent a VPC from sending any outbound traffic, allow outbound traffic from a single subnet, or allow a subnet to reach only a fixed set of upstream addresses.

Custom Routers

Custom routers are collections of routes that are used to alter a subnet’s outbound routing behavior. VPC subnets have an optional custom_router field that can be used to assign a custom router. Each network interface on an instance combines its VPC-wide system router with the custom router applied to its subnet into a single routing table. A custom router may be used by many subnets.

Custom routes have higher priority than system routes when matching a destination subnet of identical prefix length.

Tip

Custom routers are not executed before the system router.

Let’s return to our worked example. Suppose we want to drop traffic from hoenn to both of the other subnets. kanto and johto can obviously be aggregated into 10.0.0.0/23.

However, if we add a custom route to hoenn specifying (10.0.0.0/23 → drop), then it will not take effect. Both existing entries in the system router are more specific, matching on /24, and are chosen instead.

The correct solution is to create one drop rule per subnet — (subnet:kanto → drop) and (subnet:johto → drop). See Example 1: Creating/assigning routers and routes.

Example 1: Creating/assigning routers and routes

How would we configure hoenn such that it cannot reach kanto or johto? Custom routers allow for coarse filtering on instances' outbound traffic, and can be used as a complementary tool to firewall rules when per-instance exceptions aren’t required.

By default, our project contains a fully populated system router:

oxide vpc router list --project oxdoc --vpc subnet-guide
oxide vpc router route list --project oxdoc --vpc subnet-guide --router system
Command output
[
  {
    "description": "Routes are automatically added to this router as vpc subnets are created",
    "id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f",
    "kind": "system",
    "name": "system",
    "time_created": "2024-07-04T13:11:12.809802Z",
    "time_modified": "2024-07-04T13:11:12.809802Z",
    "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
  }
]

[
  {
    "description": "The default route of a vpc",
    "destination": {
      "type": "ip_net",
      "value": "0.0.0.0/0"
    },
    "id": "2ff59f00-a97b-46fc-a43d-1eb4ff010c8b",
    "kind": "default",
    "name": "default-v4",
    "target": {
      "type": "internet_gateway",
      "value": "outbound"
    },
    "time_created": "2024-07-04T13:11:12.869085Z",
    "time_modified": "2024-07-04T13:11:12.869085Z",
    "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
  }, {
    "description": "The default route of a vpc",
    "destination": {
      "type": "ip_net",
      "value": "::/0"
    },
    "id": "564dfdaa-81c8-47d0-90b0-dd81fcbf31f8",
    "kind": "default",
    "name": "default-v6",
    "target": {
      "type": "internet_gateway",
      "value": "outbound"
    },
    "time_created": "2024-07-04T13:11:12.930060Z",
    "time_modified": "2024-07-04T13:11:12.930060Z",
    "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
  }, {
    "description": "VPC Subnet route for 'hoenn'",
    "destination": {
      "type": "subnet",
      "value": "hoenn"
    },
    "id": "9b616956-4a9f-41bc-8db7-6bdcc7bc5a26",
    "kind": "vpc_subnet",
    "name": "hoenn",
    "target": {
      "type": "subnet",
      "value": "hoenn"
    },
    "time_created": "2024-07-04T13:12:08.156555Z",
    "time_modified": "2024-07-04T13:12:08.156555Z",
    "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
  }, {
    "description": "VPC Subnet route for 'johto'",
    "destination": {
      "type": "subnet",
      "value": "johto"
    },
    "id": "0f72a784-4e2c-4e4a-8090-36cc56b2fa8d",
    "kind": "vpc_subnet",
    "name": "johto",
    "target": {
      "type": "subnet",
      "value": "johto"
    },
    "time_created": "2024-07-04T13:11:51.701381Z",
    "time_modified": "2024-07-04T13:11:51.701381Z",
    "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
  }, {
    "description": "VPC Subnet route for 'kanto'",
    "destination": {
      "type": "subnet",
      "value": "kanto"
    },
    "id": "c0f3d46a-2a9b-46bb-9cd5-c87c2f85d727",
    "kind": "vpc_subnet",
    "name": "kanto",
    "target": {
      "type": "subnet",
      "value": "kanto"
    },
    "time_created": "2024-07-04T13:11:40.867186Z",
    "time_modified": "2024-07-04T13:11:40.867186Z",
    "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
  }
]

We first create a new custom router:

oxide vpc router create \
    --project oxdoc \
    --vpc subnet-guide \
    --name island-routes \
    --description "It's hard to get around when there's too much water."
Command output
{
  "description": "It's hard to get around when there's too much water.",
  "id": "d39c3426-c183-45df-ac2c-cbbea0cec859",
  "kind": "custom",
  "name": "island-routes",
  "time_created": "2024-07-04T13:48:41.512717Z",
  "time_modified": "2024-07-04T13:48:41.512717Z",
  "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
}

Note that this router has "kind": "custom". We can then attach it to the hoenn subnet:

oxide vpc subnet update \
    --project oxdoc \
    --vpc subnet-guide \
    --subnet hoenn \
    --custom-router island-routes
Command output
{
  "custom_router_id": "d39c3426-c183-45df-ac2c-cbbea0cec859",
  "description": "",
  "id": "d144e4ed-c569-4655-8062-9623c686db63",
  "ipv4_block": "192.168.0.0/24",
  "ipv6_block": "fd98:db4a:93b:5ce9::/64",
  "name": "hoenn",
  "time_created": "2024-07-04T13:12:08.100819Z",
  "time_modified": "2024-07-04T13:52:01.679857Z",
  "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
}

Finally, we insert routes to override the system router:

Route definitions
ocean-1.json
{
  "name": "ocean-1",
  "description": "A large stretch of water.",
  "destination": {"type": "subnet", "value": "kanto"},
  "target": {"type": "drop"}
}
ocean-2.json
{
  "name": "ocean-2",
  "description": "An ocean too far.",
  "destination": {"type": "subnet", "value": "johto"},
  "target": {"type": "drop"}
}
oxide vpc router route create \
    --project oxdoc \
    --vpc subnet-guide \
    --router island-routes \
    --json-body ocean-1.json

oxide vpc router route create \
    --project oxdoc \
    --vpc subnet-guide \
    --router island-routes \
    --json-body ocean-2.json

We can verify by attempting to ping between two instances: from mossdeep (192.168.0.5) to goldenrod (10.0.1.5). No packets from mossdeep (hoenn) will arrive at goldenrod (johto).

ubuntu@mossdeep:~$ ping 10.0.1.5 -c 10
PING 10.0.1.5 (10.0.1.5) 56(84) bytes of data.

--- 10.0.1.5 ping statistics ---
10 packets transmitted, 0 received, 100% packet loss, time 9227ms

# ...

ubuntu@goldenrod:~$ sudo tcpdump -i enp0s8 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C

ubuntu@goldenrod:~$
Note
Packets can still be sent from goldenrod to mossdeep, as we have only installed a custom router on hoenn.

Example 2: Detaching custom routers

To detach island-routes from hoenn, we update the subnet via vpc_subnet_update, leaving the --custom-router field unset:

oxide vpc subnet update \
    --project oxdoc \
    --vpc subnet-guide \
    --subnet hoenn
Command output
{
  "description": "",
  "id": "d144e4ed-c569-4655-8062-9623c686db63",
  "ipv4_block": "192.168.0.0/24",
  "ipv6_block": "fd98:db4a:93b:5ce9::/64",
  "name": "hoenn",
  "time_created": "2024-07-04T13:12:08.100819Z",
  "time_modified": "2024-07-04T15:13:47.390091Z",
  "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
}

Example 3: Controlling access to upstream networks

In a system router, we are able to modify the target of the 'default-v4' and 'default-v6' routes. We can use this to prevent outbound access to the wider Internet:

isolate.json
{
  "destination": {"type": "ip_net", "value": "0.0.0.0/0"},
  "target": {"type": "drop"}
}
oxide vpc router route update \
  --project oxdoc \
  --vpc subnet-guide \
  --router system \
  --route default-v4 \
  --json-body isolate.json
Command output
{
  "description": "The default route of a vpc",
  "destination": {
    "type": "ip_net",
    "value": "0.0.0.0/0"
  },
  "id": "2ff59f00-a97b-46fc-a43d-1eb4ff010c8b",
  "kind": "default",
  "name": "default-v4",
  "target": {
    "type": "drop"
  },
  "time_created": "2024-07-04T13:11:12.869085Z",
  "time_modified": "2024-07-04T15:43:50.670279Z",
  "vpc_router_id": "19c98bcf-4beb-434b-8ad1-32f9e6acb26f"
}

A natural consequence is that we can no longer ping or SSH into any of our instances! We can selectively re-enable access, possibly on an arbitrary upstream prefix, by using a custom router:

open.json
{
  "name": "hm02",
  "description": "Fly!",
  "destination": {"type": "ip_net", "value": "0.0.0.0/0"},
  "target": {"type": "internet_gateway", "value": "outbound"}
}
oxide vpc router create \
    --project oxdoc \
    --vpc subnet-guide \
    --name outbound-access \
    --description "Leaving altogether!"

oxide vpc router route create \
    --project oxdoc \
    --vpc subnet-guide \
    --router outbound-access \
    --json-body open.json

oxide vpc subnet update \
    --project oxdoc \
    --vpc subnet-guide \
    --subnet johto \
    --custom-router outbound-access

We can now SSH into goldenrod, but not mossdeep.

It’s worth noting that 0.0.0.0/0 and ::/0 are not the only valid choices of destination for a custom internet_gateway route — in custom routers, this target is compatible with all non-system destinations. This can be used to limit outbound access to a specific upstream subnet, or to specific IP addresses.

Tip
Custom routers can also be used to prevent individual subnets from having outbound Internet access, by mapping 0.0.0.0/0 or ::/0 to the drop target.

To restore the default system route’s state:

restore.json
{
  "destination": {"type": "ip_net", "value": "0.0.0.0/0"},
  "target": {"type": "internet_gateway", "value": "outbound"}
}
oxide vpc router route update \
  --project oxdoc \
  --vpc subnet-guide \
  --router system \
  --route default-v4 \
  --json-body restore.json

Example 4: Software routing & tunnels

Custom routes allow you to redirect entire subnets towards individual instances or internal IP addresses. This allows you to use an instance as a software router — either for local traffic processing, or to use it as a VPN tunnel endpoint (TEP).

Suppose we want goldenrod to be responsible for all traffic between johto and 172.30.0.0/22. We can add a custom route to serve this function:

magnet-train.json
{
  "name": "magnet-train",
  "description": "No more ships for me. Next time, I'm taking the MAGNET TRAIN.",
  "destination": {"type": "ip_net", "value": "172.30.0.0/22"},
  "target": {"type": "instance", "value": "goldenrod"}
}
oxide vpc router route create \
    --project oxdoc \
    --vpc subnet-guide \
    --router outbound-access \
    --json-body magnet-train.json

However, goldenrod will not yet receive any traffic bound for this subnet. OPTE, which handles per-guest packet processing, will drop any traffic on an interface which does not match its assigned IP address. This prevents instances from sending or receiving unwanted traffic. To allow an instance to opt-in on a wider address range, we must set a list of transit IPs on the relevant NICs.

Important
Network interfaces can only be updated on instances which are Stopped.

First, we need to fetch the name or ID of the primary NIC assigned to the instance.

oxide instance nic list --project oxdoc --instance goldenrod
Command output
[
  {
    "description": "",
    "id": "7fb1d888-246a-47eb-b428-303cdcc1cfa2",
    "instance_id": "29e9c92d-24b1-4513-897f-e18180aec568",
    "ip": "10.0.1.5",
    "mac": "A8:40:25:FE:64:16",
    "name": "nic",
    "primary": true,
    "subnet_id": "f51f4097-f4eb-48f9-a595-a01b5f969f5e",
    "time_created": "2024-07-04T14:32:47.243937Z",
    "time_modified": "2024-07-04T14:32:47.243937Z",
    "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
  }
]

We can now update the network interface as needed:

add-transit.json
{
  "transit_ips": ["172.30.0.0/22"]
}
oxide instance nic update \
    --interface 7fb1d888-246a-47eb-b428-303cdcc1cfa2 \
    --json-body add-transit.json
Command output
{
  "description": "",
  "id": "7fb1d888-246a-47eb-b428-303cdcc1cfa2",
  "instance_id": "29e9c92d-24b1-4513-897f-e18180aec568",
  "ip": "10.0.1.5",
  "mac": "A8:40:25:FE:64:16",
  "name": "nic",
  "primary": true,
  "subnet_id": "f51f4097-f4eb-48f9-a595-a01b5f969f5e",
  "time_created": "2024-07-04T14:32:47.243937Z",
  "time_modified": "2024-07-05T11:09:05.972275Z",
  "transit_ips": [
    "172.30.0.0/22"
  ],
  "vpc_id": "00345e31-8b2f-4c45-8adf-3055fba77efa"
}

If we attempt to contact an address in this range from ecruteak, then we can see that goldenrod will now receive all matching packets.

ubuntu@ecruteak:~$ ping 172.30.0.6
PING 172.30.0.6 (172.30.0.6) 56(84) bytes of data.
^C
--- 172.30.0.6 ping statistics ---
5 packets transmitted, 0 received, 100% packet loss, time 4078ms


# ...

ubuntu@goldenrod:~$ sudo tcpdump -i enp0s8 'dst 172.30.0.6'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), snapshot length 262144 bytes
11:42:37.207059 IP 10.0.1.6 > 172.30.0.6: ICMP echo request, id 1350, seq 1, length 64
11:42:38.233837 IP 10.0.1.6 > 172.30.0.6: ICMP echo request, id 1350, seq 2, length 64
11:42:39.257789 IP 10.0.1.6 > 172.30.0.6: ICMP echo request, id 1350, seq 3, length 64
11:42:40.281756 IP 10.0.1.6 > 172.30.0.6: ICMP echo request, id 1350, seq 4, length 64
11:42:41.305707 IP 10.0.1.6 > 172.30.0.6: ICMP echo request, id 1350, seq 5, length 64
^C
5 packets captured
5 packets received by filter
0 packets dropped by kernel
Tip
A consequence of this setup is that packets sent from goldenrod to 172.30.0.0/22 will be redirected back to itself. This is fine if goldenrod, as a TEP, is forwarding encapsulated packets towards another TEP on an IP outside of this range. If this is not desired behavior, you can specify an instance within another subnet as a target.
Last updated