diff --git a/Makefile b/Makefile index 9d57ac5..e7afb3a 100644 --- a/Makefile +++ b/Makefile @@ -28,5 +28,11 @@ chrome: prepare cp -f ${MANIFEST_C} ${MANIFEST} zip -9j ${BUILDDIR}${NAME}-${VERSION_C}.zip -j src/* + rm -rf ${BUILDDIR}${NAME}-${VERSION_C} + cp -f ${MANIFEST_C} ${MANIFEST} + # zip -9j ${BUILDDIR}${NAME}-${VERSION_C}.zip -j src/* + mkdir -p ${BUILDDIR}${NAME}-${VERSION_C}-unpacked-chrome + cp -r src/* ${BUILDDIR}${NAME}-${VERSION_C}-unpacked-chrome/ + clean: rm -rf ${BUILDDIR} diff --git a/README.md b/README.md index 2fbd619..0d761c5 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,21 @@ https://chrome.google.com/webstore/detail/ipvfoo/ecanpcehffngcegjmadlcijfolapgga --> ## Add to Firefox -https://addons.mozilla.org/addon/ipvfoo/ +https://addons.mozilla.org/addon/ipvfoo/ ## Add to Edge -https://microsoftedge.microsoft.com/addons/detail/ipvfoo/dphnkggpaicipkljebciobedeiaiofod +https://microsoftedge.microsoft.com/addons/detail/ipvfoo/dphnkggpaicipkljebciobedeiaiofod *(You can also run the Chrome version on Edge, as they are identical.)* ## Safari? IPvFoo cannot be [ported to Safari](https://github.com/pmarks-net/ipvfoo/issues/39) because the `webRequest` API does not report IP addresses. In theory, a Safari extension could do its own DNS lookups over HTTPS, but such behavior is beyond the scope of IPvFoo. + +## troubleshooting +### ipv4-only sites are shown as ipv6 when using nat64 +set 'NAT64 Prefix' in the extension options + diff --git a/src/background.js b/src/background.js index 56ac370..f210ef1 100644 --- a/src/background.js +++ b/src/background.js @@ -51,11 +51,7 @@ const TAB_DEAD = 2; // RequestFilter for webRequest events. const FILTER_ALL_URLS = { urls: [""] }; -// Distinguish IP address and domain name characters. -// Note that IP6_CHARS must not match "beef.de" -const IP4_CHARS = /^[0-9.]+$/; -const IP6_CHARS = /^[0-9A-Fa-f]*:[0-9A-Fa-f:.]*$/; -const DNS_CHARS = /^[0-9A-Za-z._-]+$/; + const SECONDS = 1000; // to milliseconds @@ -318,7 +314,7 @@ class TabInfo extends SaveableEntry { this.save(); } - addDomain(domain, addr, flags) { + addDomain(domain, addr, flags, nat64addr = "") { let d = this.domains[domain]; if (!d) { // Limit the number of domains per page, to avoid wasting RAM. @@ -327,7 +323,7 @@ class TabInfo extends SaveableEntry { return; } d = this.domains[domain] = - new DomainInfo(this, domain, addr || "(lost)", flags); + new DomainInfo(this, domain, addr || "(lost)", flags, nat64addr); d.countUp(); } else { const oldAddr = d.addr; @@ -360,14 +356,19 @@ class TabInfo extends SaveableEntry { let tooltip = ""; for (const [domain, d] of Object.entries(this.domains)) { if (domain == this.mainDomain) { - pattern = d.addrVersion(); + let [addrVer, _] = d.addrVersion(); + pattern = addrVer; + if (IS_MOBILE) { tooltip = d.addr; // Limited tooltip space on Android. } else { + tooltip = `${d.addr}\n${NAME_VERSION}`; } } else { - switch (d.addrVersion()) { + let [addrVer, _] = d.addrVersion(); + + switch (addrVer) { case "4": has4 = true; break; case "6": has6 = true; break; } @@ -430,12 +431,14 @@ class TabInfo extends SaveableEntry { const tuples = [mainTuple]; for (const domain of domains) { const d = this.domains[domain]; + let [addrVer, _] = d.addrVersion(); if (domain == mainTuple[0]) { mainTuple[1] = d.addr; - mainTuple[2] = d.addrVersion(); + mainTuple[2] = addrVer; mainTuple[3] = d.flags; + mainTuple[4] = d.renderAddr(); } else { - tuples.push([domain, d.addr, d.addrVersion(), d.flags]); + tuples.push([domain, d.addr, addrVer, d.flags, d.renderAddr()]); } } return tuples; @@ -448,23 +451,30 @@ class TabInfo extends SaveableEntry { // Perhaps this.domains was cleared during the request's lifetime. return null; } - return [domain, d.addr, d.addrVersion(), d.flags]; + + let [addrVer, _] = d.addrVersion(); + return [domain, d.addr, addrVer, d.flags, d.renderAddr()]; } } class DomainInfo { tabInfo; domain; + addr; - flags; + nat64Addr; + nat64AddrBitsCIDR; + isNat64; + flags; count = 0; // count of active requests inhibitZero = false; - constructor(tabInfo, domain, addr, flags) { + constructor(tabInfo, domain, addr, flags, nat64addr = "") { this.tabInfo = tabInfo; this.domain = domain; this.addr = addr; + this.getNat64Addr(nat64addr) this.flags = flags; } @@ -478,15 +488,56 @@ class DomainInfo { return new DomainInfo(tabInfo, domain, addr, flags); } + renderAddr() { + let [ver, nat64] = this.addrVersion(this.addr) + this.isNat64 = nat64 + if (ver === "4" && !nat64) { + if (options["ipv4Format"] !== "dotDecimal") { + let parseV4 = parseIPv4WithCidr(this.addr) + return renderIPv4(parseV4.addr); + } + + } + + if (this.isNat64) { + let bits = parseIPv6WithCIDR(this.addr) + return renderIPv6(bits.addr, true) + } + return this.addr + + } + + getNat64Addr(addr = "") { + if (addr === "") { + this.nat64Addr = options['nat64Prefix']; + } else { + this.nat64Addr = addr + } + + this.nat64AddrBitsCIDR = parseIPv6WithCIDR(this.nat64Addr, 96); + let [_, nat64] = this.addrVersion(this.addr) + this.isNat64 = nat64 + } + + + + // In theory, we should be using a full-blown subnet parser/matcher here, // but let's keep it simple and stick with text for now. addrVersion() { if (this.addr) { - if (/^64:ff9b::/.test(this.addr)) return "4"; // RFC6052 - if (this.addr.indexOf(".") >= 0) return "4"; - if (this.addr.indexOf(":") >= 0) return "6"; + if (this.addr.indexOf(".") >= 0) return ["4", false]; + + + let [isValidV6, problem] = isValidIPv6Addr(this.addr); + debugLog(problem) + + if (isValidV6) { + if (inAddrRange(parseIPv6WithCIDR(this.addr, -1, true), this.nat64AddrBitsCIDR)) return ["4", true]; // RFC6052 + return ["6", false]; + } } - return "?"; + return ["?", false]; } async countUp() { @@ -530,6 +581,8 @@ class RequestInfo extends SaveableEntry { if (!this.domain) { continue; // still waiting for onResponseStarted } + + tabInfo.addDomain(this.domain, null, 0); } if (Object.keys(this.tabIdToBorn).length == 0) { diff --git a/src/common.js b/src/common.js index a4e6393..0ff3ef5 100644 --- a/src/common.js +++ b/src/common.js @@ -24,6 +24,14 @@ const FLAG_CONNECTED = 0x8; const FLAG_WEBSOCKET = 0x10; const FLAG_NOTWORKER = 0x20; // from a tab, not a service worker + +// Distinguish IP address and domain name characters. +// Note that IP6_CHARS must not match "beef.de" +const IP4_CHARS = /^[0-9.]+$/; +const IP6_CHARS = /^[0-9A-Fa-f]*:[0-9A-Fa-f:.]*$/; +const DNS_CHARS = /^[0-9A-Za-z._-]+$/; + + // Returns an Object with no default properties. function newMap() { return Object.create(null); @@ -150,6 +158,9 @@ function drawSprite(ctx, size, targets, sources) { const DEFAULT_OPTIONS = { regularColorScheme: "darkfg", incognitoColorScheme: "lightfg", + nat64Prefix: "64:ff9b::/96", + nat64Format: "followV4", + ipv4Format: "dotDecimal", }; let _watchOptionsFunc = null; @@ -204,3 +215,345 @@ function setOptions(newOptions) { chrome.storage.sync.set(toSet); return true; // caller should wait for watchOptions() } + + + + +function setNibbleAtPosition(bigInt, nibble, bitPosition) { + let nibbleValue = BigInt(parseInt(nibble, 16)); + + let mask = ~(BigInt(0xF) << BigInt(bitPosition)); + bigInt = bigInt & mask; + + bigInt = bigInt | (nibbleValue << BigInt(bitPosition)); + + return bigInt; +} + +function setByteAtPosition(bigInt, byte, bitPosition) { + let byteValue = BigInt(parseInt(byte, 16)); + + let mask = ~(BigInt(0xFF) << BigInt(bitPosition)); + bigInt = bigInt & mask; + + bigInt = bigInt | (byteValue << BigInt(bitPosition)); + + return bigInt; +} + + +function inAddrRange(addr, nat64Addr) { + try { + let mask = (BigInt(1) << BigInt(128 - nat64Addr.cidr)) - BigInt(1); + addr.addr = addr.addr & ~mask; + + + nat64Addr.addr = nat64Addr.addr & ~mask; + + return addr.addr === nat64Addr.addr; + } catch (error) { + debugLog(error) + return false; + } +} + +function isValidIPv6Addr(addrMaybeCIDR) { + + let [addr, cidr] = addrMaybeCIDR.split('/'); + + if (addr === '') { + return [false, "Address is empty"] + } + + // you need at least 2 colons for a v6 addr, '::' + const colons = countOccurrences(addr, ":") + if (colons < 2) { + return [false, "Too few separators"] + } + + if (!IP6_CHARS.test(addr)) { + return [false, "Invalid characters"] + } + + + + if (addr[addr.length -1] === ':' && addr[addr.length -2] !== ':') { + return [false, "Can't end with a single separator"] + } + + let hextetLength = 0; + let colonsSeen = 0; + let doubleColon = false; + for (let i = addr.length - 1; i >= 0; i--) { + if (addr[i] !== ':') { + hextetLength += 1; + colonsSeen = 0; + } else { + hextetLength = 0; + colonsSeen += 1; + } + if (hextetLength > 4) { + return [false, "Can't have more then 4 character between a separator"] + } + + + if (colonsSeen === 2) { + if (doubleColon) { + return [false, "Can't have 2 '::' compressions in one address"] + } else { + doubleColon = true + } + } + + if (colonsSeen > 2) { + return [false, "Can't have more then 3 separators in a row"] + } + } + + if ((!doubleColon) && (colons < 7)) { + return [false, "Can't have less then 8 hextets without a '::' compression"] + } + + if (countOccurrences(addrMaybeCIDR, "/") === 1) { + if (addrMaybeCIDR.endsWith("/")) { + return [false, "Can't have empty CIDR"] + } + + if (parseInt(cidr, 10) < 0 || parseInt(cidr, 10) > 128) { + return [false, "Invalid CIDR, range is 0 - 128 inclusive"] + } + + } else if (countOccurrences(addrMaybeCIDR, "/") > 1) { + return [false, "Can't have more then 1 CIDR"] + } + + return [true, null] +} + +function countOccurrences(string, substring) { + return string.split(substring).length - 1; +} + + + + +function parseIPv4WithCidr(addressWithCIDR, defaultCIDR = -1) { + let [addressSTR, cidrSTR] = addressWithCIDR.split('/'); + + let ipv4BigInt = BigInt(0); + let octets = addressSTR.split('.').map(Number); + + for (let i = 0; i < 4; i++) { + ipv4BigInt = (ipv4BigInt << BigInt(8)) | BigInt(octets[i]); + } + + let cidr = cidrSTR ? parseInt(cidrSTR, 10) : defaultCIDR; + + + return { addr: ipv4BigInt, cidr: cidr }; +} + +function parseIPv6WithCIDR(addressWithCIDR, defaultCIDR = -1, isKnownValid = false) { + let [addressSTR, cidrSTR] = addressWithCIDR.split('/'); + let addr = BigInt(0); + let bitPos = 0; + let colonHexRemaining = 16; + let colonsSeen = 0; + + if (!isKnownValid) { + let [isValid, problem] = isValidIPv6Addr(addressSTR); + if (!isValid) { + debugLog(problem) + throw new Error('not_ipv6') + } + } + + + + let colons = countOccurrences(addressSTR, ":") + let doubleSkip = 16 * (8 - colons) + + + + for (let i = addressSTR.length - 1; i >= 0; i--) { + if (colonsSeen >= 2) { + bitPos += doubleSkip + } else if (colonsSeen === 1) { + bitPos += colonHexRemaining; + colonHexRemaining = 16; + } + if (addressSTR[i] !== ':') { + colonsSeen = 0 + addr = setNibbleAtPosition(addr, addressSTR[i], bitPos); + bitPos += 4; + colonHexRemaining -= 4; + } else { + colonsSeen += 1; + } + } + + + + let cidr = cidrSTR ? parseInt(cidrSTR, 10) : defaultCIDR; + + + return { addr: addr, cidr: cidr }; +} + + +function renderIPv4(addr, format = options["ipv4Format"]) { + + if (format === "dotDecimal") { + return renderIPv4DotDecimal(addr); + + } else if (format === "octetHex") { + return renderIPv4Hex(addr, 2); + + } else if (format === "singleBlockHex") { + return renderIPv4Hex(addr, 8, true, "shouldnotsee", "0x"); + + } else if (format === "ipv6Like") { + return renderIPv4Hex(addr, 4, true, ":"); + + } + + + return renderIPv4DotDecimal(addr) +} + +function renderIPv4DotDecimal(addr) { + let ipv4 = [] + + for (let i = 3; i >= 0; i--) { + let mask = (BigInt(1) << BigInt(8)) - BigInt(1); + + let oct = addr >> BigInt(i * 8); + oct = oct & mask + + ipv4.push(oct) + } + + return ipv4.join(".") + +} + + +function renderIPv4Hex(bigInt, groupSize, removeLeading0s = false, joiner = ":", prepend = "", append = "") { + let ipv4Bits = BigInt(bigInt) + + + let hex = ipv4Bits.toString(16).padStart(8, '0'); + + let ipv4Parts = []; + for (let i = 0; i < 8 / groupSize; i++) { + ipv4Parts.push(hex.substr(i * groupSize, groupSize)); + } + + if (removeLeading0s) { + ipv4Parts = ipv4Parts.map(group => group.replace(/^0+/, '') || '0'); + } + + + let ipv4Addr = ipv4Parts.join(joiner); + + if (prepend !== "") { + ipv4Addr = prepend + ipv4Addr + } + + if (append !== "") { + ipv4Addr = ipv4Addr + append + } + + return ipv4Addr; +} + +function renderIPv6(bigInt, nat64 = false) { + let ipv6Bits = BigInt(bigInt) + let ipv4Bits = BigInt(bigInt) + + let addrMask = (BigInt(1) << BigInt(32)) - BigInt(1); + + let ipv4Format = ""; + let changeV4Format = true; + + + if (nat64) { + debugLog("nat64 format: ", options["nat64Format"]) + if (options["nat64Format"] === "followV4") { + ipv4Format = options["ipv4Format"]; + ipv6Bits = ipv6Bits & ~addrMask + ipv4Bits = ipv4Bits & addrMask + } else if (options["nat64Format"] === "ipv6Hex") { + ipv4Format = ""; + changeV4Format = false; + } else if (options["nat64Format"] === "dotDecimal") { + ipv4Format = "dotDecimal"; + ipv6Bits = ipv6Bits & ~addrMask + ipv4Bits = ipv4Bits & addrMask + } + } + let shouldFormatNat64 = nat64 && changeV4Format; + + + let hex = ipv6Bits.toString(16).padStart(32, '0'); + + let ipv6Parts = []; + for (let i = 0; i < 8; i++) { + ipv6Parts.push(hex.substr(i * 4, 4)); + } + + ipv6Parts = ipv6Parts.map(group => group.replace(/^0+/, '') || '0'); + + let zeroStart = -1; + let zeroLength = 0; + let bestZeroStart = -1; + let bestZeroLength = 0; + + for (let i = 0; i < ipv6Parts.length; i++) { + if (ipv6Parts[i] === '0') { + if (zeroStart === -1) { + zeroStart = i; + } + zeroLength++; + } else { + if (zeroLength > bestZeroLength) { + bestZeroStart = zeroStart; + bestZeroLength = zeroLength; + } + zeroStart = -1; + zeroLength = 0; + } + } + + if (zeroLength > bestZeroLength) { + bestZeroStart = zeroStart; + bestZeroLength = zeroLength; + } + + if (bestZeroLength > 1) { + ipv6Parts.splice(bestZeroStart, bestZeroLength, ''); + } + + let ipv6Addr = ipv6Parts.join(':'); + + if (ipv6Addr.startsWith(':')) { + ipv6Addr = ':' + ipv6Addr; + } + + if (ipv6Addr.endsWith(':')) { + if (!(ipv6Parts.length -1 === 6 && shouldFormatNat64 )) { + ipv6Addr = ipv6Addr + ':'; + } + } + + if (shouldFormatNat64) { + let ipv4 = renderIPv4(ipv4Bits, ipv4Format); + ipv6Addr += ipv4 + } + + + + return ipv6Addr; +} + + diff --git a/src/options.html b/src/options.html index 4722893..42e47a2 100644 --- a/src/options.html +++ b/src/options.html @@ -20,23 +20,78 @@ + + @@ -80,7 +332,7 @@

Icon color scheme

Regular tabs: