Skip to content

Commit

Permalink
feat: alternative ips support (#243)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Kolárik <martin@kolarik.sk>
  • Loading branch information
alexey-yarmosh and MartinKolarik authored Aug 9, 2024
1 parent 35635a3 commit 02f57ff
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 6 deletions.
1 change: 1 addition & 0 deletions config/default.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
api: {
host: 'wss://api.globalping.io',
httpHost: 'https://api.globalping.io/v1',
},
update: {
releaseUrl: 'https://data.jsdelivr.com/v1/packages/gh/jsdelivr/globalping-probe/resolved',
Expand Down
1 change: 1 addition & 0 deletions config/development.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
api: {
host: 'ws://localhost:3000',
httpHost: 'http://localhost:3000/v1',
},
update: {
interval: 10,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@
"test:mocha:dev:update": "PRUNE_OLD_SNAPSHOTS=1 UPDATE_EXISTING_SNAPSHOTS=1 npm run test:mocha:dev",
"test:mocha:dev:updateNoPrune": "UPDATE_EXISTING_SNAPSHOTS=1 npm run test:mocha:dev",
"test:e2e": "npm run test:e2e:cases && npm run test:e2e:docker && npm run test:e2e:run",
"test:e2e:docker": "docker build -t globalping-probe-e2e . && docker build --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 -t globalping-api-e2e test/e2e/globalping",
"test:e2e:cases": "git clone https://github.com/jsdelivr/globalping.git test/e2e/globalping; cd test/e2e/globalping && git add . && git reset --hard && git pull --force && npm install; cd ../../../; for f in config/e2e-api-*; do cp \"$f\" \"test/e2e/globalping/config/${f#config/e2e-api-}\"; done;",
"test:e2e:docker": "docker build -t globalping-probe-e2e . && docker build --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 -t globalping-api-e2e test/e2e/globalping",
"test:e2e:run": "cd test/e2e/globalping; TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha --config ./.mocharc.e2e.cjs; cd ../../../"
},
"lint-staged": {
Expand Down
62 changes: 62 additions & 0 deletions src/helper/alt-ips-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import os from 'node:os';
import config from 'config';
import _ from 'lodash';
import { scopedLogger } from '../lib/logger.js';
import got, { RequestError } from 'got';

const logger = scopedLogger('api:connect:alt-ips-handler');

export const apiConnectAltIpsHandler = async ({ token, socketId, ip }: { token: string, socketId: string, ip: string }): Promise<void> => {
const allIps = [ ip ];
const addresses = _(os.networkInterfaces())
.values()
.filter((int): int is os.NetworkInterfaceInfo[] => !!int)
.flatten()
.uniqBy('address')
.filter(address => !address.internal)
.filter(address => !address.address.startsWith('fe80:')) // filter out link-local addresses
.filter(address => !address.address.startsWith('169.254.')) // filter out link-local addresses
.value();

const results = await Promise.allSettled(addresses.map(({ address, family }) => sendToken(address, family === 'IPv6' ? 6 : 4, token, socketId)));

results.forEach((result) => {
if (result.status === 'fulfilled') {
allIps.push(result.value);
} else {
if (!(result.reason instanceof RequestError)) {
logger.error(result.reason);
} else if (result.reason.response?.statusCode !== 400) {
logger.error(result.reason.message);
}
}
});

const uniqIps = _(allIps).uniq().value();

if (uniqIps.length === 1) {
logger.info(`IP address of the probe: ${uniqIps[0]}`);
} else {
logger.info(`IP addresses of the probe: ${uniqIps.join(', ')}`);
}
};

const sendToken = async (ip: string, dnsLookupIpVersion: 4 | 6, token: string, socketId: string) => {
const httpHost = config.get<string>('api.httpHost');
const response = await got.post<{ ip: string }>(`${httpHost}/alternative-ip`, {
localAddress: ip,
dnsLookupIpVersion,
json: {
token,
socketId,
},
retry: {
limit: 1,
methods: [ 'POST' ],
statusCodes: [ 504 ],
},
responseType: 'json',
});

return response.body.ip;
};
2 changes: 1 addition & 1 deletion src/helper/api-connect-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ProbeLocation } from '../types.js';
import { getDnsServers } from '../lib/dns.js';
import { getStatusManager } from '../lib/status-manager.js';

const logger = scopedLogger('api:connect');
const logger = scopedLogger('api:connect:location');

export const apiConnectLocationHandler = (socket: Socket) => async (data: ProbeLocation): Promise<void> => {
logger.info(`Connected from ${data.city}, ${data.country}, ${data.continent} (${data.network}, ASN: ${data.asn}, lat: ${data.latitude} long: ${data.longitude}).`);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/log-adoption-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { scopedLogger } from './logger.js';

const logger = scopedLogger('adoption-code');

export const logAdoptionCode = (code: string) => {
export const logAdoptionCode = (data: { code: string }) => {
logger.warn(`
,,
__ o-°°|\\_____/)
(___()'\`; Your adoption code is: ${code} \\_/|_) )
(___()'\`; Your adoption code is: ${data.code} \\_/|_) )
/, /\` \\ __ /
\\\\"--\\\\ (_/ (_/
`);
Expand Down
4 changes: 3 additions & 1 deletion src/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { loadAll as loadAllDeps } from './lib/dependencies.js';
import { scopedLogger } from './lib/logger.js';
import { initErrorHandler } from './helper/api-error-handler.js';
import { apiConnectLocationHandler } from './helper/api-connect-handler.js';
import { apiConnectAltIpsHandler } from './helper/alt-ips-handler.js';
import { dnsCmd, DnsCommand } from './command/dns-command.js';
import { pingCmd, PingCommand } from './command/ping-command.js';
import { traceCmd, TracerouteCommand } from './command/traceroute-command.js';
Expand Down Expand Up @@ -110,6 +111,7 @@ function connect () {
.on('disconnect', errorHandler.handleDisconnect)
.on('connect_error', errorHandler.connectError)
.on('api:connect:location', apiConnectLocationHandler(socket))
.on('api:connect:alt-ips-token', apiConnectAltIpsHandler)
.on('probe:measurement:request', (data: MeasurementRequest) => {
const status = statusManager.getStatus();

Expand Down Expand Up @@ -141,7 +143,7 @@ function connect () {
}
});
})
.on('probe:adoption:code', (data: { code: string }) => logAdoptionCode(data.code));
.on('probe:adoption:code', logAdoptionCode);

process.on('SIGTERM', () => {
logger.debug('SIGTERM received.');
Expand Down
4 changes: 4 additions & 0 deletions test/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as chai from 'chai';
import nock from 'nock';

import chaiSnapshot from './plugins/snapshot/index.js';

Expand All @@ -18,6 +19,9 @@ export const mochaHooks = {
}, { threshold: 100 });
}).catch(console.error);
}

nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
},
afterAll () {
if (Number(process.env['PRUNE_OLD_SNAPSHOTS'])) {
Expand Down
109 changes: 109 additions & 0 deletions test/unit/helper/alt-ips-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as td from 'testdouble';
import sinon from 'sinon';
import nock from 'nock';
import { expect } from 'chai';

import type { apiConnectAltIpsHandler as apiConnectAltIpsHandlerSrc } from '../../../src/helper/alt-ips-handler.js';

describe('apiConnectAltIpsHandler', async () => {
const networkInterfaces = sinon.stub();
let apiConnectAltIpsHandler: typeof apiConnectAltIpsHandlerSrc;

before(async () => {
await td.replaceEsm('node:os', {}, {
networkInterfaces,
});

({ apiConnectAltIpsHandler } = await import('../../../src/helper/alt-ips-handler.js'));
});

beforeEach(() => {
networkInterfaces.returns({
lo: [
{
address: '127.0.0.1',
netmask: '255.0.0.0',
family: 'IPv4',
mac: '00:00:00:00:00:00',
internal: true,
cidr: '127.0.0.1/8',
},
{
address: '::1',
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
family: 'IPv6',
mac: '00:00:00:00:00:00',
internal: true,
cidr: '::1/128',
scopeid: 0,
},
],
ens5: [
{
address: '172.31.43.80',
netmask: '255.255.240.0',
family: 'IPv4',
mac: '0a:ab:82:5a:50:d1',
internal: false,
cidr: '172.31.43.80/20',
},
{
address: '172.31.43.80',
netmask: '255.255.240.0',
family: 'IPv4',
mac: '0a:ab:82:5a:50:d1',
internal: false,
cidr: '172.31.43.80/20',
},
{
address: '2a05:d016:174:7b28:f47b:e6:3307:fab6',
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
family: 'IPv6',
mac: '0a:ab:82:5a:50:d1',
internal: false,
cidr: '2a05:d016:174:7b28:f47b:e6:3307:fab6/128',
scopeid: 0,
},
{
address: 'fe80::8ab:82ff:fe5a:50d1',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: '0a:ab:82:5a:50:d1',
internal: false,
cidr: 'fe80::8ab:82ff:fe5a:50d1/64',
scopeid: 2,
},
],
});
});

afterEach(() => {
nock.cleanAll();
});

after(() => {
td.reset();
});

it('should send alt ip request through valid addresses', async () => {
const reqs = [];
const nockRequest = nock('https://api.globalping.io/v1').persist()
.post('/alternative-ip', (body) => {
expect(body).to.deep.equal({ token: 'token', socketId: 'socketId' });
return true;
}).reply(200, function () {
reqs.push(this.req);
});

await apiConnectAltIpsHandler({
token: 'token',
socketId: 'socketId',
ip: '3.3.3.3',
});

expect(reqs.length).to.equal(2);
expect(reqs[0].options.localAddress).to.equal('172.31.43.80');
expect(reqs[1].options.localAddress).to.equal('2a05:d016:174:7b28:f47b:e6:3307:fab6');
expect(nockRequest.isDone()).to.equal(true);
});
});
6 changes: 5 additions & 1 deletion wallaby.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function wallaby () {
'test/plugins/**/*',
'test/utils.ts',
'test/hooks.ts',
'test/setup.ts',
'test/snapshots/**/*.json',
'package.json',
],
Expand All @@ -20,7 +21,10 @@ export default function wallaby () {
],
setup (w) {
const path = require('path');
w.testFramework.addFile(path.resolve(process.cwd(), 'test/hooks.js'));
w.testFramework.files.unshift(path.resolve(process.cwd(), 'test/hooks.js'));
w.testFramework.files.unshift(path.resolve(process.cwd(), 'test/setup.js'));
const mocha = w.testFramework;
mocha.timeout(5000);
},
env: {
type: 'node',
Expand Down

0 comments on commit 02f57ff

Please sign in to comment.