Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc-js-xds: Implement xDS Server #2783

Merged
merged 2 commits into from
Jul 3, 2024
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
1 change: 1 addition & 0 deletions packages/grpc-js-xds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/mocha": "^5.2.6",
"@types/node": "^13.11.1",
"@types/yargs": "^15.0.5",
"find-free-ports": "^3.1.1",
"gts": "^5.0.1",
"typescript": "^5.1.3",
"yargs": "^15.4.1"
Expand Down
192 changes: 192 additions & 0 deletions packages/grpc-js-xds/src/cidr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as net from 'net';
import { CidrRange__Output } from './generated/envoy/config/core/v3/CidrRange';

const IPV4_COMPONENT_COUNT = 4n;
const IPV4_COMPONENT_SIZE = 8n;
const IPV4_COMPONENT_CAP = 1n << IPV4_COMPONENT_SIZE;
const IPV4_TOTAL_SIZE = IPV4_COMPONENT_COUNT * IPV4_COMPONENT_SIZE;
const IPV6_COMPONENT_SIZE = 16n;
const IPV6_COMPONENT_COUNT = 8n;
const IPV6_COMPONENT_CAP = 1n << IPV6_COMPONENT_SIZE;
const IPV6_TOTAL_SIZE = IPV6_COMPONENT_COUNT * IPV6_COMPONENT_SIZE;

export interface CidrRange {
addressPrefix: string;
prefixLen: number;
}

export function parseIPv4(address: string): bigint {
return address.split('.').map(component => BigInt(component)).reduce((accumulator, current) => accumulator * IPV4_COMPONENT_CAP + current, 0n);
}

export function parseIPv6(address: string): bigint {
/* If an IPv6 address contains two or more consecutive components with value
* which can be collectively represented with the string '::'. For example,
* the IPv6 adddress 0:0:0:0:0:0:0:1 can also be represented as ::1. Here we
* expand any :: into the correct number of individual components. */
const sections = address.split('::');
let components: string[];
if (sections.length === 1) {
components = sections[0].split(':');
} else if (sections.length === 2) {
const beginning = sections[0].split(':').filter(value => value !== '');
const end = sections[1].split(':').filter(value => value !== '');
components = beginning.concat(Array(8 - beginning.length - end.length).fill('0'), end);
} else {
throw new Error('Invalid IPv6 address contains more than one instance of ::');
}
return components.map(component => BigInt('0x' + component)).reduce((accumulator, current) => accumulator * 65536n + current, 0n);
}

function parseIP(address: string): bigint {
switch (net.isIP(address)) {
case 4:
return parseIPv4(address);
case 6:
return parseIPv6(address);
default:
throw new Error(`Invalid IP address ${address}`);
}
}

export function formatIPv4(address: bigint): string {
const reverseComponents: bigint[] = [];
for (let i = 0; i < IPV4_COMPONENT_COUNT; i++) {
reverseComponents.push(address % IPV4_COMPONENT_CAP);
address = address / IPV4_COMPONENT_CAP;
}
return reverseComponents.reverse().map(component => component.toString(10)).join('.');
}

export function formatIPv6(address: bigint): string {
const reverseComponents: bigint[] = [];
for (let i = 0; i < IPV6_COMPONENT_COUNT; i++) {
reverseComponents.push(address % IPV6_COMPONENT_CAP);
address = address / IPV6_COMPONENT_CAP;
}
const components = reverseComponents.reverse();
/* Find the longest run of consecutive 0 values in the list of components, to
* replace it with :: in the output */
let maxZeroRunIndex = 0;
let maxZeroRunLength = 0;
let inZeroRun = false;
let currentZeroRunIndex = 0;
let currentZeroRunLength = 0;
for (let i = 0; i < components.length; i++) {
if (components[i] === 0n) {
if (inZeroRun) {
currentZeroRunLength += 1;
} else {
inZeroRun = true;
currentZeroRunIndex = i;
currentZeroRunLength = 1;
}
if (currentZeroRunLength > maxZeroRunLength) {
maxZeroRunIndex = currentZeroRunIndex;
maxZeroRunLength = currentZeroRunLength;
}
} else {
currentZeroRunLength = 0;
inZeroRun = false;
}
}
if (maxZeroRunLength >= 2) {
const beginning = components.slice(0, maxZeroRunIndex);
const end = components.slice(maxZeroRunIndex + maxZeroRunLength);
return beginning.map(value => value.toString(16)).join(':') + '::' + end.map(value => value.toString(16)).join(':');
} else {
return components.map(value => value.toString(16)).join(':');
}
}

function getSubnetMaskIPv4(prefixLen: number) {
return ~((1n << (IPV4_TOTAL_SIZE - BigInt(prefixLen))) - 1n);
}

