Skip to content
Merged
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
6 changes: 6 additions & 0 deletions libs/backend-api7/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export class ToADC {
server_addr: route.server_addr,
server_port: route.server_port,
remote_addr: route.remote_addr,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
});
}

Expand Down Expand Up @@ -224,6 +227,9 @@ export class FromADC {
server_addr: route.server_addr,
server_port: route.server_port,
remote_addr: route.remote_addr,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
});
}

Expand Down
3 changes: 3 additions & 0 deletions libs/backend-api7/src/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export interface StreamRoute {
server_addr?: string;
server_port?: number;
remote_addr?: string;
sni?: string;
snis?: Array<string>;
tls_passthrough?: boolean;
}
export interface Service {
id?: string;
Expand Down
57 changes: 57 additions & 0 deletions libs/backend-api7/test/transformer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,63 @@ describe('Transformer', () => {
});
});

// Regression: both directions used to drop the SNI match outright (ToADC
// never read it, FromADC never wrote it), because the control plane had no
// field to carry it. It does now, together with snis and tls_passthrough, so
// a TLS passthrough stream route survives a dump/sync round trip instead of
// losing what it matches on.
describe('stream route SNI match and tls_passthrough round-trip', () => {
const snis = ['a.example.com', 'b.example.com'];

it('FromADC.transformStreamRoute writes snis and tls_passthrough', () => {
const out = new FromADC().transformStreamRoute(
{
id: 'sr1',
name: 'sr1',
snis,
tls_passthrough: true,
} as ADCSDK.StreamRoute,
'svc1',
);
expect(out.snis).toEqual(snis);
expect(out.tls_passthrough).toBe(true);
});

it('ToADC.transformStreamRoute preserves snis and tls_passthrough on dump', () => {
const out = new ToADC().transformStreamRoute({
id: 'sr1',
name: 'sr1',
service_id: 'svc1',
stream_route_id: 'sr1',
snis,
tls_passthrough: true,
} as typing.StreamRoute);
expect(out.snis).toEqual(snis);
expect(out.tls_passthrough).toBe(true);
});

// The singular form gets its own case rather than being added to the two
// above: it is the value ToADC used to drop, and the gateway rejects a
// stream route carrying `sni` and `snis` at once.
it('carries the singular sni in both directions', () => {
const wire = new FromADC().transformStreamRoute(
{
id: 'sr1',
name: 'sr1',
sni: 'a.example.com',
} as ADCSDK.StreamRoute,
'svc1',
);
expect(wire.sni).toEqual('a.example.com');

const out = new ToADC().transformStreamRoute({
...wire,
id: 'sr1',
} as typing.StreamRoute);
expect(out.sni).toEqual('a.example.com');
});
});

describe('active health check req_headers round-trip', () => {
it('ToADC.transformUpstream maps req_headers to ADC http_req_headers', () => {
const out = new ToADC().transformUpstream({
Expand Down
2 changes: 2 additions & 0 deletions libs/backend-apisix-standalone/src/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ export class Operator extends ADCSDK.backend.BackendEventSource {
server_addr: res.server_addr,
server_port: res.server_port,
sni: res.sni,
snis: res.snis,
tls_passthrough: res.tls_passthrough,
service_id: event.parentId!,
} satisfies typing.StreamRoute as typing.StreamRoute;
}
Expand Down
2 changes: 2 additions & 0 deletions libs/backend-apisix-standalone/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export const toADC = (input: typing.APISIXStandalone) => {
server_addr: route.server_addr,
server_port: route.server_port,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
plugins: route.plugins,
}))
.map(ADCSDK.utils.recursiveOmitUndefined),
Expand Down
2 changes: 2 additions & 0 deletions libs/backend-apisix-standalone/src/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ const StreamRouteSchema = z.strictObject({
server_addr: z.string().optional(),
server_port: Port.optional(),
sni: z.string().optional(),
snis: z.array(z.string()).min(1).optional(),
tls_passthrough: z.boolean().optional(),
service_id: Metadata.id,
plugins: Plugins.optional(),
protocol: z
Expand Down
4 changes: 4 additions & 0 deletions libs/backend-apisix/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ export class ToADC {
server_addr: streamRoute.server_addr,
server_port: streamRoute.server_port,
sni: streamRoute.sni,
snis: streamRoute.snis,
tls_passthrough: streamRoute.tls_passthrough,
} as ADCSDK.StreamRoute);
}

Expand Down Expand Up @@ -463,6 +465,8 @@ export class FromADC {
server_addr: streamRoute.server_addr,
server_port: streamRoute.server_port,
sni: streamRoute.sni,
snis: streamRoute.snis,
tls_passthrough: streamRoute.tls_passthrough,
service_id: parentId,
} as unknown as typing.StreamRoute);
}
Expand Down
2 changes: 2 additions & 0 deletions libs/backend-apisix/src/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export interface StreamRoute {
server_addr?: string;
server_port?: number;
sni?: string;
snis?: Array<string>;
tls_passthrough?: boolean;
upstream?: InlineUpstream;
upstream_id?: string;
service_id?: string;
Expand Down
28 changes: 28 additions & 0 deletions libs/backend-apisix/test/transformer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ describe('Transformer', () => {
});
});

