fix: 严格校验TCP发布动作
This commit is contained in:
parent
f27c82a082
commit
760a9693e7
@ -30,6 +30,14 @@ export type TcpReleaseMutation =
|
|||||||
|
|
||||||
export class NetworkTcpReleasePolicyError extends Error {}
|
export class NetworkTcpReleasePolicyError extends Error {}
|
||||||
|
|
||||||
|
const INVALID_MUTATION_MESSAGE = 'Invalid TCP NATMap mutation';
|
||||||
|
const RELEASE_STATE_KEYS = [
|
||||||
|
'externalPort',
|
||||||
|
'internalPort',
|
||||||
|
'natmapDesiredEnabled',
|
||||||
|
'protocolMode',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NetworkTcpReleasePolicyService {
|
export class NetworkTcpReleasePolicyService {
|
||||||
constructor(private readonly configService: ConfigService) {}
|
constructor(private readonly configService: ConfigService) {}
|
||||||
@ -102,28 +110,64 @@ export class NetworkTcpReleasePolicyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private assertMutationShape(mutation: TcpReleaseMutation): void {
|
private assertMutationShape(mutation: TcpReleaseMutation): void {
|
||||||
const value = mutation as Partial<{
|
const value = this.record(mutation);
|
||||||
after: TcpReleaseState;
|
|
||||||
before: TcpReleaseState;
|
|
||||||
current: TcpReleaseState;
|
|
||||||
kind: TcpReleaseMutation['kind'];
|
|
||||||
}>;
|
|
||||||
switch (value.kind) {
|
switch (value.kind) {
|
||||||
case 'create':
|
case 'create': {
|
||||||
if (value.after) return;
|
this.assertExactKeys(value, ['after', 'kind']);
|
||||||
break;
|
this.releaseState(value.after);
|
||||||
case 'update':
|
return;
|
||||||
case 'natmap-disable':
|
}
|
||||||
case 'protocol-shrink':
|
case 'update': {
|
||||||
if (value.before && value.after) return;
|
this.assertExactKeys(value, ['after', 'before', 'kind']);
|
||||||
break;
|
this.releaseState(value.before);
|
||||||
|
this.releaseState(value.after);
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'retry':
|
case 'retry':
|
||||||
case 'natmap-enable':
|
case 'delete': {
|
||||||
case 'delete':
|
this.assertExactKeys(value, ['current', 'kind']);
|
||||||
if (value.current) return;
|
this.releaseState(value.current);
|
||||||
break;
|
return;
|
||||||
|
}
|
||||||
|
case 'natmap-enable': {
|
||||||
|
this.assertExactKeys(value, ['current', 'kind']);
|
||||||
|
const current = this.releaseState(value.current);
|
||||||
|
if (!this.hasTcp(current) || !current.natmapDesiredEnabled) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'natmap-disable': {
|
||||||
|
this.assertExactKeys(value, ['after', 'before', 'kind']);
|
||||||
|
const before = this.releaseState(value.before);
|
||||||
|
const after = this.releaseState(value.after);
|
||||||
|
if (
|
||||||
|
!this.hasTcp(before) ||
|
||||||
|
before.protocolMode !== after.protocolMode ||
|
||||||
|
!this.portsUnchanged(before, after) ||
|
||||||
|
!before.natmapDesiredEnabled ||
|
||||||
|
after.natmapDesiredEnabled
|
||||||
|
) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'protocol-shrink': {
|
||||||
|
this.assertExactKeys(value, ['after', 'before', 'kind']);
|
||||||
|
const before = this.releaseState(value.before);
|
||||||
|
const after = this.releaseState(value.after);
|
||||||
|
if (
|
||||||
|
before.protocolMode !== 'tcp_udp' ||
|
||||||
|
after.protocolMode !== 'udp' ||
|
||||||
|
!this.portsUnchanged(before, after) ||
|
||||||
|
after.natmapDesiredEnabled
|
||||||
|
) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new NetworkTcpReleasePolicyError('Invalid TCP NATMap mutation');
|
this.invalidMutation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private touchesTcp(mutation: TcpReleaseMutation): boolean {
|
private touchesTcp(mutation: TcpReleaseMutation): boolean {
|
||||||
@ -138,34 +182,10 @@ export class NetworkTcpReleasePolicyService {
|
|||||||
if (mutation.kind === 'delete') {
|
if (mutation.kind === 'delete') {
|
||||||
return mutation.current.protocolMode === 'tcp';
|
return mutation.current.protocolMode === 'tcp';
|
||||||
}
|
}
|
||||||
if (mutation.kind === 'natmap-disable') {
|
return (
|
||||||
const valid =
|
mutation.kind === 'natmap-disable' ||
|
||||||
this.hasTcp(mutation.before) &&
|
mutation.kind === 'protocol-shrink'
|
||||||
mutation.before.protocolMode === mutation.after.protocolMode &&
|
);
|
||||||
this.portsUnchanged(mutation.before, mutation.after) &&
|
|
||||||
mutation.before.natmapDesiredEnabled &&
|
|
||||||
!mutation.after.natmapDesiredEnabled;
|
|
||||||
if (!valid) {
|
|
||||||
throw new NetworkTcpReleasePolicyError(
|
|
||||||
'Invalid TCP NATMap disable mutation',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (mutation.kind === 'protocol-shrink') {
|
|
||||||
const valid =
|
|
||||||
mutation.before.protocolMode === 'tcp_udp' &&
|
|
||||||
mutation.after.protocolMode === 'udp' &&
|
|
||||||
this.portsUnchanged(mutation.before, mutation.after) &&
|
|
||||||
!mutation.after.natmapDesiredEnabled;
|
|
||||||
if (!valid) {
|
|
||||||
throw new NetworkTcpReleasePolicyError(
|
|
||||||
'Invalid TCP NATMap protocol shrink',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutationPort(mutation: TcpReleaseMutation): number {
|
private mutationPort(mutation: TcpReleaseMutation): number {
|
||||||
@ -183,6 +203,66 @@ export class NetworkTcpReleasePolicyService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record(value: unknown): Record<string, unknown> {
|
||||||
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertExactKeys(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
expected: readonly string[],
|
||||||
|
): void {
|
||||||
|
const expectedKeys = new Set(expected);
|
||||||
|
const actualKeys = Reflect.ownKeys(value);
|
||||||
|
if (
|
||||||
|
actualKeys.length !== expected.length ||
|
||||||
|
actualKeys.some(
|
||||||
|
(key) => typeof key !== 'string' || !expectedKeys.has(key),
|
||||||
|
) ||
|
||||||
|
expected.some(
|
||||||
|
(key) => !Object.prototype.hasOwnProperty.call(value, key),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseState(value: unknown): TcpReleaseState {
|
||||||
|
const state = this.record(value);
|
||||||
|
this.assertExactKeys(state, RELEASE_STATE_KEYS);
|
||||||
|
const protocolMode = state.protocolMode;
|
||||||
|
const externalPort = state.externalPort;
|
||||||
|
const internalPort = state.internalPort;
|
||||||
|
const natmapDesiredEnabled = state.natmapDesiredEnabled;
|
||||||
|
if (
|
||||||
|
(protocolMode !== 'tcp' &&
|
||||||
|
protocolMode !== 'tcp_udp' &&
|
||||||
|
protocolMode !== 'udp') ||
|
||||||
|
!this.isPort(externalPort) ||
|
||||||
|
!this.isPort(internalPort) ||
|
||||||
|
typeof natmapDesiredEnabled !== 'boolean' ||
|
||||||
|
(protocolMode === 'udp' && natmapDesiredEnabled)
|
||||||
|
) {
|
||||||
|
this.invalidMutation();
|
||||||
|
}
|
||||||
|
return state as TcpReleaseState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPort(value: unknown): value is number {
|
||||||
|
return (
|
||||||
|
typeof value === 'number' &&
|
||||||
|
Number.isInteger(value) &&
|
||||||
|
value >= 1 &&
|
||||||
|
value <= 65_535
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidMutation(): never {
|
||||||
|
throw new NetworkTcpReleasePolicyError(INVALID_MUTATION_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
private invalidCanaryPorts(): never {
|
private invalidCanaryPorts(): never {
|
||||||
throw new NetworkTcpReleasePolicyError('Invalid TCP NATMap canary ports');
|
throw new NetworkTcpReleasePolicyError('Invalid TCP NATMap canary ports');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,63 @@ const udp = (overrides = {}) => ({
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const INVALID_MUTATION_MESSAGE = 'Invalid TCP NATMap mutation';
|
||||||
|
const validMutationCases = [
|
||||||
|
{
|
||||||
|
kind: 'create',
|
||||||
|
mutation: { after: tcp(), kind: 'create' },
|
||||||
|
stateKeys: ['after'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'update',
|
||||||
|
mutation: { after: tcp(), before: tcp(), kind: 'update' },
|
||||||
|
stateKeys: ['after', 'before'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'retry',
|
||||||
|
mutation: { current: tcp(), kind: 'retry' },
|
||||||
|
stateKeys: ['current'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'natmap-enable',
|
||||||
|
mutation: { current: tcp(), kind: 'natmap-enable' },
|
||||||
|
stateKeys: ['current'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'natmap-disable',
|
||||||
|
mutation: {
|
||||||
|
after: tcp({ natmapDesiredEnabled: false }),
|
||||||
|
before: tcp(),
|
||||||
|
kind: 'natmap-disable',
|
||||||
|
},
|
||||||
|
stateKeys: ['after', 'before'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'protocol-shrink',
|
||||||
|
mutation: {
|
||||||
|
after: udp({ externalPort: 48213, internalPort: 48213 }),
|
||||||
|
before: tcp({ protocolMode: 'tcp_udp' }),
|
||||||
|
kind: 'protocol-shrink',
|
||||||
|
},
|
||||||
|
stateKeys: ['after', 'before'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'delete',
|
||||||
|
mutation: { current: tcp(), kind: 'delete' },
|
||||||
|
stateKeys: ['current'],
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function expectInvalidMutation(mutation: unknown): void {
|
||||||
|
const service = policy({
|
||||||
|
NETWORK_TCP_NATMAP_CANARY_PORTS: 'not-a-port',
|
||||||
|
NETWORK_TCP_NATMAP_RELEASE_MODE: 'not-a-mode',
|
||||||
|
});
|
||||||
|
const validate = () => service.assertMutationAllowed(mutation as never);
|
||||||
|
expect(validate).toThrow(NetworkTcpReleasePolicyError);
|
||||||
|
expect(validate).toThrow(INVALID_MUTATION_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
describe('NetworkTcpReleasePolicyService', () => {
|
describe('NetworkTcpReleasePolicyService', () => {
|
||||||
it('fails closed by default and parses the four release modes', () => {
|
it('fails closed by default and parses the four release modes', () => {
|
||||||
expect(policy({}).readMode()).toBe('off');
|
expect(policy({}).readMode()).toBe('off');
|
||||||
@ -198,4 +255,122 @@ describe('NetworkTcpReleasePolicyService', () => {
|
|||||||
service.assertMutationAllowed({ current: tcp(), kind: 'retry' }),
|
service.assertMutationAllowed({ current: tcp(), kind: 'retry' }),
|
||||||
).toThrow(NetworkTcpReleasePolicyError);
|
).toThrow(NetworkTcpReleasePolicyError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(validMutationCases)(
|
||||||
|
'accepts the exact valid $kind mutation record',
|
||||||
|
({ mutation }) => {
|
||||||
|
expect(() =>
|
||||||
|
policy({
|
||||||
|
NETWORK_TCP_NATMAP_RELEASE_MODE: 'on',
|
||||||
|
}).assertMutationAllowed(mutation),
|
||||||
|
).not.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(validMutationCases)(
|
||||||
|
'rejects malformed top-level and nested records for $kind before reading release config',
|
||||||
|
({ mutation, stateKeys }) => {
|
||||||
|
const record = mutation as unknown as Record<string, unknown>;
|
||||||
|
expectInvalidMutation({ ...record, unexpected: true });
|
||||||
|
expectInvalidMutation(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(record).filter(([key]) => key !== 'kind'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const stateKey of stateKeys) {
|
||||||
|
expectInvalidMutation(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(record).filter(([key]) => key !== stateKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const invalidState of [null, [], {}, 'tcp', 1]) {
|
||||||
|
expectInvalidMutation({ ...record, [stateKey]: invalidState });
|
||||||
|
}
|
||||||
|
expectInvalidMutation({
|
||||||
|
...record,
|
||||||
|
[stateKey]: {
|
||||||
|
...(record[stateKey] as Record<string, unknown>),
|
||||||
|
unexpected: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['missing protocol', { ...tcp(), protocolMode: undefined }],
|
||||||
|
['unknown protocol', { ...tcp(), protocolMode: 'icmp' }],
|
||||||
|
['missing external port', { ...tcp(), externalPort: undefined }],
|
||||||
|
['NaN external port', { ...tcp(), externalPort: Number.NaN }],
|
||||||
|
['fractional external port', { ...tcp(), externalPort: 48213.5 }],
|
||||||
|
['zero external port', { ...tcp(), externalPort: 0 }],
|
||||||
|
['out-of-range external port', { ...tcp(), externalPort: 65_536 }],
|
||||||
|
['string external port', { ...tcp(), externalPort: '48213' }],
|
||||||
|
['missing internal port', { ...tcp(), internalPort: undefined }],
|
||||||
|
['NaN internal port', { ...tcp(), internalPort: Number.NaN }],
|
||||||
|
['fractional internal port', { ...tcp(), internalPort: 48213.5 }],
|
||||||
|
['zero internal port', { ...tcp(), internalPort: 0 }],
|
||||||
|
['out-of-range internal port', { ...tcp(), internalPort: 65_536 }],
|
||||||
|
['string internal port', { ...tcp(), internalPort: '48213' }],
|
||||||
|
[
|
||||||
|
'missing NATMap intent',
|
||||||
|
{ ...tcp(), natmapDesiredEnabled: undefined },
|
||||||
|
],
|
||||||
|
['non-boolean NATMap intent', { ...tcp(), natmapDesiredEnabled: 1 }],
|
||||||
|
['UDP with NATMap enabled', { ...udp(), natmapDesiredEnabled: true }],
|
||||||
|
])('rejects a state with %s', (_label, after) => {
|
||||||
|
expectInvalidMutation({ after, kind: 'create' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'NATMap enable on UDP',
|
||||||
|
{ current: udp(), kind: 'natmap-enable' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'NATMap enable without enabled intent',
|
||||||
|
{
|
||||||
|
current: tcp({ natmapDesiredEnabled: false }),
|
||||||
|
kind: 'natmap-enable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'NATMap disable on UDP',
|
||||||
|
{
|
||||||
|
after: udp(),
|
||||||
|
before: udp(),
|
||||||
|
kind: 'natmap-disable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'NATMap disable without an intent transition',
|
||||||
|
{
|
||||||
|
after: tcp(),
|
||||||
|
before: tcp(),
|
||||||
|
kind: 'natmap-disable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'protocol shrink from TCP-only',
|
||||||
|
{
|
||||||
|
after: udp({ externalPort: 48213, internalPort: 48213 }),
|
||||||
|
before: tcp(),
|
||||||
|
kind: 'protocol-shrink',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])('rejects the impossible %s mutation', (_label, mutation) => {
|
||||||
|
expectInvalidMutation(mutation);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['null', null],
|
||||||
|
['array', []],
|
||||||
|
['string', 'create'],
|
||||||
|
['number', 1],
|
||||||
|
['empty object', {}],
|
||||||
|
['unknown kind', { after: tcp(), kind: 'replace' }],
|
||||||
|
])('rejects a %s mutation container', (_label, mutation) => {
|
||||||
|
expectInvalidMutation(mutation);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user