function getSubnetMaskIPv6(prefixLen: number) {
return ~((1n << (IPV6_TOTAL_SIZE - BigInt(prefixLen))) - 1n);
}

export function firstNBitsIPv4(address: string, prefixLen: number): string {
const addressNum = parseIPv4(address);
const prefixMask = getSubnetMaskIPv4(prefixLen);
return formatIPv4(addressNum & prefixMask);
}

export function firstNBitsIPv6(address: string, prefixLen: number): string {
const addressNum = parseIPv6(address);
const prefixMask = getSubnetMaskIPv6(prefixLen);
return formatIPv6(addressNum & prefixMask);
}

export function normalizeCidrRange(range: CidrRange): CidrRange {
switch (net.isIP(range.addressPrefix)) {
case 4: {
const prefixLen = Math.min(Math.max(range.prefixLen, 0), 32);
return {
addressPrefix: firstNBitsIPv4(range.addressPrefix, prefixLen),
prefixLen: prefixLen
};
}
case 6: {
const prefixLen = Math.min(Math.max(range.prefixLen, 0), 128);
return {
addressPrefix: firstNBitsIPv6(range.addressPrefix, prefixLen),
prefixLen: prefixLen
};
}
default:
throw new Error(`Invalid IP address prefix ${range.addressPrefix}`);
}
}

export function getCidrRangeSubnetMask(range: CidrRange): bigint {
switch (net.isIP(range.addressPrefix)) {
case 4:
return getSubnetMaskIPv4(range.prefixLen);
case 6:
return getSubnetMaskIPv6(range.prefixLen);
default:
throw new Error('Invalid CIDR range');
}
}

export function inCidrRange(range: CidrRange, address: string): boolean {
if (net.isIP(range.addressPrefix) !== net.isIP(address)) {
return false;
}
return (parseIP(address) & getCidrRangeSubnetMask(range)) === parseIP(range.addressPrefix);
}

export function cidrRangeEqual(range1: CidrRange | undefined, range2: CidrRange | undefined): boolean {
if (range1 === undefined && range2 === undefined) {
return true;
}
if (range1 === undefined || range2 === undefined) {
return false;
}
return range1.addressPrefix === range2.addressPrefix && range1.prefixLen === range2.prefixLen;
}

export function cidrRangeMessageToCidrRange(message: CidrRange__Output): CidrRange {
return {
addressPrefix: message.address_prefix,
prefixLen: message.prefix_len?.value ?? 0
};
}
48 changes: 48 additions & 0 deletions packages/grpc-js-xds/src/cross-product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Types and function from https://stackoverflow.com/a/72059390/159388, with modifications
type ElementType<A> = A extends ReadonlyArray<infer T> ? T | undefined : never;

type ElementsOfAll<Inputs, R extends ReadonlyArray<unknown> = []> = Inputs extends readonly [infer F, ...infer M] ? ElementsOfAll<M, [...R, ElementType<F>]> : R;

type CartesianProduct<Inputs> = ElementsOfAll<Inputs>[];

/**
* Get the cross product or Cartesian product of a list of groups. The
* implementation is copied, with some modifications, from
* https://stackoverflow.com/a/72059390/159388.
* @param sets A list of groups of elements
* @returns A list of all possible combinations of one element from each group
* in sets. Empty groups will result in undefined in that slot in each
* combination.
*/
export function crossProduct<Sets extends ReadonlyArray<ReadonlyArray<unknown>>>(sets: Sets): CartesianProduct<Sets> {
/* The input is an array of arrays, and the expected output is an array of
* each possible combination of one element each of the input arrays, with
* the exception that if one of the input arrays is empty, each combination
* gets [undefined] in that slot.
*
* At each step in the reduce call, we start with the cross product of the
* first N groups, and the next group. For each combation, for each element
* of the next group, extend the combination with that element.
*
* The type assertion at the end is needed because TypeScript doesn't track
* the types well enough through the reduce calls to see that the result has
* the expected type.
*/
return sets.map(x => x.length === 0 ? [undefined] : x).reduce((combinations: unknown[][], nextGroup) => combinations.flatMap(combination => nextGroup.map(element => [...combination, element])), [[]] as unknown[][]) as CartesianProduct<Sets>;
}
2 changes: 2 additions & 0 deletions packages/grpc-js-xds/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import * as round_robin_lb from './lb-policy-registry/round-robin';
import * as typed_struct_lb from './lb-policy-registry/typed-struct';
import * as pick_first_lb from './lb-policy-registry/pick-first';

export { XdsServer } from './server';

/**
* Register the "xds:" name scheme with the @grpc/grpc-js library.
*/
Expand Down
Loading
Loading