Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/vpc-networks-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"wrangler": minor
"miniflare": minor
"@cloudflare/workers-utils": minor
---

Add `vpc_networks` binding type and `wrangler vpc network` CLI commands

Workers can now bind to an entire Cloudflare Tunnel or an explicitly created VPC network using the new `vpc_networks` configuration field. At runtime, `env.MY_VPC.fetch("http://any-internal-host/")` routes requests through the tunnel without requiring per-target registration.

Example `wrangler.json` configuration:

```jsonc
{
"vpc_networks": [
// Simple case: bind directly to a tunnel
{ "binding": "MY_VPC", "tunnel_id": "your-tunnel-uuid" },
// Custom DNS: bind to an explicitly created network
{ "binding": "MY_DNS_VPC", "network_id": "your-network-uuid" },
],
}
```

New CLI commands for managing VPC networks:

- `wrangler vpc network create <name>` — create a network with `--tunnel-id` and optional `--resolver-ips`
- `wrangler vpc network list` — list all VPC networks
- `wrangler vpc network get <network-id>` — get network details
- `wrangler vpc network update <network-id>` — update a network
- `wrangler vpc network delete <network-id>` — delete a network

Each binding generates a `Fetcher` type for TypeScript type generation. Like `vpc_services`, VPC network bindings are always remote in `wrangler dev`.
4 changes: 4 additions & 0 deletions packages/miniflare/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
VERSION_METADATA_PLUGIN,
VERSION_METADATA_PLUGIN_NAME,
} from "./version-metadata";
import { VPC_NETWORKS_PLUGIN, VPC_NETWORKS_PLUGIN_NAME } from "./vpc-networks";
import { VPC_SERVICES_PLUGIN, VPC_SERVICES_PLUGIN_NAME } from "./vpc-services";
import {
WORKER_LOADER_PLUGIN,
Expand Down Expand Up @@ -64,6 +65,7 @@ export const PLUGINS = {
[DISPATCH_NAMESPACE_PLUGIN_NAME]: DISPATCH_NAMESPACE_PLUGIN,
[IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN,
[VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN,
[VPC_NETWORKS_PLUGIN_NAME]: VPC_NETWORKS_PLUGIN,
[VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN,
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
[HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN,
Expand Down Expand Up @@ -128,6 +130,7 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
z.input<typeof DISPATCH_NAMESPACE_PLUGIN.options> &
z.input<typeof IMAGES_PLUGIN.options> &
z.input<typeof VECTORIZE_PLUGIN.options> &
z.input<typeof VPC_NETWORKS_PLUGIN.options> &
z.input<typeof VPC_SERVICES_PLUGIN.options> &
z.input<typeof MTLS_PLUGIN.options> &
z.input<typeof HELLO_WORLD_PLUGIN.options> &
Expand Down Expand Up @@ -207,6 +210,7 @@ export * from "./browser-rendering";
export * from "./dispatch-namespace";
export * from "./images";
export * from "./vectorize";
export * from "./vpc-networks";
export * from "./vpc-services";
export * from "./mtls";
export * from "./hello-world";
Expand Down
75 changes: 75 additions & 0 deletions packages/miniflare/src/plugins/vpc-networks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from "zod";
import {
getUserBindingServiceName,
Plugin,
ProxyNodeBinding,
remoteProxyClientWorker,
RemoteProxyConnectionString,
} from "../shared";

const VpcNetworksSchema = z.object({
target_id: z.string(),
remoteProxyConnectionString: z
.custom<RemoteProxyConnectionString>()
.optional(),
});

export const VpcNetworksOptionsSchema = z.object({
vpcNetworks: z.record(VpcNetworksSchema).optional(),
});

export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks";

export const VPC_NETWORKS_PLUGIN: Plugin<typeof VpcNetworksOptionsSchema> = {
options: VpcNetworksOptionsSchema,
async getBindings(options) {
if (!options.vpcNetworks) {
return [];
}

return Object.entries(options.vpcNetworks).map(
([name, { target_id, remoteProxyConnectionString }]) => {
return {
name,

service: {
name: getUserBindingServiceName(
VPC_NETWORKS_PLUGIN_NAME,
target_id,
remoteProxyConnectionString
),
},
};
}
);
},
getNodeBindings(options: z.infer<typeof VpcNetworksOptionsSchema>) {
if (!options.vpcNetworks) {
return {};
}
return Object.fromEntries(
Object.keys(options.vpcNetworks).map((name) => [
name,
new ProxyNodeBinding(),
])
);
},
async getServices({ options }) {
if (!options.vpcNetworks) {
return [];
}

return Object.entries(options.vpcNetworks).map(
([name, { target_id, remoteProxyConnectionString }]) => {
return {
name: getUserBindingServiceName(
VPC_NETWORKS_PLUGIN_NAME,
target_id,
remoteProxyConnectionString
),
worker: remoteProxyClientWorker(remoteProxyConnectionString, name),
};
}
);
},
};
1 change: 1 addition & 0 deletions packages/workers-utils/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,5 @@ export const defaultWranglerConfig: Config = {
streaming_tail_consumers: undefined,
pipelines: [],
vpc_services: [],
vpc_networks: [],
};
20 changes: 20 additions & 0 deletions packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,26 @@ export interface EnvironmentNonInheritable {
/** Whether the VPC service is remote or not */
remote?: boolean;
}[];

/**
* Specifies VPC networks that are bound to this Worker environment.
*
* NOTE: This field is not automatically inherited from the top level environment,
* and so must be specified in every named environment.
*
* @default []
* @nonInheritable
*/
vpc_networks: {
/** The binding name used to refer to the VPC network in the Worker. */
binding: string;
/** The tunnel ID of the Cloudflare Tunnel to route traffic through. Mutually exclusive with network_id. */
tunnel_id?: string;
/** The network ID of an explicitly created VPC network. Mutually exclusive with tunnel_id. */
network_id?: string;
/** Whether the VPC network is remote or not */
remote?: boolean;
}[];
}

/**
Expand Down
75 changes: 74 additions & 1 deletion packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export type ConfigBindingFieldName =
| "assets"
| "unsafe_hello_world"
| "worker_loaders"
| "vpc_services";
| "vpc_services"
| "vpc_networks";

/**
* @deprecated new code should use getBindingTypeFriendlyName() instead
Expand Down Expand Up @@ -139,6 +140,7 @@ export const friendlyBindingNames: Record<ConfigBindingFieldName, string> = {
unsafe_hello_world: "Hello World",
worker_loaders: "Worker Loader",
vpc_services: "VPC Service",
vpc_networks: "VPC Network",
} as const;

/**
Expand Down Expand Up @@ -178,6 +180,7 @@ const bindingTypeFriendlyNames: Record<Binding["type"], string> = {
ratelimit: "Rate Limit",
worker_loader: "Worker Loader",
vpc_service: "VPC Service",
vpc_network: "VPC Network",
media: "Media",
assets: "Assets",
inherit: "Inherited",
Expand Down Expand Up @@ -1872,6 +1875,16 @@ function normalizeAndValidateEnvironment(
validateBindingArray(envName, validateVpcServiceBinding),
[]
),
vpc_networks: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"vpc_networks",
validateBindingArray(envName, validateVpcNetworkBinding),
[]
),
version_metadata: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -2917,6 +2930,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
"pipeline",
"worker_loader",
"vpc_service",
"vpc_network",
"media",
];

Expand Down Expand Up @@ -3980,6 +3994,65 @@ const validateVpcServiceBinding: ValidatorFn = (diagnostics, field, value) => {
return isValid;
};

const validateVpcNetworkBinding: ValidatorFn = (diagnostics, field, value) => {
if (typeof value !== "object" || value === null) {
diagnostics.errors.push(
`"vpc_networks" bindings should be objects, but got ${JSON.stringify(
value
)}`
);
return false;
}
let isValid = true;
// VPC network bindings must have a binding and exactly one of tunnel_id or network_id.
if (!isRequiredProperty(value, "binding", "string")) {
diagnostics.errors.push(
`"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
}
const hasTunnelId = hasProperty(value, "tunnel_id");
const hasNetworkId = hasProperty(value, "network_id");
if (hasTunnelId && hasNetworkId) {
diagnostics.errors.push(
`"${field}" bindings must have either "tunnel_id" or "network_id", but not both.`
);
isValid = false;
} else if (!hasTunnelId && !hasNetworkId) {
diagnostics.errors.push(
`"${field}" bindings must have either a "tunnel_id" or "network_id" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
} else if (hasTunnelId && typeof value.tunnel_id !== "string") {
diagnostics.errors.push(
`"${field}" bindings must have a string "tunnel_id" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
} else if (hasNetworkId && typeof value.network_id !== "string") {
diagnostics.errors.push(
`"${field}" bindings must have a string "network_id" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
}

validateAdditionalProperties(diagnostics, field, Object.keys(value), [
"binding",
"tunnel_id",
"network_id",
"remote",
]);

return isValid;
};

/**
* Check that bindings whose names might conflict, don't.
*
Expand Down
13 changes: 13 additions & 0 deletions packages/workers-utils/src/map-worker-metadata-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ export function mapWorkerMetadataBindings(
];
}
break;
case "vpc_network":
{
configObj.vpc_networks = [
...(configObj.vpc_networks ?? []),
{
binding: binding.name,
...(binding.tunnel_id
? { tunnel_id: binding.tunnel_id }
: { network_id: binding.network_id }),
},
];
}
break;
default: {
configObj.unsafe = {
bindings: [...(configObj.unsafe?.bindings ?? []), binding],
Expand Down
8 changes: 8 additions & 0 deletions packages/workers-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
CfUnsafeBinding,
CfUserLimits,
CfVectorize,
CfVpcNetwork,
CfVpcService,
CfWorkerLoader,
CfWorkflow,
Expand Down Expand Up @@ -159,6 +160,12 @@ export type WorkerMetadataBinding =
simple: { limit: number; period: 10 | 60 };
}
| { type: "vpc_service"; name: string; service_id: string }
| {
type: "vpc_network";
name: string;
tunnel_id?: string;
network_id?: string;
}
| {
type: "worker_loader";
name: string;
Expand Down Expand Up @@ -319,6 +326,7 @@ export type Binding =
| ({ type: "ratelimit" } & NameOmit<CfRateLimit>)
| ({ type: "worker_loader" } & BindingOmit<CfWorkerLoader>)
| ({ type: "vpc_service" } & BindingOmit<CfVpcService>)
| ({ type: "vpc_network" } & BindingOmit<CfVpcNetwork>)
| ({ type: "media" } & BindingOmit<CfMediaBinding>)
| ({ type: `unsafe_${string}` } & Omit<CfUnsafeBinding, "name" | "type">)
| { type: "assets" }
Expand Down
7 changes: 7 additions & 0 deletions packages/workers-utils/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@ export interface CfVpcService {
remote?: boolean;
}

export interface CfVpcNetwork {
binding: string;
tunnel_id?: string;
network_id?: string;
remote?: boolean;
}

export interface CfAnalyticsEngineDataset {
binding: string;
dataset?: string;
Expand Down
Loading
Loading