// snis (the plural SNI match) and tls_passthrough are newer gateway stream
// route fields; both directions have to carry them, or a TLS passthrough
// route silently degrades into a terminating one that matches nothing.
describe('stream route snis and tls_passthrough', () => {
const snis = ['a.example.com', 'b.example.com'];

it('should write snis and tls_passthrough to the wire', () => {
const wire = new FromADC().transformStreamRoute(
{ name: 'my-stream-route', snis, tls_passthrough: true },
'svc1',
'native',
);
expect(wire.snis).toEqual(snis);
expect(wire.tls_passthrough).toBe(true);
});

it('should read snis and tls_passthrough back on dump', () => {
expect(
new ToADC().transformStreamRoute({
id: 'sr1',
name: 'sr1',
snis,
tls_passthrough: true,
} as typing.StreamRoute),
).toMatchObject({ snis, tls_passthrough: true });
});
});

describe('stream route name persistence', () => {
const streamRoute = { name: 'my-stream-route' } as ADCSDK.StreamRoute;

Expand Down
4 changes: 4 additions & 0 deletions libs/sdk/src/core/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ const streamRouteSchema = z.strictObject({
remote_addr: z.string().optional(),
server_addr: z.string().optional(),
server_port: portSchema.optional(),
// `sni` and `snis` are singular/plural forms of the same match, and the
// gateway rejects a stream route carrying both.
sni: hostSchema.optional(),
snis: z.array(hostSchema).min(1).optional(),
tls_passthrough: z.boolean().optional(),
});
export type StreamRoute = z.infer<typeof streamRouteSchema>;
export { streamRouteSchema };
Expand Down
75 changes: 74 additions & 1 deletion rust/crates/adc-backend-api7/src/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,9 @@ impl From<typing::StreamRoute> for adc::StreamRoute {
remote_addr: route.remote_addr,
server_addr: route.server_addr,
server_port: route.server_port,
sni: None,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
}
}
}
Expand Down Expand Up @@ -384,6 +386,9 @@ pub fn transform_stream_route(route: adc::StreamRoute, parent_id: String) -> typ
server_addr: route.server_addr,
server_port: route.server_port,
remote_addr: route.remote_addr,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
}
}

Expand Down Expand Up @@ -541,6 +546,8 @@ mod tests {
server_addr: None,
server_port: None,
sni: None,
snis: None,
tls_passthrough: None,
};

let wire = transform_stream_route(route, "svc1".to_string());
Expand All @@ -565,13 +572,79 @@ mod tests {
server_addr: Some("1.1.1.1".to_string()),
server_port: Some(80),
remote_addr: None,
sni: None,
snis: None,
tls_passthrough: None,
};

let route = adc::StreamRoute::from(wire);

assert_eq!(route.plugins, Some(ip_restriction_plugins()));
}

