Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: highlight local network peers #1266

Merged
merged 16 commits into from
Oct 29, 2019
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
"i18next-icu": "^1.1.2",
"i18next-xhr-backend": "^3.2.0",
"internal-nav-helper": "^3.1.0",
"ip": "^1.1.5",
"ipfs-css": "^0.13.1",
"ipfs-geoip": "^3.0.0",
"ipfs-redux-bundle": "^5.1.2",
"ipld-explorer-components": "^1.5.1",
"is-binary": "^0.1.0",
"is-ipfs": "^0.6.1",
"memoizee": "^0.4.14",
"milliseconds": "^1.0.3",
"money-clip": "^3.0.1",
"multiaddr": "^7.1.0",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/en/peers.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"address": "Address",
"location": "Location",
"unknownLocation": "Unknown",
"localNetwork": "Local Network",
"nearby": "nearby",
"latency": "latency",
"bootstrapNode": "bootstrap node",
"viaRelay": "via <0>{node}</0>",
Expand Down
58 changes: 56 additions & 2 deletions src/bundles/peer-locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { getConfiguredCache } from 'money-clip'
import geoip from 'ipfs-geoip'
import PQueue from 'p-queue'
import HLRU from 'hashlru'
import Multiaddr from 'multiaddr'
import ms from 'milliseconds'
import ip from 'ip'
import memoizee from 'memoizee'

