Skip to content

Commit

Permalink
Merge pull request #2783 from murgatroid99/grpc-js-xds_server2
Browse files Browse the repository at this point in the history
grpc-js-xds: Implement xDS Server
  • Loading branch information
murgatroid99 authored Jul 3, 2024
2 parents d83355b + f2dcb21 commit da54e75
Show file tree
Hide file tree
Showing 29 changed files with 2,405 additions and 718 deletions.
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

0 comments on commit da54e75

Please sign in to comment.