Freestyle Docs

Freestyle / Docs

VPCs

Create private networks and attach Freestyle VMs to them.

VPCs let Freestyle VMs communicate over private addresses. Use them for databases, internal services, worker pools, or any group of VMs that should not depend on public domains.

Freestyle VPCs are dual-stack. Every VPC gets an IPv6 /64 and an IPv4 /24, and members take an address from each — so a service that binds 0.0.0.0, or a container on a v4-only bridge, is reachable from the rest of the network without anyone configuring anything.

Create A VPC

import { Freestyle } from "freestyle";

const freestyle = new Freestyle();
const { vpc, vpcId, data } = await freestyle.vpc.create({
  slug: "workspace-network",
  cidr: "10.40.0.0/24",
  cidrV6: "fd00:40::/64",
  // Membership is not permission: without this rule, members of the network
  // do not reach each other at all. See the firewall page.
  firewall: { rules: [{ action: "allow", source: {}, destination: {} }] },
});

console.log(vpcId, data.cidr, data.cidrV6);

Both CIDRs are optional: omit cidr and Freestyle derives a /24 out of 10.0.0.0/8, omit cidrV6 and it derives a unique-local /64. Name cidr yourself when the network has to reach an existing estate over a tunnel and must not overlap it, or when it needs more than 254 IPv4 members.

The firewall block declares rules with the network, where a bare endpoint means the network being created — “members reach each other” is the rule nearly every network wants, and nothing on the network moves without it.

Attach VMs

A VM can join at most one VPC. Declare the attachment when the VM is created — pass the VPC ID or your slug for it — or change it later with the update-networks API (PUT /v5/vms/{vmIdOrSlug}/networks), which attaches, re-addresses, or detaches declaratively whether the VM is stopped, running, or paused:

const apiNet = { ipv4: "10.40.0.10", ipv6: "fd00:40::10" };

const { vm: apiVm, vmId: apiVmId } = await freestyle.vms.create({
  // Required: a VM reaches nothing it has not been allowed to.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
  slug: "api",
  vpcs: [{ vpcId, ...apiNet }],
});

const { vm: workerVm, data: worker } = await freestyle.vms.create({
  // Required: a VM reaches nothing it has not been allowed to.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
  slug: "worker",
  vpcs: [{ vpcId, ipv4: true }],
});

console.log({ vpcId, vmId: apiVmId, ...apiNet });
console.log(worker.vpcs[0]?.ipv4, worker.vpcs[0]?.ipv6);

Both families are allocated by default, so ipv4/ipv6 only matter when you want to pin an address or opt out. Pass a string to pin one, true to allocate explicitly, or false to make the member single-family.

Preselecting addresses also makes it easier to identify which VM sent traffic to a VPC service, router, or proxy. Choose available addresses within cidr and cidrV6, save them with the returned vmId, and match the connection’s private source IP to that mapping. For automatically allocated addresses, read data.vpcs from the VM creation response instead.

Scope the mapping to the VPC and update it when an attachment changes or a VM is deleted. Addresses can be reused, so retain historical mappings alongside saved traffic logs. To reuse a preselected address for a replacement VM, request it again after the previous attachment has released it. See identifying VMs at an exit node and filtering mitmproxy flows by source IP.

Install an in-guest static route when another VPC member is acting as a router:

const { vm } = await freestyle.vms.create({
  // Required: a VM reaches nothing it has not been allowed to.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
  vpcs: [{
    vpcId,
    ipv4: "10.40.0.20",
    routes: [{ cidr: "172.20.0.0/16", via: "10.40.0.1" }],
  }],
});

Address Conflicts

Each IPv4 and IPv6 address can belong to only one attachment in a VPC. VMs, router VMs, and tunnel attachments share that address pool. Two VMs cannot share either address, even if their other address differs.

If a VM already holds 10.40.0.10, creating another VM with ipv4: "10.40.0.10" in the same VPC fails with HTTP 409 Conflict. The SDK throws FreestyleApiError with status: 409 and a message saying the address is already reserved. The first VM keeps its address. A conflicting ipv6 request fails the same way, and Freestyle does not silently choose another address for an explicit request. Concurrent requests cannot both reserve the same address.

Choose a different available address for each VM, or omit ipv4 and ipv6 to let Freestyle allocate free addresses. An attached VM holds its addresses while stopped or paused too; reuse them only after the previous attachment has released them. Private address reservations are scoped to the VPC, so separate VPCs can reuse the same address ranges.

The addresses in routes[].via identify a router or exit node. Multiple workers using the same exit should share those next-hop addresses while keeping their own ipv4 and ipv6 addresses distinct. The mitmproxy guide shows two workers configured this way.

Test Private Connectivity

Bind destination services to 0.0.0.0 for IPv4, or :: for both address families, then connect using the address in VmData.vpcs — worker was allocated its address rather than pinning one, so read it back rather than guessing it:

const workerIp = worker.vpcs[0]?.ipv4;
await workerVm.exec("nohup python3 -m http.server 8080 --bind 0.0.0.0 >/tmp/http.log 2>&1 &");
const result = await apiVm.exec(`curl http://${workerIp}:8080`);
console.log(result.stdout);

This works because the network was created with the members-reach-each-other rule above; without it, the curl would time out.

Connect From Your Computer

Create a persistent WireGuard peer with tunnels, then use the returned config whenever your computer needs access to the VPC.

Manage VPCs

const { vpcs } = await freestyle.vpc.list();
const current = await freestyle.vpc.get("workspace-network");
await vpc.update({ slug: "private-workers" });
await vpc.delete();

Delete or move every attached VM, and detach every tunnel, before deleting a VPC.

esc