Skip to content

Commit 56617d1

Browse files
committed
gw: add restrict_mode port whitelist (fail-close)
Apps can now declare `restrict_mode: true` in app-compose to make the gateway reject connections to any port not listed under `ports`. Combined with this PR's existing PROXY protocol support, this gives apps explicit control over which ports the gateway will forward. Implementation: - Collapse the previous `port_attrs: Option<BTreeMap>` and `port_attrs_hash: String` fields into a single `Option<PortPolicy>` carrying `{ ports, restrict_mode }`. One Option distinguishes "not reported" from "reported empty" cleanly. - RPC: `RegisterCvmRequest.port_attrs` (PortAttrsList) becomes `port_policy` (PortPolicy { ports, restrict_mode }). - Enforcement: `filter_allowed_addresses` runs before `connect_multiple_hosts` in both TLS-terminate and TLS-passthrough paths. A denied connection bubbles up as a normal error and the proxy closes the inbound TCP stream — no special HTTP response. - Failure mode: fail-close. An unknown policy (cache miss) denies the connection and triggers a background fetch, so subsequent connections proceed once the policy is known. Apps that opt into restrict_mode must run a CVM that reports policy at registration time; legacy CVMs that fall back to the lazy `Info()` fetch get the open default (`restrict_mode = false`) so they keep working. - Unknown instance_ids (e.g. the `localhost` shortcut) bypass the check — the policy machinery only applies to registered CVMs. - Rename the lazy-fetch config block from `port_attrs_fetch` to `port_policy_fetch` for consistency (only ever shipped on this unmerged branch). Tests: - 4 new unit tests covering allow/deny, disabled mode, fail-close on unknown policy, and the unknown-instance bypass. - Snapshot tests updated for the renamed fields.
1 parent 94d9c71 commit 56617d1

18 files changed

Lines changed: 334 additions & 143 deletions

dstack-types/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ pub struct AppCompose {
4848
/// Per-port attributes consumed by the gateway (e.g. PROXY protocol).
4949
#[serde(default)]
5050
pub ports: Vec<PortAttrs>,
51+
/// When true, the gateway only forwards traffic to ports listed in `ports`.
52+
/// All other ports are rejected at TCP-accept time.
53+
#[serde(default)]
54+
pub restrict_mode: bool,
5155
}
5256

5357
#[derive(Deserialize, Serialize, Debug, Clone)]

