Skip to content

Commit da54e75

Browse files
authored
Merge pull request #2783 from murgatroid99/grpc-js-xds_server2
grpc-js-xds: Implement xDS Server
2 parents d83355b + f2dcb21 commit da54e75

29 files changed

+2405
-718
lines changed

packages/grpc-js-xds/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@types/mocha": "^5.2.6",
4040
"@types/node": "^13.11.1",
4141
"@types/yargs": "^15.0.5",
42+
"find-free-ports": "^3.1.1",
4243
"gts": "^5.0.1",
4344
"typescript": "^5.1.3",
4445
"yargs": "^15.4.1"

packages/grpc-js-xds/src/cidr.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as net from 'net';
18+
import { CidrRange__Output } from './generated/envoy/config/core/v3/CidrRange';
19+
20+
const IPV4_COMPONENT_COUNT = 4n;
21+
const IPV4_COMPONENT_SIZE = 8n;
22+
const IPV4_COMPONENT_CAP = 1n << IPV4_COMPONENT_SIZE;
23+
const IPV4_TOTAL_SIZE = IPV4_COMPONENT_COUNT * IPV4_COMPONENT_SIZE;
24+
const IPV6_COMPONENT_SIZE = 16n;
25+
const IPV6_COMPONENT_COUNT = 8n;
26+
const IPV6_COMPONENT_CAP = 1n << IPV6_COMPONENT_SIZE;
27+
const IPV6_TOTAL_SIZE = IPV6_COMPONENT_COUNT * IPV6_COMPONENT_SIZE;
28+
29+
export interface CidrRange {
30+
addressPrefix: string;
31+
prefixLen: number;
32+
}
33+
34+
export function parseIPv4(address: string): bigint {
35+
return address.split('.').map(component => BigInt(component)).reduce((accumulator, current) => accumulator * IPV4_COMPONENT_CAP + current, 0n);
36+
}
37+
38+
export function parseIPv6(address: string): bigint {
39+
/* If an IPv6 address contains two or more consecutive components with value
40+
* which can be collectively represented with the string '::'. For example,
41+
* the IPv6 adddress 0:0:0:0:0:0:0:1 can also be represented as ::1. Here we
42+
* expand any :: into the correct number of individual components. */
43+
const sections = address.split('::');
44+
let components: string[];
45+
if (sections.length === 1) {
46+
components = sections[0].split(':');
47+
} else if (sections.length === 2) {
48+
const beginning = sections[0].split(':').filter(value => value !== '');
49+
const end = sections[1].split(':').filter(value => value !== '');
50+
components = beginning.concat(Array(8 - beginning.length - end.length).fill('0'), end);
51+
} else {
52+
throw new Error('Invalid IPv6 address contains more than one instance of ::');
53+
}
54+
return components.map(component => BigInt('0x' + component)).reduce((accumulator, current) => accumulator * 65536n + current, 0n);
55+
}
56+
57+
function parseIP(address: string): bigint {
58+
switch (net.isIP(address)) {
59+
case 4:
60+
return parseIPv4(address);
61+
case 6:
62+
return parseIPv6(address);
63+
default:
64+
throw new Error(`Invalid IP address ${address}`);
65+
}
66+
}
67+
68+
export function formatIPv4(address: bigint): string {
69+
const reverseComponents: bigint[] = [];
70+
for (let i = 0; i < IPV4_COMPONENT_COUNT; i++) {
71+
reverseComponents.push(address % IPV4_COMPONENT_CAP);
72+
address = address / IPV4_COMPONENT_CAP;
73+
}
74+
return reverseComponents.reverse().map(component => component.toString(10)).join('.');
75+
}
76+
77+
export function formatIPv6(address: bigint): string {
78+
const reverseComponents: bigint[] = [];
79+
for (let i = 0; i < IPV6_COMPONENT_COUNT; i++) {
80+
reverseComponents.push(address % IPV6_COMPONENT_CAP);
81+
address = address / IPV6_COMPONENT_CAP;
82+
}
83+
const components = reverseComponents.reverse();
84+
/* Find the longest run of consecutive 0 values in the list of components, to
85+
* replace it with :: in the output */
86+
let maxZeroRunIndex = 0;
87+
let maxZeroRunLength = 0;
88+
let inZeroRun = false;
89+
let currentZeroRunIndex = 0;
90+
let currentZeroRunLength = 0;
91+
for (let i = 0; i < components.length; i++) {
92+
if (components[i] === 0n) {
93+
if (inZeroRun) {
94+
currentZeroRunLength += 1;
95+
} else {
96+
inZeroRun = true;
97+
currentZeroRunIndex = i;
98+
currentZeroRunLength = 1;
99+
}
100+
if (currentZeroRunLength > maxZeroRunLength) {
101+
maxZeroRunIndex = currentZeroRunIndex;
102+
maxZeroRunLength = currentZeroRunLength;
103+
}
104+
} else {
105+
currentZeroRunLength = 0;
106+
inZeroRun = false;
107+
}
108+
}
109+
if (maxZeroRunLength >= 2) {
110+
const beginning = components.slice(0, maxZeroRunIndex);
111+
const end = components.slice(maxZeroRunIndex + maxZeroRunLength);
112+
return beginning.map(value => value.toString(16)).join(':') + '::' + end.map(value => value.toString(16)).join(':');
113+
} else {
114+
return components.map(value => value.toString(16)).join(':');
115+
}
116+
}
117+
118+
function getSubnetMaskIPv4(prefixLen: number) {
119+
return ~((1n << (IPV4_TOTAL_SIZE - BigInt(prefixLen))) - 1n);
120+
}
121+
122+
function getSubnetMaskIPv6(prefixLen: number) {
123+
return ~((1n << (IPV6_TOTAL_SIZE - BigInt(prefixLen))) - 1n);
124+
}
125+
126+
export function firstNBitsIPv4(address: string, prefixLen: number): string {
127+
const addressNum = parseIPv4(address);
128+
const prefixMask = getSubnetMaskIPv4(prefixLen);
129+
return formatIPv4(addressNum & prefixMask);
130+
}
131+
132+
export function firstNBitsIPv6(address: string, prefixLen: number): string {
133+
const addressNum = parseIPv6(address);
134+
const prefixMask = getSubnetMaskIPv6(prefixLen);
135+
return formatIPv6(addressNum & prefixMask);
136+
}
137+
138+
export function normalizeCidrRange(range: CidrRange): CidrRange {
139+
switch (net.isIP(range.addressPrefix)) {
140+
case 4: {
141+
const prefixLen = Math.min(Math.max(range.prefixLen, 0), 32);
142+
return {
143+
addressPrefix: firstNBitsIPv4(range.addressPrefix, prefixLen),
144+
prefixLen: prefixLen
145+
};
146+
}
147+
case 6: {
148+
const prefixLen = Math.min(Math.max(range.prefixLen, 0), 128);
149+
return {
150+
addressPrefix: firstNBitsIPv6(range.addressPrefix, prefixLen),
151+
prefixLen: prefixLen
152+
};
153+
}
154+
default:
155+
throw new Error(`Invalid IP address prefix ${range.addressPrefix}`);
156+
}
157+
}
158+
159+
export function getCidrRangeSubnetMask(range: CidrRange): bigint {
160+
switch (net.isIP(range.addressPrefix)) {
161+
case 4:
162+
return getSubnetMaskIPv4(range.prefixLen);
163+
case 6:
164+
return getSubnetMaskIPv6(range.prefixLen);
165+
default:
166+
throw new Error('Invalid CIDR range');
167+
}
168+
}
169+
170+
export function inCidrRange(range: CidrRange, address: string): boolean {
171+
if (net.isIP(range.addressPrefix) !== net.isIP(address)) {
172+
return false;
173+
}
174+
return (parseIP(address) & getCidrRangeSubnetMask(range)) === parseIP(range.addressPrefix);
175+
}
176+
177+
export function cidrRangeEqual(range1: CidrRange | undefined, range2: CidrRange | undefined): boolean {
178+
if (range1 === undefined && range2 === undefined) {
179+
return true;
180+
}
181+
if (range1 === undefined || range2 === undefined) {
182+
return false;
183+
}
184+
return range1.addressPrefix === range2.addressPrefix && range1.prefixLen === range2.prefixLen;
185+
}
186+
187+
export function cidrRangeMessageToCidrRange(message: CidrRange__Output): CidrRange {
188+
return {
189+
addressPrefix: message.address_prefix,
190+
prefixLen: message.prefix_len?.value ?? 0
191+
};
192+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Types and function from https://stackoverflow.com/a/72059390/159388, with modifications
18+
type ElementType<A> = A extends ReadonlyArray<infer T> ? T | undefined : never;
19+
20+
type ElementsOfAll<Inputs, R extends ReadonlyArray<unknown> = []> = Inputs extends readonly [infer F, ...infer M] ? ElementsOfAll<M, [...R, ElementType<F>]> : R;
21+
22+
type CartesianProduct<Inputs> = ElementsOfAll<Inputs>[];
23+
24+
/**
25+
* Get the cross product or Cartesian product of a list of groups. The
26+
* implementation is copied, with some modifications, from
27+
* https://stackoverflow.com/a/72059390/159388.
28+
* @param sets A list of groups of elements
29+
* @returns A list of all possible combinations of one element from each group
30+
* in sets. Empty groups will result in undefined in that slot in each
31+
* combination.
32+
*/
33+
export function crossProduct<Sets extends ReadonlyArray<ReadonlyArray<unknown>>>(sets: Sets): CartesianProduct<Sets> {
34+
/* The input is an array of arrays, and the expected output is an array of
35+
* each possible combination of one element each of the input arrays, with
36+
* the exception that if one of the input arrays is empty, each combination
37+
* gets [undefined] in that slot.
38+
*
39+
* At each step in the reduce call, we start with the cross product of the
40+
* first N groups, and the next group. For each combation, for each element
41+
* of the next group, extend the combination with that element.
42+
*
43+
* The type assertion at the end is needed because TypeScript doesn't track
44+
* the types well enough through the reduce calls to see that the result has
45+
* the expected type.
46+
*/
47+
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>;
48+
}

packages/grpc-js-xds/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import * as round_robin_lb from './lb-policy-registry/round-robin';
3131
import * as typed_struct_lb from './lb-policy-registry/typed-struct';
3232
import * as pick_first_lb from './lb-policy-registry/pick-first';
3333

34+
export { XdsServer } from './server';
35+
3436
/**
3537
* Register the "xds:" name scheme with the @grpc/grpc-js library.
3638
*/

0 commit comments

Comments
 (0)