ip-asn-map 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/map.ts ADDED
@@ -0,0 +1,188 @@
1
+ import type { AsInfo, IpInfo, Numbers } from './types.ts';
2
+ import { parseIPv4, parseIPv6, stringifyIP4, stringifyIP6 } from './utils.ts';
3
+
4
+ export class IpMap {
5
+
6
+ buffer: ArrayBuffer;
7
+ bytes: Uint8Array;
8
+ view: DataView;
9
+
10
+ readonly ip4Count: number;
11
+ readonly ip6Count: number;
12
+ readonly asOffset: number;
13
+
14
+ readonly ip6View: DataView;
15
+ readonly ip4View: DataView;
16
+ readonly ip6AsView: DataView;
17
+ readonly ip4AsView: DataView;
18
+ readonly asView: DataView;
19
+
20
+ static version = 1;
21
+
22
+ /**
23
+ * Constructs an IpMap instance using the provided ArrayBuffer with the previously generated binary data.
24
+ * @param buffer
25
+ */
26
+ constructor(buffer: ArrayBuffer) {
27
+
28
+ this.buffer = buffer;
29
+ const bytes = this.bytes = new Uint8Array(buffer);
30
+ const view = this.view = new DataView(buffer);
31
+
32
+ new ArrayBuffer()
33
+
34
+ if (String.fromCharCode(...bytes.slice(0, 6)) != 'ip-map') throw new Error('Not an ip-asn-map buffer');
35
+ const [version, num6, num4, asOffset, bufSize] = [0, 1, 2, 3, 4].map(i => view.getUint32(8 + 4 * i)) as Numbers<5>;
36
+ if (version != IpMap.version) throw new Error(`Expected file version ${IpMap.version}, got ${version}`);
37
+ if (buffer.byteLength != bufSize) throw new Error(`Expected file size ${bufSize }, got ${buffer.byteLength}`);
38
+
39
+ this.ip4Count = num4;
40
+ this.ip6Count = num6;
41
+ this.asOffset = asOffset;
42
+
43
+ this.ip6View = new DataView(buffer, 32, num6 * 16);
44
+ this.ip4View = new DataView(buffer, this.ip6View.byteOffset + this.ip6View.byteLength, num4 * 4);
45
+ this.ip6AsView = new DataView(buffer, this.ip4View.byteOffset + this.ip4View.byteLength, num6 * 4);
46
+ this.ip4AsView = new DataView(buffer, this.ip6AsView.byteOffset + this.ip6AsView.byteLength, num4 * 4);
47
+ this.asView = new DataView(buffer, this.asOffset, buffer.byteLength - this.asOffset);
48
+
49
+ if (this.ip4AsView.byteOffset + this.ip4AsView.byteLength != this.asView.byteOffset) throw new Error('Something *really* went wrong');
50
+
51
+ }
52
+
53
+ private getAsAtPos(pos: number): AsInfo {
54
+ const asNum = this.asView.getUint32(pos);
55
+ const ccInt = this.asView.getUint16(pos + 4);
56
+ const descLen = this.asView.getUint16(pos + 6);
57
+ const descAt = this.asOffset + pos + 8;
58
+ const desc = !descLen ? null : String.fromCharCode(...this.bytes.slice(descAt, descAt + descLen));
59
+ return {
60
+ number: asNum,
61
+ country: ccInt ? String.fromCharCode((ccInt >> 8) & 255, ccInt & 255) : null,
62
+ description: desc,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Looks up an IPv4 address in the little-endian uint32 representation (byte order as written)
68
+ * @param needle
69
+ */
70
+ findV4(needle: number): IpInfo<4> {
71
+
72
+ if (needle < 0 || needle > 0xffff_ffff) {
73
+ throw new Error('Invalid IPv4 integer');
74
+ }
75
+
76
+ const view = this.ip4View;
77
+
78
+ let low = 0;
79
+ let high = this.ip4Count - 1;
80
+
81
+ while (high - low > 1) {
82
+ const at = Math.round(low + (high - low) * 0.5);
83
+ const addr = view.getUint32(at * 4);
84
+ if (addr > needle) {
85
+ high = at;
86
+ }
87
+ else {
88
+ low = at;
89
+ }
90
+ }
91
+
92
+ const first = view.getUint32(low * 4);
93
+ const next = view.getUint32(high * 4);
94
+ const last = next - 1;
95
+
96
+ const as = this.getAsAtPos(this.ip4AsView.getUint32(4 * low));
97
+
98
+ return {
99
+ family: 4,
100
+ ip: stringifyIP4(needle),
101
+ int: needle,
102
+ as,
103
+ range: {
104
+ family: 4,
105
+ first: stringifyIP4(first),
106
+ last: stringifyIP4(last),
107
+ firstInt: first,
108
+ lastInt: last,
109
+ size: next - first,
110
+ index: low,
111
+ },
112
+ }
113
+
114
+ }
115
+
116
+ /**
117
+ * Looks up an IPv6 address in the little-endian uint128 representation (byte order as written)
118
+ * @param needle
119
+ */
120
+ findV6(needle: bigint): IpInfo<6> {
121
+
122
+ if (needle < 0n || needle >= (1n << 128n)) {
123
+ throw new Error('Invalid IPv6 bigint');
124
+ }
125
+
126
+ const view = this.ip6View;
127
+
128
+ let low = 0;
129
+ let high = this.ip6Count - 1;
130
+
131
+ const readAddr = (pos: number) => {
132
+ const a = view.getBigUint64(pos * 16);
133
+ const b = view.getBigUint64(pos * 16 + 8);
134
+ return (a << 64n) | b;
135
+ }
136
+
137
+ while (high - low > 1) {
138
+ const at = Math.round(low + (high - low) * 0.5);
139
+ const addr = readAddr(at);
140
+ if (addr > needle) {
141
+ high = at;
142
+ }
143
+ else {
144
+ low = at;
145
+ }
146
+ }
147
+
148
+ const first = readAddr(low);
149
+ const next = readAddr(high);
150
+ const last = next - 1n;
151
+
152
+ const as = this.getAsAtPos(this.ip6AsView.getUint32(4 * low));
153
+
154
+ return {
155
+ family: 6,
156
+ ip: stringifyIP6(needle),
157
+ int: needle,
158
+ as,
159
+ range: {
160
+ family: 6,
161
+ first: stringifyIP6(first),
162
+ last: stringifyIP6(last),
163
+ firstInt: first,
164
+ lastInt: last,
165
+ size: next - first,
166
+ index: low,
167
+ },
168
+ }
169
+
170
+ }
171
+
172
+ /**
173
+ * Looks up an IP address in the form 1.2.3.4 or a:b::c:d, returns {@link IpInfo}
174
+ * @param address
175
+ */
176
+ lookup(address: string): IpInfo {
177
+ if (address.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)) {
178
+ return this.findV4(parseIPv4(address));
179
+ }
180
+ else if (address.match(/^([0-9a-f]*):/)) {
181
+ return this.findV6(parseIPv6(address));
182
+ }
183
+ else {
184
+ throw new Error('Unexpected address format');
185
+ }
186
+ }
187
+
188
+ }
package/src/test.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { stat, writeFile } from 'node:fs/promises';
2
+ import { IpMap } from './map.ts';
3
+ import { loadIpMap, parseIp2AsnGzip } from './buffer.ts';
4
+ import * as assert from 'node:assert';
5
+ import type { Tuple } from './types.ts';
6
+ import * as process from 'node:process';
7
+
8
+ const gz = `data/ip2asn-combined.tsv.gz`;
9
+ const bin = `data/ip-map.bin`;
10
+
11
+ if (!await stat(gz).catch(() => null)) {
12
+ console.log('Fetching', gz);
13
+ const res = await fetch('https://iptoasn.com/data/ip2asn-combined.tsv.gz');
14
+ if (!res.ok) throw new Error(res.statusText);
15
+ await writeFile(gz, Buffer.from(await res.arrayBuffer()));
16
+ }
17
+
18
+ const { createInterface } = await import('node:readline');
19
+ const { createReadStream } = await import('node:fs');
20
+ const { createGunzip } = await import('node:zlib');
21
+ const stream = createReadStream(gz);
22
+ const gunzip = createGunzip();
23
+ stream.pipe(gunzip);
24
+ const rl = createInterface(gunzip);
25
+ const tsv = await Array.fromAsync(rl).then(arr => arr.filter(s => s.trim()));
26
+
27
+ let t = performance.now();
28
+ if (!await stat(bin).catch(() => null)) {
29
+ const buf = await parseIp2AsnGzip(gz);
30
+ console.log('parse:', performance.now() - t, 'ms');
31
+ await writeFile(bin, buf);
32
+ }
33
+
34
+ const map = await loadIpMap(bin);
35
+
36
+ const checkN = 10000;
37
+
38
+ console.log('IPv4 random test');
39
+ for (let i = 0; i < checkN; i++) {
40
+ const int = Math.round(Math.random() * 0xffff_ffff);
41
+ const res = map.findV4(int);
42
+ assert.equal(res.int, int);
43
+ assert.ok(res.range.firstInt <= int);
44
+ assert.ok(res.range.lastInt >= int);
45
+ }
46
+
47
+ console.log('IPv6 random test');
48
+ for (let i = 0; i < checkN; i++) {
49
+ let int = BigInt(2000 + Math.round(Math.random() * 0xcff));
50
+ for (let c = 0; c < 7; c++) {
51
+ int = (int << 16n) | BigInt(Math.round(Math.random() * 0xffff));
52
+ }
53
+ const res = map.findV6(int);
54
+ assert.equal(res.int, int);
55
+ assert.ok(res.range.firstInt <= int);
56
+ assert.ok(res.range.lastInt >= int);
57
+ }
58
+
59
+ console.log('Random TSV lines test');
60
+ for (let i = 0; i < checkN; i++) {
61
+ const line = tsv[Math.floor(Math.random() * tsv.length)]!;
62
+ const [start, _end, asNumStr, ccStr, desc] = line.split('\t') as Tuple<string, 5>;
63
+ const res = map.lookup(start);
64
+ assert.equal(res.as.country, ccStr == 'None' || ccStr == 'Unknown' ? null : ccStr);
65
+ assert.equal(String(res.as.number), asNumStr);
66
+ assert.equal(res.as.description, asNumStr == '0' ? null : desc);
67
+ }
68
+
69
+ if (!process.argv.includes('bench')) process.exit();
70
+
71
+ const benchN = 1_000_000;
72
+
73
+ t = performance.now();
74
+ for (let i = 0; i < benchN; i++) {
75
+ const int = Math.round(Math.random() * 0xffff_ffff);
76
+ map.findV4(int);
77
+ }
78
+ {
79
+ const ms = performance.now() - t;
80
+ console.log('IPv4 random int', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per lookup');
81
+ }
82
+
83
+ t = performance.now();
84
+ for (let i = 0; i < benchN; i++) {
85
+ let int = BigInt(2000 + Math.round(Math.random() * 0xcff));
86
+ for (let c = 0; c < 7; c++) {
87
+ int = (int << 16n) | BigInt(Math.round(Math.random() * 0xffff));
88
+ }
89
+ map.findV6(int);
90
+ }
91
+ {
92
+ const ms = performance.now() - t;
93
+ console.log('IPv6 random int', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per lookup');
94
+ }
95
+
96
+ t = performance.now();
97
+ for (let i = 0; i < benchN; i++) {
98
+ const line = tsv[Math.floor(Math.random() * tsv.length)]!;
99
+ map.lookup(line.split('\t')[0]!);
100
+ }
101
+ {
102
+ const ms = performance.now() - t;
103
+ console.log('Random TSV row', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per parse + lookup');
104
+ }
package/src/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ export interface TupleOf<T> {
2
+ 0: [];
3
+ 1: [T];
4
+ 2: [T, T];
5
+ 3: [T, T, T];
6
+ 4: [T, T, T, T];
7
+ 5: [T, T, T, T, T];
8
+ 6: [T, T, T, T, T, T];
9
+ 7: [T, T, T, T, T, T, T];
10
+ 8: [T, T, T, T, T, T, T, T];
11
+ 9: [T, T, T, T, T, T, T, T, T];
12
+ 10: [T, T, T, T, T, T, T, T, T, T];
13
+ 11: [T, T, T, T, T, T, T, T, T, T, T];
14
+ 12: [T, T, T, T, T, T, T, T, T, T, T, T];
15
+ }
16
+
17
+ export type AnyTupleSize = keyof TupleOf<any>;
18
+
19
+ export type Tuple<T, N extends AnyTupleSize> = TupleOf<T>[N];
20
+
21
+ export type Numbers<N extends AnyTupleSize> = Tuple<number, N>;
22
+
23
+ /**
24
+ * Information about the Autonomous System for the IP range.
25
+ */
26
+ export interface AsInfo {
27
+ /**
28
+ * AS number. If 0, IP range is not assigned.
29
+ */
30
+ number: number;
31
+ /**
32
+ * 2-digit country code, where present
33
+ */
34
+ country: string | null;
35
+ /**
36
+ * AS description string, where present
37
+ */
38
+ description: string | null;
39
+ }
40
+
41
+ export interface AsRecord extends AsInfo {
42
+ offset: number;
43
+ }
44
+
45
+ /**
46
+ * IP range info
47
+ */
48
+ export interface RangeInfo<Int extends number | bigint> {
49
+
50
+ family: 4 | 6;
51
+
52
+ /**
53
+ * First address in string form.
54
+ */
55
+ first: string;
56
+ /**
57
+ * Last address in string form.
58
+ */
59
+ last: string;
60
+
61
+ firstInt: Int;
62
+ lastInt: Int;
63
+
64
+ /**
65
+ * Number of addresses.
66
+ */
67
+ size: Int;
68
+
69
+ /**
70
+ * Range index in the map, for debugging purposes.
71
+ */
72
+ index: number;
73
+
74
+ }
75
+
76
+ export type IpFamily = 4 | 6;
77
+ export type IntRepr<F extends IpFamily> = F extends 4 ? number : bigint;
78
+
79
+ /**
80
+ * IP address lookup result
81
+ */
82
+ export interface IpInfo<F extends IpFamily = IpFamily> {
83
+ family: F;
84
+ ip: string;
85
+ int: IntRepr<F>;
86
+ as: AsInfo;
87
+ range: RangeInfo<IntRepr<F>>;
88
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,38 @@
1
+ export function parseIPv6(addr: string) {
2
+ const [headStr, tailStr] = addr.split('::') as [string, string];
3
+ const headChunks = headStr.split(':').filter(s => !!s).map(c => c.padStart(4, '0'));
4
+ const tailChunks = tailStr?.split(':').filter(s => !!s).map(c => c.padStart(4, '0')) ?? [];
5
+ const full = [
6
+ '0x',
7
+ ...headChunks,
8
+ '0'.repeat(4 * (8 - (headChunks.length + tailChunks.length))),
9
+ ...tailChunks,
10
+ ];
11
+ return BigInt(full.join(''));
12
+ }
13
+
14
+ export function parseIPv4(addr: string) {
15
+ return parseInt(
16
+ addr.split('.').map(s => parseInt(s, 10).toString(16).padStart(2, '0')).join(''),
17
+ 16,
18
+ );
19
+ }
20
+
21
+ export function stringifyIP4(int: number) {
22
+ return [
23
+ (int >>> 24) & 255,
24
+ (int >>> 16) & 255,
25
+ (int >>> 8) & 255,
26
+ int & 255
27
+ ].join('.');
28
+ }
29
+
30
+
31
+ export function stringifyIP6(int: bigint) {
32
+ const chunks: number[] = [];
33
+ for (let s = 0; s < 8; s++) {
34
+ chunks.push(Number(int >> BigInt(s * 16) & 0xffffn));
35
+ }
36
+ while (chunks.length && !chunks[0]) chunks.shift();
37
+ return chunks.toReversed().map(c => c.toString(16)).join(':') + (chunks.length < 8 ? '::' : '');
38
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "nodenext",
4
+ "moduleResolution": "nodenext",
5
+ "target": "esnext",
6
+ "strict": true,
7
+ "sourceMap": true,
8
+ "declaration": true,
9
+ "noEmitOnError": false,
10
+ "allowSyntheticDefaultImports": true,
11
+ "strictNullChecks": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "erasableSyntaxOnly": true,
14
+ "allowImportingTsExtensions": true,
15
+ "rewriteRelativeImportExtensions": true,
16
+ "rootDir": "./src",
17
+ "outDir": "./dist",
18
+ "types": ["node"]
19
+ },
20
+ "include": [
21
+ "./src/*.ts",
22
+ ]
23
+ }