// After this time interval, we re-check the locations for each peer
// once again through PeerLocationResolver.
Expand Down Expand Up @@ -48,7 +51,8 @@ export default function (opts) {
'selectPeers',
'selectPeerLocations',
'selectBootstrapPeers',
(peers, locations = {}, bootstrapPeers) => peers && peers.map(peer => {
'selectIdentity',
(peers, locations = {}, bootstrapPeers, identity) => peers && peers.map(peer => {
const peerId = peer.peer.toB58String()
const locationObj = locations ? locations[peerId] : null
const location = toLocationString(locationObj)
Expand All @@ -61,6 +65,7 @@ export default function (opts) {
const address = peer.addr.toString()
const latency = parseLatency(peer.latency)
const notes = parseNotes(peer, bootstrapPeers)
const { isPrivate, isNearby } = isPrivateAndNearby(peer.addr, identity)

return {
peerId,
Expand All @@ -70,7 +75,9 @@ export default function (opts) {
connection,
address,
latency,
notes
notes,
isPrivate,
isNearby
}
})
)
Expand Down Expand Up @@ -114,6 +121,53 @@ const parseLatency = (latency) => {
return value
}

const getPublicIP = memoizee((identity) => {
if (!identity) return
hacdias marked this conversation as resolved.
Show resolved Hide resolved

for (const maddr of identity.addresses) {
try {
const addr = Multiaddr(maddr).nodeAddress()
if (!ip.isPrivate(addr.address)) {
return addr.address
}
} catch (_) {}
}
})

const isPrivateAndNearby = (maddr, identity) => {
const publicIP = getPublicIP(identity)
let isPrivate = false
let isNearby = false
let addr

if (!publicIP) {
return { isPrivate, isNearby }
}

try {
addr = maddr.nodeAddress()
} catch (_) {
autonome marked this conversation as resolved.
Show resolved Hide resolved
// Might explode if maddr does not have an IP or cannot be converted
// to a node address. This might happen if it's a relay. We do not print
// or handle the error, otherwise we would get perhaps thousands of logs.
return { isPrivate, isNearby }
}

// At this point, addr.address and publicIP must be valid IP addresses. Hence,
// none of the calls bellow for ip library should fail.
isPrivate = ip.isPrivate(addr.address)

if (addr.family === 4) {
isNearby = ip.cidrSubnet(`${publicIP}/24`).contains(addr.address)
} else if (addr.family === 6) {
isNearby = ip.cidrSubnet(`${publicIP}/48`).contains(addr.address) &&
!ip.cidrSubnet('fc00::/8').contains(addr.address)
// peerIP6 ∉ fc00::/8 to fix case of cjdns where IPs are not spatial allocated.
}

return { isPrivate, isNearby }
}

const parseNotes = (peer, bootstrapPeers) => {
const peerId = peer.peer.toB58String()
const addr = peer.addr
Expand Down
10 changes: 10 additions & 0 deletions src/bundles/peer-locations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ async function fakePeer (data = {}) {

const fakePeers = (count = 5) => Promise.all(Array(count).fill(0).map(fakePeer))

function createMockIdentityBundle () {
return {
name: 'identity',
selectIdentity: () => null
}
}

function createMockRouteBundle () {
return {
name: 'route',
Expand Down Expand Up @@ -138,6 +145,7 @@ function expectLocation (obj) {
it('should get locations for peers', async () => {
const store = composeBundlesRaw(
createMockRouteBundle(),
createMockIdentityBundle(),
createMockAppTimeBundle(),
createMockOnlineBundle(),
createReactorBundle(),
Expand Down Expand Up @@ -186,6 +194,7 @@ it('should get locations for peers', async () => {
it('should fail on non IPv4 address', async () => {
const store = composeBundlesRaw(
createMockRouteBundle(),
createMockIdentityBundle(),
createMockAppTimeBundle(),
createMockOnlineBundle(),
createReactorBundle(),
Expand Down Expand Up @@ -221,6 +230,7 @@ it('should fail on non IPv4 address', async () => {
it('should resolve alternative address for failed address lookup', async () => {
const store = composeBundlesRaw(
createMockRouteBundle(),
createMockIdentityBundle(),
createMockAppTimeBundle(),
createMockOnlineBundle(),
createReactorBundle(),
Expand Down
30 changes: 19 additions & 11 deletions src/peers/PeersTable/PeersTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,33 @@ export class PeersTable extends React.Component {
this.sort = this.sort.bind(this)
}

flagRenderer = (flagCode) => {
flagRenderer = (flagCode, isPrivate) => {
// Check if the OS is Windows to render the flags as SVGs
// Windows doesn't render the flags as emojis ¯\_(ツ)_/¯
const isWindows = window.navigator.appVersion.indexOf('Win') !== -1
return (
<span className='f4 pr2'>
{flagCode ? <CountryFlag code={flagCode} svg={isWindows} /> : '🌐'}
{isPrivate ? '🤝' : flagCode ? <CountryFlag code={flagCode} svg={isWindows} /> : '🌐'}
</span>
)
}

locationCellRenderer = ({ rowData }) => (
<span title={ rowData.location || this.props.t('unknownLocation')}>
{ this.flagRenderer(rowData.flagCode) }
{ rowData.location ? rowData.location : (
<span className='charcoal-muted fw4'>{this.props.t('unknownLocation')}</span>
) }
</span>
)
locationCellRenderer = ({ rowData }) => {
const location = rowData.isPrivate
? this.props.t('localNetwork')
: rowData.location
? rowData.isNearby
? <span>{rowData.location} <span className='charcoal-muted'>({this.props.t('nearby')})</span></span>
: rowData.location
: <span className='charcoal-muted fw4'>{this.props.t('unknownLocation')}</span>

return (
<span title={ rowData.location || this.props.t('unknownLocation')}>
{ this.flagRenderer(rowData.flagCode, rowData.isPrivate) }
{ location }
</span>
)
}

latencyCellRenderer = ({ cellData }) => {
const style = { width: '60px' }
Expand All @@ -57,7 +65,7 @@ export class PeersTable extends React.Component {
<Cid value={cellData} identicon />
)

notesCellRenderer = ({ cellData }) => {
notesCellRenderer = ({ cellData, rowData }) => {
if (!cellData) return

if (cellData.type === 'BOOTSTRAP_NODE') {
Expand Down