diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-02-11 17:13:11 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-02-12 17:02:49 +0100 |
| commit | 9a3a4ff5f6e8652f4126699ae3513f4c0a194662 (patch) | |
| tree | c3a2c81b3e44f6157d459ba2c8555f9e00c24195 | |
| parent | 85c7801a87e99af8e698a6847604510d7233cd7a (diff) | |
| download | mullvadvpn-9a3a4ff5f6e8652f4126699ae3513f4c0a194662.tar.xz mullvadvpn-9a3a4ff5f6e8652f4126699ae3513f4c0a194662.zip | |
Replace ip dependency with our own implementation
| -rw-r--r-- | gui/package-lock.json | 14 | ||||
| -rw-r--r-- | gui/package.json | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 26 | ||||
| -rw-r--r-- | gui/src/renderer/lib/ip.ts | 278 |
4 files changed, 290 insertions, 30 deletions
diff --git a/gui/package-lock.json b/gui/package-lock.json index e8c2721c45..64c8311241 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -18782,15 +18782,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "@types/ip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz", - "integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -26355,11 +26346,6 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", diff --git a/gui/package.json b/gui/package.json index c92a4a88d5..baf7717269 100644 --- a/gui/package.json +++ b/gui/package.json @@ -17,7 +17,6 @@ "d3-geo": "^1.12.1", "gettext-parser": "^4.0.3", "google-protobuf": "^4.0.0-rc.2", - "ip": "^1.1.5", "linux-app-list": "^1.0.1", "moment": "^2.24.0", "node-gettext": "^3.0.0", @@ -45,7 +44,6 @@ "@types/gettext-parser": "^4.0.0", "@types/google-protobuf": "^3.7.2", "@types/history": "^4.7.8", - "@types/ip": "^1.1.0", "@types/mkdirp": "^1.0.0", "@types/mocha": "^5.2.6", "@types/node": "^10.12.3", diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 3f840f4492..b24b849b5f 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -1,4 +1,3 @@ -import ip from 'ip'; import * as React from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; @@ -10,6 +9,7 @@ import { } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import consumePromise from '../../shared/promise'; +import { IpAddress } from '../lib/ip'; import { WgKeyState } from '../redux/settings/reducers'; import { StyledButtonCellGroup, @@ -575,21 +575,19 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { }; private addDnsAddress = async (address: string, confirmed?: boolean) => { - if (ip.isV4Format(address) || ip.isV6Format(address)) { - if (ip.isPublic(address) && !confirmed) { - this.setState({ publicDnsIpToConfirm: address }); + try { + const ipAddress = IpAddress.fromString(address); + + if (ipAddress.isLocal() || confirmed) { + await this.props.setDnsOptions({ + custom: this.props.dns.custom || this.state.showAddCustomDns, + addresses: [...this.props.dns.addresses, address], + }); + this.hideAddCustomDnsRow(); } else { - try { - await this.props.setDnsOptions({ - custom: this.props.dns.custom || this.state.showAddCustomDns, - addresses: [...this.props.dns.addresses, address], - }); - this.hideAddCustomDnsRow(); - } catch (_e) { - this.setState({ invalidDnsIp: true }); - } + this.setState({ publicDnsIpToConfirm: address }); } - } else { + } catch (e) { this.setState({ invalidDnsIp: true }); } }; diff --git a/gui/src/renderer/lib/ip.ts b/gui/src/renderer/lib/ip.ts new file mode 100644 index 0000000000..369d66a479 --- /dev/null +++ b/gui/src/renderer/lib/ip.ts @@ -0,0 +1,278 @@ +// Number of groups for each IP format +type IPv4Octets = [number, number, number, number]; +type IPv6Groups = [number, number, number, number, number, number, number, number]; + +// Number of bits in each group for each IP format +const IPv4OctetSize = 8; +const IPv6GroupSize = 16; + +// Abstract class representing an IP address +export abstract class IpAddress<G extends number[]> { + public constructor(public readonly groups: G) {} + + public abstract isLocal(): boolean; + + public static fromString(ip: string): IPv4Address | IPv6Address { + try { + return IPv4Address.fromString(ip); + } catch (e) { + return IPv6Address.fromString(ip); + } + } +} + +// Abstract class representing an IP range or subnet +export abstract class IpRange<G extends number[]> { + public constructor(public readonly groups: G, public readonly prefixSize: number) {} + + // Returns whether or not this subnet includes the privided IP + protected includes<T extends IpAddress<G>>(ip: T, groupSize: number): boolean { + return IpRange.match(groupSize, ip.groups, [this.groups, this.prefixSize]); + } + + // Matches each group of the ip/subnet from left to right to determine if they match + private static match( + groupSize: number, + [ipGroup, ...ipGroups]: number[], + [[subnetGroup, ...subnetGroups], prefixSize]: [number[], number], + ): boolean { + if (prefixSize >= groupSize) { + // If the current group is part of the prefix the only needed check is if they are equal + return ( + ipGroup === subnetGroup && + IPv4Range.match(groupSize, ipGroups, [subnetGroups, prefixSize - groupSize]) + ); + } else { + // If the group (or parts of the group) isn't part of the prefix the non-prefix part needs to + // be compared + // variableBits contains the maximum value that the non-prefix bits can have + const variableBits = getBitsMax(groupSize - prefixSize); + // Calculate smallest IP in the subnet + const subnetMin = subnetGroup & (getBitsMax(groupSize) - variableBits); + // Calculate greatest IP in the subnet + const subnetMax = subnetGroup | variableBits; + // Check if the provided ip is between subnetMin/-Max + return ipGroup >= subnetMin && ipGroup <= subnetMax; + } + } +} + +export class IPv4Address extends IpAddress<IPv4Octets> { + public constructor(octets: IPv4Octets) { + super(octets); + + // Ensure that each octets is the correct number of bits + if (octets.some((octets) => !isNumberOfBits(octets, IPv4OctetSize))) { + throw new Error(`Invalid ip: ${octets.join('.')}`); + } + } + + public isLocal(): boolean { + const localSubnets = [...IPV4_LAN_SUBNETS, IPV4_LOOPBACK_SUBNET]; + return localSubnets.some((subnet) => subnet.includes(this)); + } + + // Parses an ip address from a string of the quad-dotted format, e.g. 127.0.0.1 + public static fromString(ip: string): IPv4Address { + try { + const octets = IPv4Address.octetsFromString(ip); + return new IPv4Address(octets); + } catch (e) { + throw new Error(`Invalid ip: ${ip}`); + } + } + + public static octetsFromString(ip: string): IPv4Octets { + try { + const octets = ip.split('.'); + if (octets.every((octet) => /^\d{1,3}$/.test(octet))) { + const parsedOctets = octets.map((octet) => parseInt(octet, 10)); + if (IPv4Address.isIPv4Octets(parsedOctets)) { + return parsedOctets; + } + } + } catch (e) { + // no-op + } + + throw new Error(`Invalid ip: ${ip}`); + } + + public static isValid(ip: string): boolean { + try { + IPv4Address.fromString(ip); + return true; + } catch (e) { + return false; + } + } + + // Makes sure that the number of octets is correct and values where parsed correctly + private static isIPv4Octets(octets: number[]): octets is IPv4Octets { + return octets.length === 4 && octets.every((octet) => !isNaN(octet)); + } +} + +export class IPv4Range extends IpRange<IPv4Octets> { + public constructor(octets: IPv4Octets, prefixSize: number) { + super(octets, prefixSize); + + // Makes sure that the prefix is within the correct range + if (prefixSize < 0 || prefixSize > 32) { + throw new Error(`Invalid ip: ${octets.join('.')}/${prefixSize}`); + } + } + + public static fromString(subnet: string): IPv4Range { + try { + // In addition to parsing the ip the subnet-mask also needs to be parsed + const parts = subnet.split('/'); + if (/^\d{1,2}$/.test(parts[1])) { + const octets = IPv4Address.octetsFromString(parts[0]); + const prefixSize = parseInt(parts[1]); + return new IPv4Range(octets, prefixSize); + } + } catch (e) { + // no-op + } + + throw new Error(`Invalid ip: ${subnet}`); + } + + public includes(ip: IPv4Address): boolean { + return super.includes(ip, IPv4OctetSize); + } +} + +export class IPv6Address extends IpAddress<IPv6Groups> { + public constructor(groups: IPv6Groups) { + super(groups); + + // Ensure that each group is the correct number of bits + if (groups.some((group) => !isNumberOfBits(group, 16))) { + throw new Error(`Invalid ip: ${groups.join(':')}`); + } + } + + public isLocal(): boolean { + const localSubnets = [...IPV6_LAN_SUBNETS, IPV6_LOOPBACK_SUBNET]; + return localSubnets.some((subnet) => subnet.includes(this)); + } + + // Parses IPv6 addresses where the groups are seperated by ':' and supports shortened addresses. + public static fromString(ip: string): IPv6Address { + try { + const groups = IPv6Address.groupsFromString(ip); + return new IPv6Address(groups); + } catch (e) { + throw new Error(`Invalid ip: ${ip}`); + } + } + + public static groupsFromString(ip: string): IPv6Groups { + try { + // Split on shortening separator and make sure there's only one separator + const shortened = ip.split('::'); + if (shortened.length <= 2) { + // Split each part of the shortened address into groups and remove any empty groups, such as + // the one before the separator in ::1 + const parts = shortened.map((groups) => groups.split(':').filter((group) => group !== '')); + + let groups: string[]; + if (parts.length === 2) { + // If the address contained the shortening separator the parts are concatenated with empty + // groups in between + const shortened = Array(8 - parts[0].length - parts[1].length).fill(0x0); + groups = [...parts[0], ...shortened, ...parts[1]]; + } else { + // If it wasn't shortened all groups are used as is + groups = parts.flat(); + } + + if (groups.every((group) => /^[0-9a-fA-F]{1,4}$/.test(group))) { + const parsedGroups = groups.map((group) => parseInt(group, 16)); + + if (IPv6Address.isIPv6Groups(parsedGroups)) { + return parsedGroups; + } + } + } + } catch (e) { + // no-op + } + + throw new Error(`Invalid ip: ${ip}`); + } + + public static isValid(ip: string): boolean { + try { + IPv6Address.fromString(ip); + return true; + } catch (e) { + return false; + } + } + + // Makes sure that the number of groups is correct and values where parsed correctly + private static isIPv6Groups(groups: number[]): groups is IPv6Groups { + return groups.length === 8 && groups.every((group) => !isNaN(group)); + } +} + +export class IPv6Range extends IpRange<IPv6Groups> { + public constructor(groups: IPv6Groups, prefixSize: number) { + super(groups, prefixSize); + + // Makes sure that the prefix is within the correct range + if (prefixSize < 0 || prefixSize > 128) { + throw new Error(`Invalid subnet: ${groups.join(':')}/${prefixSize}`); + } + } + + public static fromString(subnet: string): IPv6Range { + try { + // In addition to parsing the ip the subnet-mask also needs to be parsed + const parts = subnet.split('/'); + if (/^\d{1,3}$/.test(parts[1])) { + const groups = IPv6Address.groupsFromString(parts[0]); + const prefixSize = parseInt(parts[1], 10); + return new IPv6Range(groups, prefixSize); + } + } catch (e) { + // no-op + } + + throw new Error(`Invalid subnet: ${subnet}`); + } + + public includes(ip: IPv6Address): boolean { + return super.includes(ip, IPv6GroupSize); + } +} + +// Returns the maximum value possible with the provided size +function getBitsMax(bits: number): number { + return Math.pow(2, bits) - 1; +} + +// Returns whether or not a number is possible to represent as an unsigned in of the provided size +function isNumberOfBits(value: number, bits: number): boolean { + return value >= 0 && value < Math.pow(2, bits); +} + +// IPv4 addresses reserved for local networks +const IPV4_LAN_SUBNETS = [ + new IPv4Range([10, 0, 0, 0], 8), + new IPv4Range([172, 16, 0, 0], 12), + new IPv4Range([192, 168, 0, 0], 16), + new IPv4Range([169, 254, 0, 0], 16), +]; + +// IPv6 addresses reserved for local networks +const IPV6_LAN_SUBNETS = [ + new IPv6Range([0xfe80, 0, 0, 0, 0, 0, 0, 0], 10), + new IPv6Range([0xfc00, 0, 0, 0, 0, 0, 0, 0], 7), +]; + +const IPV4_LOOPBACK_SUBNET = new IPv4Range([127, 0, 0, 0], 8); +const IPV6_LOOPBACK_SUBNET = new IPv6Range([0, 0, 0, 0, 0, 0, 0, 1], 128); |
