Group Mullvad exit nodes by country
This commit is contained in:
@@ -35,12 +35,19 @@ function displayHostName(hostName, dnsName) {
|
||||
return shortDnsName(dnsName) || host || "Unknown"
|
||||
}
|
||||
|
||||
function isMullvadPeer(peer) {
|
||||
var hostName = String((peer && peer.HostName) || "").toLowerCase()
|
||||
var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase()
|
||||
return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1
|
||||
}
|
||||
|
||||
function osIcon(os) {
|
||||
var value = String(os || "").toLowerCase()
|
||||
if (value === "linux") return ""
|
||||
if (value === "macos" || value === "ios") return ""
|
||||
if (value === "windows") return ""
|
||||
if (value === "android") return ""
|
||||
if (value === "mullvad") return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -57,16 +64,113 @@ function peerFromStatus(id, peer) {
|
||||
id: id,
|
||||
HostName: displayHostName(peer.HostName, peer.DNSName),
|
||||
DNSName: cleanDnsName(peer.DNSName),
|
||||
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
||||
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
||||
TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []),
|
||||
Online: peer.Online === true,
|
||||
OS: String(peer.OS || ""),
|
||||
Tags: peer.Tags || [],
|
||||
ExitNodeOption: peer.ExitNodeOption === true,
|
||||
ExitNode: peer.ExitNode === true
|
||||
ExitNode: peer.ExitNode === true,
|
||||
Mullvad: isMullvadPeer(peer)
|
||||
}
|
||||
}
|
||||
|
||||
function sliceTableColumn(line, start, end) {
|
||||
var text = String(line || "")
|
||||
if (start < 0 || start >= text.length) return ""
|
||||
if (end < 0) return text.substring(start).trim()
|
||||
return text.substring(start, Math.min(end, text.length)).trim()
|
||||
}
|
||||
|
||||
function parseExitNodeList(raw) {
|
||||
var lines = String(raw || "").split(/\r?\n/)
|
||||
var header = ""
|
||||
var headerIndex = -1
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (/^\s*IP\s+HOSTNAME\s+COUNTRY\s+CITY\s+STATUS\s*$/.test(lines[i])) {
|
||||
header = lines[i]
|
||||
headerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headerIndex === -1) return []
|
||||
|
||||
var ipStart = header.indexOf("IP")
|
||||
var hostStart = header.indexOf("HOSTNAME")
|
||||
var countryStart = header.indexOf("COUNTRY")
|
||||
var cityStart = header.indexOf("CITY")
|
||||
var statusStart = header.indexOf("STATUS")
|
||||
var byHost = {}
|
||||
|
||||
for (var j = headerIndex + 1; j < lines.length; j++) {
|
||||
var line = lines[j]
|
||||
if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue
|
||||
|
||||
var ip = sliceTableColumn(line, ipStart, hostStart)
|
||||
var host = sliceTableColumn(line, hostStart, countryStart)
|
||||
var country = sliceTableColumn(line, countryStart, cityStart)
|
||||
var city = sliceTableColumn(line, cityStart, statusStart)
|
||||
var status = sliceTableColumn(line, statusStart, -1)
|
||||
if (host.indexOf(".mullvad.ts.net") === -1) continue
|
||||
|
||||
byHost[host] = {
|
||||
id: "mullvad:" + host,
|
||||
HostName: host,
|
||||
DNSName: host,
|
||||
DisplayName: (city && city !== "Any" ? city + ", " : "") + country,
|
||||
TailscaleIPs: ip ? [ip] : [],
|
||||
TailscaleIPv6: [],
|
||||
Online: true,
|
||||
OS: "mullvad",
|
||||
Tags: [],
|
||||
ExitNodeOption: true,
|
||||
ExitNode: status !== "" && status !== "-",
|
||||
Mullvad: true,
|
||||
Country: country,
|
||||
City: city,
|
||||
Status: status
|
||||
}
|
||||
}
|
||||
|
||||
var result = []
|
||||
for (var hostName in byHost) result.push(byHost[hostName])
|
||||
result.sort(function(a, b) {
|
||||
var countryCompare = String(a.Country).localeCompare(String(b.Country))
|
||||
if (countryCompare !== 0) return countryCompare
|
||||
return String(a.DisplayName).localeCompare(String(b.DisplayName))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function mullvadCountryOptions(nodes) {
|
||||
var byCountry = {}
|
||||
var values = Array.isArray(nodes) ? nodes : []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var node = values[i] || {}
|
||||
if (node.Mullvad !== true) continue
|
||||
var country = String(node.Country || "").trim()
|
||||
if (country === "") continue
|
||||
var current = byCountry[country]
|
||||
if (!current || node.City === "Any") {
|
||||
var option = {}
|
||||
for (var key in node) option[key] = node[key]
|
||||
option.id = "mullvad-country:" + country
|
||||
option.DisplayName = country
|
||||
option.Country = country
|
||||
option.MullvadCountry = true
|
||||
byCountry[country] = option
|
||||
}
|
||||
}
|
||||
|
||||
var result = []
|
||||
for (var name in byCountry) result.push(byCountry[name])
|
||||
result.sort(function(a, b) {
|
||||
return String(a.Country).localeCompare(String(b.Country))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function parseStatus(raw) {
|
||||
var text = String(raw || "").trim()
|
||||
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
|
||||
@@ -83,6 +187,7 @@ function parseStatus(raw) {
|
||||
for (var id in rawPeers) {
|
||||
var peer = rawPeers[id] || {}
|
||||
var normalized = peerFromStatus(id, peer)
|
||||
if (normalized.Mullvad) continue
|
||||
if (normalized.Online) {
|
||||
peers.push(normalized)
|
||||
if (normalized.ExitNodeOption) exitNodes.push(normalized)
|
||||
@@ -155,7 +260,10 @@ if (typeof module !== "undefined") {
|
||||
displayHostName: displayHostName,
|
||||
osIcon: osIcon,
|
||||
accountLabel: accountLabel,
|
||||
isMullvadPeer: isMullvadPeer,
|
||||
peerFromStatus: peerFromStatus,
|
||||
parseExitNodeList: parseExitNodeList,
|
||||
mullvadCountryOptions: mullvadCountryOptions,
|
||||
parseStatus: parseStatus,
|
||||
parseAccounts: parseAccounts
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user