/// Regression: both stream route conversions used to drop the SNI
/// match outright (the read direction hardcoded `sni: None`), because
/// the control plane had no field to carry it. It does now, together
/// with `snis` and `tls_passthrough`, so a TLS passthrough stream route
/// survives a dump/sync round trip instead of losing what it matches on.
#[test]
fn stream_route_round_trips_its_sni_match_and_tls_passthrough() {
let route = adc::StreamRoute {
id: Some("sr1".to_string()),
name: "sr1".to_string(),
description: None,
labels: None,
plugins: None,
remote_addr: None,
server_addr: None,
server_port: None,
sni: None,
snis: Some(vec!["a.example.com".to_string()]),
tls_passthrough: Some(true),
};

let wire = transform_stream_route(route.clone(), "svc1".to_string());
assert_eq!(wire.snis, Some(vec!["a.example.com".to_string()]));
assert_eq!(wire.tls_passthrough, Some(true));

let back = adc::StreamRoute::from(typing::StreamRoute {
id: wire.stream_route_id.clone(),
..wire
});
assert_eq!(back.snis, route.snis);
assert_eq!(back.tls_passthrough, route.tls_passthrough);
}

/// The singular form gets its own case rather than being added to the
/// fixture above: it is the exact value the read direction used to
/// hardcode to `None`, and the gateway rejects a stream route that
/// carries `sni` and `snis` at once, so the two cannot share a fixture.
#[test]
fn stream_route_round_trips_its_singular_sni() {
let route = adc::StreamRoute {
id: Some("sr1".to_string()),
name: "sr1".to_string(),
description: None,
labels: None,
plugins: None,
remote_addr: None,
server_addr: None,
server_port: None,
sni: Some("a.example.com".to_string()),
snis: None,
tls_passthrough: None,
};

let wire = transform_stream_route(route.clone(), "svc1".to_string());
assert_eq!(wire.sni, Some("a.example.com".to_string()));

let back = adc::StreamRoute::from(typing::StreamRoute {
id: wire.stream_route_id.clone(),
..wire
});
assert_eq!(back.sni, route.sni);
}

/// `server_port` is `u16` at the wire level too (not a wider int
/// narrowed later): a port outside 0-65535 is never a real port, so a
/// server response containing one is rejected right at deserialization
Expand Down
6 changes: 6 additions & 0 deletions rust/crates/adc-backend-api7/src/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ pub struct StreamRoute {
pub server_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_addr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sni: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snis: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls_passthrough: Option<bool>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ fn stream_route_to_adc(route: &typing::StreamRoute) -> adc::StreamRoute {
server_addr: route.server_addr.clone(),
server_port: route.server_port,
sni: route.sni.clone(),
snis: route.snis.clone(),
tls_passthrough: route.tls_passthrough,
}
}

Expand Down Expand Up @@ -464,6 +466,8 @@ fn stream_route_to_wire(route: &adc::StreamRoute, service_id: &str) -> typing::S
server_addr: route.server_addr.clone(),
server_port: route.server_port,
sni: route.sni.clone(),
snis: route.snis.clone(),
tls_passthrough: route.tls_passthrough,
service_id: service_id.to_string(),

protocol: None,
Expand Down Expand Up @@ -850,6 +854,8 @@ mod tests {
server_addr: None,
server_port: Some(3000 + index as u16 * 10 + k as u16),
sni: None,
snis: None,
tls_passthrough: None,
})
.collect(),
}),
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/adc-backend-apisix-standalone/src/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@ pub struct StreamRoute {
pub server_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sni: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snis: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls_passthrough: Option<bool>,
pub service_id: String,

#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ fn stream_route(name: &str, server_port: u16) -> adc::StreamRoute {
server_addr: None,
server_port: Some(server_port),
sni: None,
snis: None,
tls_passthrough: None,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ fn stream_route(name: &str, server_port: u16) -> adc::StreamRoute {
server_addr: None,
server_port: Some(server_port),
sni: None,
snis: None,
tls_passthrough: None,
}
}

Expand Down
4 changes: 4 additions & 0 deletions rust/crates/adc-backend-apisix/src/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ impl From<typing::StreamRoute> for adc::StreamRoute {
server_addr: route.server_addr,
server_port: route.server_port,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
}
}
}
Expand Down Expand Up @@ -679,6 +681,8 @@ pub fn transform_stream_route(
server_addr: route.server_addr,
server_port: route.server_port,
sni: route.sni,
snis: route.snis,
tls_passthrough: route.tls_passthrough,
upstream: None,
upstream_id: None,
service_id: Some(parent_id),
Expand Down
Loading
Loading