diff --git a/package.json b/package.json index b5b9970d..7393c724 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ }, "dependencies": { "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", "dns-over-http-resolver": "^2.1.0", "err-code": "^3.0.1", "multiformats": "^11.0.0", @@ -175,8 +176,7 @@ "devDependencies": { "@types/varint": "^6.0.0", "aegir": "^38.1.0", - "sinon": "^15.0.0", - "util": "^0.12.3" + "sinon": "^15.0.0" }, "browser": { "./dist/src/resolvers/dns.js": "./dist/src/resolvers/dns.browser.js" diff --git a/src/convert.ts b/src/convert.ts index 17bc0d72..4df1f24a 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -15,6 +15,12 @@ import varint from 'varint' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import type { Multiaddr } from './index.js' +import { IpNet } from '@chainsafe/netmask' + +const ip4Protocol = getProtocol('ip4') +const ip6Protocol = getProtocol('ip6') +const ipcidrProtocol = getProtocol('ipcidr') /** * converts (serializes) addresses @@ -107,6 +113,23 @@ export function convertToBytes (proto: string | number, str: string): Uint8Array } } +export function convertToIpNet (multiaddr: Multiaddr): IpNet { + let mask: string | undefined + let addr: string | undefined + multiaddr.stringTuples().forEach(([code, value]) => { + if (code === ip4Protocol.code || code === ip6Protocol.code) { + addr = value + } + if (code === ipcidrProtocol.code) { + mask = value + } + }) + if (mask == null || addr == null) { + throw new Error('Invalid multiaddr') + } + return new IpNet(addr, mask) +} + const decoders = Object.values(bases).map((c) => c.decoder) const anybaseDecoder = (function () { let acc = decoders[0].or(decoders[1]) diff --git a/src/filter/multiaddr-filter.ts b/src/filter/multiaddr-filter.ts new file mode 100644 index 00000000..21cd1e19 --- /dev/null +++ b/src/filter/multiaddr-filter.ts @@ -0,0 +1,46 @@ +import type { IpNet } from '@chainsafe/netmask' +import { convertToIpNet } from '../convert.js' +import { multiaddr, Multiaddr, MultiaddrInput } from '../index.js' + +/** + * A utility class to determine if a Multiaddr contains another + * multiaddr. + * + * This can be used with ipcidr ranges to determine if a given + * multiaddr is in a ipcidr range. + * + * @example + * + * ```js + * import { multiaddr, MultiaddrFilter } from '@multiformats/multiaddr' + * + * const range = multiaddr('/ip4/192.168.10.10/ipcidr/24') + * const filter = new MultiaddrFilter(range) + * + * const input = multiaddr('/ip4/192.168.10.2/udp/60') + * console.info(filter.contains(input)) // true + * ``` + */ +export class MultiaddrFilter { + private readonly multiaddr: Multiaddr + private readonly netmask: IpNet + + public constructor (input: MultiaddrInput) { + this.multiaddr = multiaddr(input) + this.netmask = convertToIpNet(this.multiaddr) + } + + public contains (input: MultiaddrInput): boolean { + if (input == null) return false + const m = multiaddr(input) + let ip + for (const [code, value] of m.stringTuples()) { + if (code === 4 || code === 41) { + ip = value + break + } + } + if (ip === undefined) return false + return this.netmask.contains(ip) + } +} diff --git a/src/index.ts b/src/index.ts index 60b34603..33712354 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,6 +94,8 @@ export interface AbortOptions { export const resolvers = new Map() const symbol = Symbol.for('@multiformats/js-multiaddr/multiaddr') +export { MultiaddrFilter } from './filter/multiaddr-filter.js' + export interface Multiaddr { bytes: Uint8Array diff --git a/test/convert.spec.ts b/test/convert.spec.ts index 5d395c1e..c4638a45 100644 --- a/test/convert.spec.ts +++ b/test/convert.spec.ts @@ -2,6 +2,7 @@ import * as convert from '../src/convert.js' import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { multiaddr } from '../src/index.js' describe('convert', () => { it('handles ip4 buffers', () => { @@ -100,4 +101,22 @@ describe('convert', () => { const bytesOut = convert.convertToBytes(466, outcome) expect(bytesOut.toString()).to.equal(bytes.toString()) }) + + it('convertToIpNet ip4', function () { + const ipnet = convert.convertToIpNet(multiaddr('/ip4/192.0.2.0/ipcidr/24')) + expect(ipnet.toString()).equal('192.0.2.0/24') + }) + + it('convertToIpNet ip6', function () { + const ipnet = convert.convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/ipcidr/64')) + expect(ipnet.toString()).equal('2001:0db8:85a3:0000:0000:0000:0000:0000/64') + }) + + it('convertToIpNet not ipcidr', function () { + expect(() => convert.convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/tcp/64'))).to.throw() + }) + + it('convertToIpNet not ipv6', function () { + expect(() => convert.convertToIpNet(multiaddr('/dns6/foo.com/ipcidr/64'))).to.throw() + }) }) diff --git a/test/filter/multiaddr-filter.spec.ts b/test/filter/multiaddr-filter.spec.ts new file mode 100644 index 00000000..833df2e9 --- /dev/null +++ b/test/filter/multiaddr-filter.spec.ts @@ -0,0 +1,25 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { MultiaddrFilter, multiaddr, MultiaddrInput } from '../../src/index.js' + +describe('MultiaddrFilter', () => { + const cases: Array<[MultiaddrInput, MultiaddrInput, boolean]> = [ + ['/ip4/192.168.10.10/ipcidr/24', '/ip4/192.168.10.2/tcp/60', true], + [multiaddr('/ip4/192.168.10.10/ipcidr/24'), '/ip4/192.168.10.2/tcp/60', true], + [multiaddr('/ip4/192.168.10.10/ipcidr/24').bytes, '/ip4/192.168.10.2/tcp/60', true], + ['/ip4/192.168.10.10/ipcidr/24', '/ip4/192.168.10.2/udp/60', true], + ['/ip4/192.168.10.10/ipcidr/24', multiaddr('/ip4/192.168.11.2/tcp/60'), false], + ['/ip4/192.168.10.10/ipcidr/24', null, false], + ['/ip4/192.168.10.10/ipcidr/24', multiaddr('/ip4/192.168.11.2/udp/60').bytes, false], + ['/ip4/192.168.10.10/ipcidr/24', '/ip4/192.168.11.2/udp/60', false], + ['/ip4/192.168.10.10/ipcidr/24', '/ip6/2001:db8:3333:4444:5555:6666:7777:8888/tcp/60', false], + ['/ip6/2001:db8:3333:4444:5555:6666:7777:8888/ipcidr/60', '/ip6/2001:0db8:3333:4440:0000:0000:0000:0000/tcp/60', true], + ['/ip6/2001:db8:3333:4444:5555:6666:7777:8888/ipcidr/60', '/ip6/2001:0db8:3333:4450:0000:0000:0000:0000/tcp/60', false] + ] + + cases.forEach(([cidr, ip, result]) => { + it(`multiaddr filter cidr=${cidr} ip=${ip} result=${String(result)}`, function () { + expect(new MultiaddrFilter(cidr).contains(ip)).to.be.equal(result) + }) + }) +})