dstack-util/src/system_setup.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ use crate::{
5353
use cert_client::CertRequestClient;
5454
use cmd_lib::run_fun as cmd;
5555
use dstack_gateway_rpc::{
56-
gateway_client::GatewayClient, PortAttrs as RpcPortAttrs, PortAttrsList, RegisterCvmRequest,
57-
RegisterCvmResponse, WireGuardPeer,
56+
gateway_client::GatewayClient, PortAttrs as RpcPortAttrs, PortPolicy as RpcPortPolicy,
57+
RegisterCvmRequest, RegisterCvmResponse, WireGuardPeer,
5858
};
5959
use ra_tls::rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256};
6060
use serde_human_bytes as hex_bytes;
@@ -447,8 +447,8 @@ impl<'a> GatewayContext<'a> {
447447
gateway_url: &str,
448448
key_store: &GatewayKeyStore,
449449
) -> Result<RegisterCvmResponse> {
450-
let port_attrs = PortAttrsList {
451-
attrs: self
450+
let port_policy = RpcPortPolicy {
451+
ports: self
452452
.shared
453453
.app_compose
454454
.ports
@@ -458,13 +458,14 @@ impl<'a> GatewayContext<'a> {
458458
pp: p.pp,
459459
})
460460
.collect(),
461+
restrict_mode: self.shared.app_compose.restrict_mode,
461462
};
462463
let client =
463464
self.create_gateway_client(gateway_url, &key_store.client_key, &key_store.client_cert)?;
464465
let result = client
465466
.register_cvm(RegisterCvmRequest {
466467
client_public_key: key_store.wg_pk.clone(),
467-
port_attrs: Some(port_attrs.clone()),
468+
port_policy: Some(port_policy.clone()),
468469
})
469470
.await
470471
.context("Failed to register CVM");
@@ -485,7 +486,7 @@ impl<'a> GatewayContext<'a> {
485486
client
486487
.register_cvm(RegisterCvmRequest {
487488
client_public_key: key_store.wg_pk.clone(),
488-
port_attrs: Some(port_attrs),
489+
port_policy: Some(port_policy),
489490
})
490491
.await
491492
.context("Failed to register CVM")

gateway/dstack-app/builder/entrypoint.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ shutdown = "${TIMEOUT_SHUTDOWN:-5s}"
125125
total = "${TIMEOUT_TOTAL:-5h}"
126126
pp_header = "${TIMEOUT_PP_HEADER:-5s}"
127127
128-
[core.proxy.port_attrs_fetch]
129-
timeout = "${PORT_ATTRS_FETCH_TIMEOUT:-10s}"
130-
max_retries = ${PORT_ATTRS_FETCH_MAX_RETRIES:-5}
131-
backoff_initial = "${PORT_ATTRS_FETCH_BACKOFF_INITIAL:-1s}"
132-
backoff_max = "${PORT_ATTRS_FETCH_BACKOFF_MAX:-30s}"
128+
[core.proxy.port_policy_fetch]
129+
timeout = "${PORT_POLICY_FETCH_TIMEOUT:-10s}"
130+
max_retries = ${PORT_POLICY_FETCH_MAX_RETRIES:-5}
131+
backoff_initial = "${PORT_POLICY_FETCH_BACKOFF_INITIAL:-1s}"
132+
backoff_max = "${PORT_POLICY_FETCH_BACKOFF_MAX:-30s}"
133133
134134
[core.recycle]
135135
enabled = true

gateway/dstack-app/docker-compose.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ services:
4343
- ADMIN_LISTEN_PORT=${ADMIN_LISTEN_PORT:-8001}
4444
- INBOUND_PP_ENABLED=${INBOUND_PP_ENABLED:-false}
4545
- TIMEOUT_PP_HEADER=${TIMEOUT_PP_HEADER:-5s}
46-
- PORT_ATTRS_FETCH_TIMEOUT=${PORT_ATTRS_FETCH_TIMEOUT:-10s}
47-
- PORT_ATTRS_FETCH_MAX_RETRIES=${PORT_ATTRS_FETCH_MAX_RETRIES:-5}
48-
- PORT_ATTRS_FETCH_BACKOFF_INITIAL=${PORT_ATTRS_FETCH_BACKOFF_INITIAL:-1s}
49-
- PORT_ATTRS_FETCH_BACKOFF_MAX=${PORT_ATTRS_FETCH_BACKOFF_MAX:-30s}
46+
- PORT_POLICY_FETCH_TIMEOUT=${PORT_POLICY_FETCH_TIMEOUT:-10s}
47+
- PORT_POLICY_FETCH_MAX_RETRIES=${PORT_POLICY_FETCH_MAX_RETRIES:-5}
48+
- PORT_POLICY_FETCH_BACKOFF_INITIAL=${PORT_POLICY_FETCH_BACKOFF_INITIAL:-1s}
49+
- PORT_POLICY_FETCH_BACKOFF_MAX=${PORT_POLICY_FETCH_BACKOFF_MAX:-30s}
5050
restart: always
5151

5252
volumes:

gateway/gateway.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ max_connections_per_app = 2000
6161
# Whether to read PROXY protocol from inbound connections (e.g. from Cloudflare).
6262
inbound_pp_enabled = false
6363

64-
[core.proxy.port_attrs_fetch]
65-
# Background lazy-fetch of port_attrs from legacy CVM agents.
64+
[core.proxy.port_policy_fetch]
65+
# Background lazy-fetch of port_policy from legacy CVM agents.
6666
# Single Info() RPC timeout.
6767
timeout = "10s"
6868
# Retries cover the WireGuard / agent warmup window after registration.

gateway/rpc/proto/gateway_rpc.proto

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,22 @@ package gateway;
1212
message RegisterCvmRequest {
1313
// The public key of the WireGuard interface of the CVM.
1414
string client_public_key = 1;
15-
// Per-port attributes the gateway should apply when proxying to this CVM.
15+
// Per-port policy the gateway should apply when proxying to this CVM.
1616
// Wrapped in a message so we can distinguish "not reported" (old CVM →
1717
// gateway falls back to fetching app-compose via Info()) from "reported
1818
// empty" (new CVM with no special port behaviour).
19-
optional PortAttrsList port_attrs = 2;
20-
}
21-
22-
// PortAttrsList wraps a list of PortAttrs so it can be optional on the wire.
23-
message PortAttrsList {
24-
repeated PortAttrs attrs = 1;
19+
optional PortPolicy port_policy = 2;
20+
}
21+
22+
// PortPolicy carries the gateway-relevant per-port configuration declared by
23+
// the app in its compose file. Keeping `ports` and `restrict_mode` together
24+
// lets a single Option distinguish "not reported" from "reported".
25+
message PortPolicy {
26+
// Per-port attributes (PROXY protocol opt-in, etc.).
27+
repeated PortAttrs ports = 1;
28+
// When true, the gateway only forwards traffic to ports listed in `ports`
29+
// and rejects connections to any other port at TCP-accept time.
30+
bool restrict_mode = 2;
2531
}
2632

2733
// PortAttrs declares per-port behaviour for the gateway.

gateway/src/config.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,18 @@ pub struct ProxyConfig {
118118
/// Maximum concurrent connections per app. 0 means unlimited.
119119
pub max_connections_per_app: u64,
120120
/// Port the dstack guest-agent listens on inside each CVM. Used by the
121-
/// gateway to fetch app metadata (e.g. port_attrs for legacy CVMs).
121+
/// gateway to fetch app metadata (e.g. port_policy for legacy CVMs).
122122
pub agent_port: u16,
123123
/// Whether to read PROXY protocol headers from inbound connections
124124
/// (e.g. when behind a PP-aware load balancer like Cloudflare).
125125
#[serde(default)]
126126
pub inbound_pp_enabled: bool,
127-
/// Background lazy-fetch behaviour for `port_attrs` (legacy CVMs).
128-
pub port_attrs_fetch: PortAttrsFetchConfig,
127+
/// Background lazy-fetch behaviour for `port_policy` (legacy CVMs).
128+
pub port_policy_fetch: PortPolicyFetchConfig,
129129
}
130130

131131
#[derive(Debug, Clone, Deserialize)]
132-
pub struct PortAttrsFetchConfig {
132+
pub struct PortPolicyFetchConfig {
133133
/// Timeout for a single `Info()` RPC attempt.
134134
#[serde(with = "serde_duration")]
135135
pub timeout: Duration,

gateway/src/kv/mod.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,37 @@ pub struct PortFlags {
5050
pub pp: bool,
5151
}
5252

53+
/// Gateway-relevant per-port policy declared by the app in its compose file.
54+
/// Reported atomically at CVM registration; `Option<PortPolicy>` distinguishes
55+
/// "not reported" (legacy CVM) from "reported with no entries".
56+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
57+
pub struct PortPolicy {
58+
/// Per-port flags (PROXY protocol opt-in, etc.).
59+
#[serde(default)]
60+
pub ports: BTreeMap<u16, PortFlags>,
61+
/// When true, only ports listed in `ports` are forwarded; connections to
62+
/// any other port are rejected at TCP-accept time.
63+
#[serde(default)]
64+
pub restrict_mode: bool,
65+
}
66+
5367
/// Instance core data (persistent)
5468
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5569
pub struct InstanceData {
5670
pub app_id: String,
5771
pub ip: Ipv4Addr,
5872
pub public_key: String,
5973
pub reg_time: u64,
60-
/// Per-port flags reported at registration. `None` means "not reported"
74+
/// Port policy reported at registration. `None` means "not reported"
6175
/// (legacy CVM); the gateway will fall back to fetching app-compose via
6276
/// Info() on first connection and populate this lazily.
6377
#[serde(default)]
64-
pub port_attrs: Option<BTreeMap<u16, PortFlags>>,
65-
/// Hex-encoded compose_hash that `port_attrs` was learned against.
78+
pub port_policy: Option<PortPolicy>,
79+
/// Hex-encoded compose_hash that `port_policy` was learned against.
6680
/// When a re-registration presents a different compose_hash (app upgrade),
6781
/// the cache is invalidated and re-fetched lazily.
6882
#[serde(default)]
69-
pub port_attrs_hash: String,
83+
pub port_policy_hash: String,
7084
}
7185

7286
/// Gateway node status (stored separately for independent updates)

0 commit comments

Comments
 (0)