Skip to content

Commit

Permalink
[Data] Add IPv6 support by the data plugin (#99837) (#99896)
Browse files Browse the repository at this point in the history
  • Loading branch information
dokmic authored May 12, 2021
1 parent 4dab3f3 commit a93d77a
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 206 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ search: {
})[];
InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError;
InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError;
Ipv4Address: typeof Ipv4Address;
IpAddress: typeof IpAddress;
isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig;
isNumberType: (agg: import("../common").AggConfig) => boolean;
isStringType: (agg: import("../common").AggConfig) => boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ search: {
})[];
InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError;
InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError;
Ipv4Address: typeof Ipv4Address;
IpAddress: typeof IpAddress;
isNumberType: (agg: import("../common").AggConfig) => boolean;
isStringType: (agg: import("../common").AggConfig) => boolean;
isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/data/common/search/aggs/buckets/ip_range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { KBN_FIELD_TYPES } from '../../../../common';
import { BaseAggParams } from '../types';

const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', {
defaultMessage: 'IPv4 Range',
defaultMessage: 'IP Range',
});

export enum IP_RANGE_TYPES {
Expand Down
85 changes: 38 additions & 47 deletions src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,47 @@
import { CidrMask } from './cidr_mask';

describe('CidrMask', () => {
test('should throw errors with invalid CIDR masks', () => {
expect(
() =>
// @ts-ignore
new CidrMask()
).toThrowError();

expect(() => new CidrMask('')).toThrowError();
expect(() => new CidrMask('hello, world')).toThrowError();
expect(() => new CidrMask('0.0.0.0')).toThrowError();
expect(() => new CidrMask('0.0.0.0/0')).toThrowError();
expect(() => new CidrMask('0.0.0.0/33')).toThrowError();
expect(() => new CidrMask('256.0.0.0/32')).toThrowError();
expect(() => new CidrMask('0.0.0.0/32/32')).toThrowError();
expect(() => new CidrMask('1.2.3/1')).toThrowError();
expect(() => new CidrMask('0.0.0.0/123d')).toThrowError();
describe('constructor', () => {
it.each`
mask
${''}
${'hello, world'}
${'0.0.0.0'}
${'0.0.0.0/33'}
${'256.0.0.0/32'}
${'0.0.0.0/32/32'}
${'0.0.0.0/123d'}
${'::1'}
${'::1/129'}
${'fffff::/128'}
${'ffff::/128/128'}
`('should throw an error on $mask', ({ mask }) => {
expect(() => new CidrMask(mask)).toThrowError();
});
});

test('should correctly grab IP address and prefix length', () => {
let mask = new CidrMask('0.0.0.0/1');
expect(mask.initialAddress.toString()).toBe('0.0.0.0');
expect(mask.prefixLength).toBe(1);

mask = new CidrMask('128.0.0.1/31');
expect(mask.initialAddress.toString()).toBe('128.0.0.1');
expect(mask.prefixLength).toBe(31);
describe('toString', () => {
it.each`
mask | expected
${'192.168.1.1/24'} | ${'192.168.1.1/24'}
${'192.168.257/32'} | ${'192.168.1.1/32'}
${'ffff:0:0:0:0:0:0:0/128'} | ${'ffff::/128'}
`('should format $mask as $expected', ({ mask, expected }) => {
expect(new CidrMask(mask).toString()).toBe(expected);
});
});

test('should calculate a range of IP addresses', () => {
let mask = new CidrMask('0.0.0.0/1');
let range = mask.getRange();
expect(range.from.toString()).toBe('0.0.0.0');
expect(range.to.toString()).toBe('127.255.255.255');

mask = new CidrMask('1.2.3.4/2');
range = mask.getRange();
expect(range.from.toString()).toBe('0.0.0.0');
expect(range.to.toString()).toBe('63.255.255.255');

mask = new CidrMask('67.129.65.201/27');
range = mask.getRange();
expect(range.from.toString()).toBe('67.129.65.192');
expect(range.to.toString()).toBe('67.129.65.223');
});

test('toString()', () => {
let mask = new CidrMask('.../1');
expect(mask.toString()).toBe('0.0.0.0/1');

mask = new CidrMask('128.0.0.1/31');
expect(mask.toString()).toBe('128.0.0.1/31');
describe('getRange', () => {
it.each`
mask | from | to
${'0.0.0.0/0'} | ${'0.0.0.0'} | ${'255.255.255.255'}
${'0.0.0.0/1'} | ${'0.0.0.0'} | ${'127.255.255.255'}
${'1.2.3.4/2'} | ${'0.0.0.0'} | ${'63.255.255.255'}
${'67.129.65.201/27'} | ${'67.129.65.192'} | ${'67.129.65.223'}
${'::/1'} | ${'::'} | ${'7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'}
${'8000::/1'} | ${'8000::'} | ${'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'}
`('should return $from-$to for $mask', ({ mask, from, to }) => {
expect(new CidrMask(mask).getRange()).toEqual({ from, to });
});
});
});
72 changes: 45 additions & 27 deletions src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,60 @@
* Side Public License, v 1.
*/

import { Ipv4Address } from '../../utils';

const NUM_BITS = 32;

function throwError(mask: string) {
throw Error('Invalid CIDR mask: ' + mask);
}
import ipaddr from 'ipaddr.js';
import { IpAddress } from '../../utils';

export class CidrMask {
public readonly initialAddress: Ipv4Address;
public readonly prefixLength: number;
private address: number[];
private netmask: number;

constructor(mask: string) {
const splits = mask.split('/');
if (splits.length !== 2) {
throwError(mask);
}
this.initialAddress = new Ipv4Address(splits[0]);
this.prefixLength = Number(splits[1]);
if (isNaN(this.prefixLength) || this.prefixLength < 1 || this.prefixLength > NUM_BITS) {
throwError(mask);
constructor(cidr: string) {
try {
const [address, netmask] = ipaddr.parseCIDR(cidr);

this.address = address.toByteArray();
this.netmask = netmask;
} catch {
throw Error('Invalid CIDR mask: ' + cidr);
}
}

public getRange() {
const variableBits = NUM_BITS - this.prefixLength;
// eslint-disable-next-line no-bitwise
const fromAddress = ((this.initialAddress.valueOf() >> variableBits) << variableBits) >>> 0; // >>> 0 coerces to unsigned
const numAddresses = Math.pow(2, variableBits);
private getBroadcastAddress() {
/* eslint-disable no-bitwise */
const netmask = (1n << BigInt(this.address.length * 8 - this.netmask)) - 1n;
const broadcast = this.address.map((byte, index) => {
const offset = BigInt(this.address.length - index - 1) * 8n;
const mask = Number((netmask >> offset) & 255n);

return byte | mask;
});
/* eslint-enable no-bitwise */

return new IpAddress(broadcast).toString();
}

private getNetworkAddress() {
/* eslint-disable no-bitwise */
const netmask = (1n << BigInt(this.address.length * 8 - this.netmask)) - 1n;
const network = this.address.map((byte, index) => {
const offset = BigInt(this.address.length - index - 1) * 8n;
const mask = Number((netmask >> offset) & 255n) ^ 255;

return byte & mask;
});
/* eslint-enable no-bitwise */

return new IpAddress(network).toString();
}

getRange() {
return {
from: new Ipv4Address(fromAddress).toString(),
to: new Ipv4Address(fromAddress + numAddresses - 1).toString(),
from: this.getNetworkAddress(),
to: this.getBroadcastAddress(),
};
}

public toString() {
return this.initialAddress.toString() + '/' + this.prefixLength;
toString() {
return `${new IpAddress(this.address)}/${this.netmask}`;
}
}
2 changes: 1 addition & 1 deletion src/plugins/data/common/search/aggs/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histog
export { getDateHistogramMetaDataByDatatableColumn } from './get_date_histogram_meta';
export * from './date_interval_utils';
export * from './get_format_with_aggs';
export * from './ipv4_address';
export * from './ip_address';
export * from './prop_filter';
export * from './to_angular_json';
export * from './infer_time_zone';
67 changes: 67 additions & 0 deletions src/plugins/data/common/search/aggs/utils/ip_address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { IpAddress } from './ip_address';

describe('IpAddress', () => {
describe('constructor', () => {
it.each`
address
${''}
${'hello, world'}
${'256.0.0.0'}
${'-1.0.0.0'}
${Number.MAX_SAFE_INTEGER}
${'fffff::'}
${'ffff:0:0:0:0:0:0:0:0'}
`('should throw an error on $address', ({ address }) => {
expect(() => new IpAddress(address)).toThrowError();
});

it.each`
address | expected
${'192.168.257'} | ${'192.168.1.1'}
${2116932386} | ${'126.45.211.34'}
${'126.45.211.34'} | ${'126.45.211.34'}
${[126, 45, 211, 34]} | ${'126.45.211.34'}
${'ffff:0:0:0:0:0:0:0'} | ${'ffff::'}
${'ffff::'} | ${'ffff::'}
${[0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]} | ${'ffff::'}
`('should parse $address', ({ address, expected }) => {
expect(new IpAddress(address).toString()).toBe(expected);
});
});

describe('valueOf', () => {
it.each`
address | expected
${'0.0.0.0'} | ${'0'}
${'0.0.0.1'} | ${'1'}
${'126.45.211.34'} | ${'2116932386'}
${'ffff::'} | ${'340277174624079928635746076935438991360'}
`(
'should return $expected as a decimal representation of $address',
({ address, expected }) => {
expect(new IpAddress(address).valueOf().toString()).toBe(expected);
}
);
});

describe('toString()', () => {
it.each`
address | expected
${'0.000.00000.1'} | ${'0.0.0.1'}
${'192.168.257'} | ${'192.168.1.1'}
${'ffff:0:0:0:0:0:0:0'} | ${'ffff::'}
${'0:0:0:0:0:0:0:ffff'} | ${'::ffff'}
${'f:0:0:0:0:0:0:f'} | ${'f::f'}
`('should serialize $address as $expected', ({ address, expected }) => {
expect(new IpAddress(address).toString()).toBe(expected);
});
});
});
47 changes: 47 additions & 0 deletions src/plugins/data/common/search/aggs/utils/ip_address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import ipaddr, { IPv4, IPv6 } from 'ipaddr.js';

function isIPv6(value: IPv4 | IPv6): value is IPv6 {
return value.kind() === 'ipv6';
}

export class IpAddress {
private value: IPv4 | IPv6;

constructor(ipAddress: string | number | number[]) {
try {
this.value = Array.isArray(ipAddress)
? ipaddr.fromByteArray(ipAddress)
: ipaddr.parse(`${ipAddress}`);
} catch {
throw Error('Invalid IP address: ' + ipAddress);
}
}

toString() {
if (isIPv6(this.value)) {
return this.value.toRFC5952String();
}

return this.value.toString();
}

valueOf(): number | bigint {
const value = this.value
.toByteArray()
.reduce((result, octet) => result * 256n + BigInt(octet), 0n);

if (value > Number.MAX_SAFE_INTEGER) {
return value;
}

return Number(value);
}
}
54 changes: 0 additions & 54 deletions src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts

This file was deleted.

Loading

0 comments on commit a93d77a

Please sign in to comment.