1 2
import { randomBytes } from 'crypto'
2 2
import { Errors } from 'ilp-packet'
3 2
import { create as createLogger } from '../common/log'
4 2
const log = createLogger('route-broadcaster')
5 2
import { find } from 'lodash'
6 2
import RoutingTable from './routing-table'
7 2
import ForwardingRoutingTable, { RouteUpdate } from './forwarding-routing-table'
8 2
import Accounts from './accounts'
9 2
import Config from './config'
10 2
import Peer from '../routing/peer'
11 2
import { canDragonFilter } from '../routing/dragon'
12 2
import { Relation, getRelationPriority } from '../routing/relation'
13 2
import {
14
  formatRoutingTableAsJson,
15
  formatRouteAsJson,
16
  formatForwardingRoutingTableAsJson
17
} from '../routing/utils'
18
import {
19
  Route,
20
  IncomingRoute
21
} from '../types/routing'
22
import reduct = require('reduct')
23 2
import { sha256, hmac } from '../lib/utils'
24
import {
25
  CcpRouteControlRequest,
26
  CcpRouteUpdateRequest
27
} from 'ilp-protocol-ccp'
28 2
const { BadRequestError } = Errors
29

30 2
export default class RouteBroadcaster {
31
  private deps: reduct.Injector
32
  // Local routing table, used for actually routing packets
33
  private localRoutingTable: RoutingTable
34
  // Master routing table, used for routes that we broadcast
35
  private forwardingRoutingTable: ForwardingRoutingTable
36

37
  private accounts: Accounts
38
  private config: Config
39

40
  private peers: Map<string, Peer>
41
  private localRoutes: Map<string, Route>
42
  private routingSecret: Buffer
43 2
  private untrackCallbacks: Map<string, () => void> = new Map()
44

45
  constructor (deps: reduct.Injector) {
46 2
    this.deps = deps
47 2
    this.localRoutingTable = deps(RoutingTable)
48 2
    this.forwardingRoutingTable = deps(ForwardingRoutingTable)
49 2
    this.accounts = deps(Accounts)
50 2
    this.config = deps(Config)
51

52 2
    if (this.config.routingSecret) {
53 2
      log.info('loaded routing secret from config.')
54 2
      this.routingSecret = Buffer.from(this.config.routingSecret, 'base64')
55
    } else {
56 2
      log.info('generated random routing secret.')
57 2
      this.routingSecret = randomBytes(32)
58
    }
59

60 2
    this.peers = new Map() // peerId:string -> peer:Peer
61 2
    this.localRoutes = new Map()
62
  }
63

64
  start () {
65 0
    this.reloadLocalRoutes()
66

67 0
    for (const accountId of this.accounts.getAccountIds()) {
68 0
      this.track(accountId)
69
    }
70
  }
71

72
  stop () {
73 0
    for (const accountId of this.peers.keys()) {
74 0
      this.remove(accountId)
75
    }
76
  }
77

78
  track (accountId: string) {
79 2
    if (this.untrackCallbacks.has(accountId)) {
80
      // Already tracked
81 0
      return
82
    }
83

84 2
    const plugin = this.accounts.getPlugin(accountId)
85

86 2
    const connectHandler = () => {
87 2
      if (!plugin.isConnected()) {
88
        // some plugins don't set `isConnected() = true` before emitting the
89
        // connect event, setImmediate has a good chance of working.
90 0
        log.error('(!!!) plugin emitted connect, but then returned false for isConnected, broken plugin. account=%s', accountId)
91 0
        setImmediate(() => this.add(accountId))
92
      } else {
93 0
        this.add(accountId)
94
      }
95
    }
96 2
    const disconnectHandler = () => {
97 0
      this.remove(accountId)
98
    }
99

100 2
    plugin.on('connect', connectHandler)
101 2
    plugin.on('disconnect', disconnectHandler)
102

103 2
    this.untrackCallbacks.set(accountId, () => {
104 2
      plugin.removeListener('connect', connectHandler)
105 2
      plugin.removeListener('disconnect', disconnectHandler)
106
    })
107

108 2
    this.add(accountId)
109
  }
110

111
  untrack (accountId: string) {
112 2
    this.remove(accountId)
113

114 2
    const callback = this.untrackCallbacks.get(accountId)
115

116 2
    if (callback) {
117 2
      callback()
118
    }
119
  }
120

121
  add (accountId: string) {
122 2
    const accountInfo = this.accounts.getInfo(accountId)
123

124
    let sendRoutes
125 2
    if (typeof accountInfo.sendRoutes === 'boolean') {
126 2
      sendRoutes = accountInfo.sendRoutes
127 2
    } else if (accountInfo.relation !== 'child') {
128 2
      sendRoutes = true
129
    } else {
130 2
      sendRoutes = false
131
    }
132

133
    let receiveRoutes
134 2
    if (typeof accountInfo.receiveRoutes === 'boolean') {
135 2
      receiveRoutes = accountInfo.receiveRoutes
136 2
    } else if (accountInfo.relation !== 'child') {
137 2
      receiveRoutes = true
138
    } else {
139 2
      receiveRoutes = false
140
    }
141

142 2
    if (!sendRoutes && !receiveRoutes) {
143 2
      log.warn('not sending/receiving routes for peer, set sendRoutes/receiveRoutes to override. accountId=%s', accountId)
144 2
      return
145
    }
146

147 2
    const existingPeer = this.peers.get(accountId)
148 2
    if (existingPeer) {
149
      // Every time we reconnect, we'll send a new route control message to make
150
      // sure they are still sending us routes.
151 0
      const receiver = existingPeer.getReceiver()
152

153 2
      if (receiver) {
154 0
        receiver.sendRouteControl()
155
      } else {
156 0
        log.warn('unable to send route control message, receiver object undefined. peer=%s', existingPeer)
157
      }
158

159 0
      return
160
    }
161

162 2
    const plugin = this.accounts.getPlugin(accountId)
163

164 2
    if (plugin.isConnected()) {
165 2
      log.trace('add peer. accountId=%s sendRoutes=%s receiveRoutes=%s', accountId, sendRoutes, receiveRoutes)
166 2
      const peer = new Peer({ deps: this.deps, accountId, sendRoutes, receiveRoutes })
167 2
      this.peers.set(accountId, peer)
168 2
      const receiver = peer.getReceiver()
169 2
      if (receiver) {
170 2
        receiver.sendRouteControl()
171
      }
172 2
      this.reloadLocalRoutes()
173
    }
174
  }
175

176
  remove (accountId: string) {
177 2
    const peer = this.peers.get(accountId)
178

179 2
    if (!peer) {
180 0
      return
181
    }
182

183 2
    const sender = peer.getSender()
184 2
    const receiver = peer.getReceiver()
185

186 2
    log.trace('remove peer. peerId=' + accountId)
187 2
    if (sender) {
188 2
      sender.stop()
189
    }
190

191
    // We have to remove the peer before calling updatePrefix on each of its
192
    // advertised prefixes in order to find the next best route.
193 2
    this.peers.delete(accountId)
194 2
    if (receiver) {
195 2
      for (let prefix of receiver.getPrefixes()) {
196 2
        this.updatePrefix(prefix)
197
      }
198
    }
199 2
    if (this.getAccountRelation(accountId) === 'child') {
200 0
      this.updatePrefix(this.accounts.getChildAddress(accountId))
201
    }
202
  }
203

204
  handleRouteControl (sourceAccount: string, routeControl: CcpRouteControlRequest) {
205 0
    const peer = this.peers.get(sourceAccount)
206

207 2
    if (!peer) {
208 0
      log.debug('received route control message from non-peer. sourceAccount=%s', sourceAccount)
209 0
      throw new BadRequestError('cannot process route control messages from non-peers.')
210
    }
211

212 0
    const sender = peer.getSender()
213

214 2
    if (!sender) {
215 0
      log.debug('received route control message from peer not authorized to receive routes from us (sendRoutes=false). sourceAccount=%s', sourceAccount)
216 0
      throw new BadRequestError('rejecting route control message, we are configured not to send routes to you.')
217
    }
218

219 0
    sender.handleRouteControl(routeControl)
220
  }
221

222
  handleRouteUpdate (sourceAccount: string, routeUpdate: CcpRouteUpdateRequest) {
223 2
    const peer = this.peers.get(sourceAccount)
224

225 2
    if (!peer) {
226 0
      log.debug('received route update from non-peer. sourceAccount=%s', sourceAccount)
227 0
      throw new BadRequestError('cannot process route update messages from non-peers.')
228
    }
229

230 2
    const receiver = peer.getReceiver()
231

232 2
    if (!receiver) {
233 0
      log.debug('received route update from peer not authorized to advertise routes to us (receiveRoutes=false). sourceAccount=%s', sourceAccount)
234 0
      throw new BadRequestError('rejecting route update, we are configured not to receive routes from you.')
235
    }
236

237
    // Apply import filters
238
    // TODO Route filters should be much more configurable
239
    // TODO We shouldn't modify this object in place
240 2
    routeUpdate.newRoutes = routeUpdate.newRoutes
241
      // Filter incoming routes that aren't part of the current global prefix or
242
      // cover the entire global prefix (i.e. the default route.)
243
      .filter(route =>
244 2
        route.prefix.startsWith(this.getGlobalPrefix()) &&
245
        route.prefix.length > this.getGlobalPrefix().length
246
      )
247
      // Filter incoming routes that include us as a hop (i.e. routing loops)
248
      .filter(route =>
249 2
        !route.path.includes(this.accounts.getOwnAddress())
250
      )
251

252 2
    const changedPrefixes = receiver.handleRouteUpdate(routeUpdate)
253

254
    let haveRoutesChanged
255 2
    for (const prefix of changedPrefixes) {
256 2
      haveRoutesChanged = this.updatePrefix(prefix) || haveRoutesChanged
257
    }
258 2
    if (haveRoutesChanged && this.config.routeBroadcastEnabled) {
259
      // TODO: Should we even trigger an immediate broadcast when routes change?
260
      //       Note that BGP does not do this AFAIK
261 2
      for (const peer of this.peers.values()) {
262 2
        const sender = peer.getSender()
263 2
        if (sender) {
264 2
          sender.scheduleRouteUpdate()
265
        }
266
      }
267
    }
268
  }
269

270
  reloadLocalRoutes () {
271 2
    log.trace('reload local and configured routes.')
272

273 2
    this.localRoutes = new Map()
274 2
    const localAccounts = this.accounts.getAccountIds()
275

276
    // Add a route for our own address
277 2
    const ownAddress = this.accounts.getOwnAddress()
278 2
    this.localRoutes.set(ownAddress, {
279
      nextHop: '',
280
      path: [],
281
      auth: hmac(this.routingSecret, ownAddress)
282
    })
283

284 2
    let defaultRoute = this.config.defaultRoute
285 2
    if (defaultRoute === 'auto') {
286 2
      defaultRoute = localAccounts.filter(id => this.accounts.getInfo(id).relation === 'parent')[0]
287
    }
288 2
    if (defaultRoute) {
289 0
      const globalPrefix = this.getGlobalPrefix()
290 0
      this.localRoutes.set(globalPrefix, {
291
        nextHop: defaultRoute,
292
        path: [],
293
        auth: hmac(this.routingSecret, globalPrefix)
294
      })
295
    }
296

297 2
    for (let accountId of localAccounts) {
298 2
      if (this.getAccountRelation(accountId) === 'child') {
299 2
        const childAddress = this.accounts.getChildAddress(accountId)
300 2
        this.localRoutes.set(childAddress, {
301
          nextHop: accountId,
302
          path: [],
303
          auth: hmac(this.routingSecret, childAddress)
304
        })
305
      }
306
    }
307

308 2
    const localPrefixes = Array.from(this.localRoutes.keys())
309 2
    const configuredPrefixes = this.config.routes
310 2
      ? this.config.routes.map(r => r.targetPrefix)
311
      : []
312

313 2
    for (let prefix of localPrefixes.concat(configuredPrefixes)) {
314 2
      this.updatePrefix(prefix)
315
    }
316
  }
317

318
  updatePrefix (prefix: string) {
319 2
    const newBest = this.getBestPeerForPrefix(prefix)
320

321 2
    return this.updateLocalRoute(prefix, newBest)
322
  }
323

324
  getBestPeerForPrefix (prefix: string): Route | undefined {
325
    // configured routes have highest priority
326 2
    const configuredRoute = find(this.config.routes, { targetPrefix: prefix })
327 2
    if (configuredRoute) {
328 2
      if (this.accounts.exists(configuredRoute.peerId)) {
329 2
        return {
330
          nextHop: configuredRoute.peerId,
331
          path: [],
332
          auth: hmac(this.routingSecret, prefix)
333
        }
334
      } else {
335 0
        log.warn('ignoring configured route, account does not exist. prefix=%s accountId=%s', configuredRoute.targetPrefix, configuredRoute.peerId)
336
      }
337
    }
338

339 2
    const localRoute = this.localRoutes.get(prefix)
340 2
    if (localRoute) {
341 2
      return localRoute
342
    }
343

344 2
    const weight = (route: IncomingRoute) => {
345 0
      const relation = this.getAccountRelation(route.peer)
346 0
      return getRelationPriority(relation)
347
    }
348

349 2
    const bestRoute = Array.from(this.peers.values())
350 2
      .map(peer => peer.getReceiver())
351 2
      .map(receiver => receiver && receiver.getPrefix(prefix))
352 2
      .filter((a): a is IncomingRoute => !!a)
353
      .sort((a?: IncomingRoute, b?: IncomingRoute) => {
354 2
        if (!a && !b) {
355 0
          return 0
356 2
        } else if (!a) {
357 0
          return 1
358 2
        } else if (!b) {
359 0
          return -1
360
        }
361

362
        // First sort by peer weight
363 0
        const weightA = weight(a)
364 0
        const weightB = weight(b)
365

366 2
        if (weightA !== weightB) {
367 0
          return weightB - weightA
368
        }
369

370
        // Then sort by path length
371 0
        const pathA = a.path.length
372 0
        const pathB = b.path.length
373

374 2
        if (pathA !== pathB) {
375 0
          return pathA - pathB
376
        }
377

378
        // Finally, tie-break by accountId
379 2
        if (a.peer > b.peer) {
380 0
          return 1
381 2
        } else if (b.peer > a.peer) {
382 0
          return -1
383
        } else {
384 0
          return 0
385
        }
386
      })[0]
387

388 2
    return bestRoute && {
389
      nextHop: bestRoute.peer,
390
      path: bestRoute.path,
391
      auth: bestRoute.auth
392
    }
393
  }
394

395
  getGlobalPrefix () {
396 2
    switch (this.config.env) {
397 2
      case 'production':
398 0
        return 'g'
399
      case 'test':
400 2
        return 'test'
401
      default:
402 0
        throw new Error('invalid value for `env` config. env=' + this.config.env)
403
    }
404
  }
405

406
  getStatus () {
407 2
    return {
408
      routingTableId: this.forwardingRoutingTable.routingTableId,
409
      currentEpoch: this.forwardingRoutingTable.currentEpoch,
410
      localRoutingTable: formatRoutingTableAsJson(this.localRoutingTable),
411
      forwardingRoutingTable: formatForwardingRoutingTableAsJson(this.forwardingRoutingTable),
412
      routingLog: this.forwardingRoutingTable.log
413
        .filter(Boolean)
414 2
        .map(entry => ({
415
          ...entry,
416 2
          route: entry && entry.route && formatRouteAsJson(entry.route)
417
        })),
418
      peers: Array.from(this.peers.values()).reduce((acc, peer) => {
419 2
        const sender = peer.getSender()
420 2
        const receiver = peer.getReceiver()
421 2
        acc[peer.getAccountId()] = {
422 2
          send: sender && sender.getStatus(),
423 2
          receive: receiver && receiver.getStatus()
424
        }
425 2
        return acc
426
      }, {})
427
    }
428
  }
429

430
  private updateLocalRoute (prefix: string, route?: Route) {
431 2
    const currentBest = this.localRoutingTable.get(prefix)
432 2
    const currentNextHop = currentBest && currentBest.nextHop
433 2
    const newNextHop = route && route.nextHop
434

435 2
    if (newNextHop !== currentNextHop) {
436 2
      if (route) {
437 2
        log.trace('new best route for prefix. prefix=%s oldBest=%s newBest=%s', prefix, currentNextHop, newNextHop)
438 2
        this.localRoutingTable.insert(prefix, route)
439
      } else {
440 2
        log.trace('no more route available for prefix. prefix=%s', prefix)
441 2
        this.localRoutingTable.delete(prefix)
442
      }
443

444 2
      this.updateForwardingRoute(prefix, route)
445

446 2
      return true
447
    }
448

449 2
    return false
450
  }
451

452
  private updateForwardingRoute (prefix: string, route?: Route) {
453 2
    if (route) {
454 2
      route = {
455
        ...route,
456
        path: [this.accounts.getOwnAddress(), ...route.path],
457
        auth: sha256(route.auth)
458
      }
459

460 2
      if (
461
        // Routes must start with the global prefix
462 2
        !prefix.startsWith(this.getGlobalPrefix()) ||
463

464
        // Don't publish the default route
465
        prefix === this.getGlobalPrefix() ||
466

467
        // Don't advertise local customer routes that we originated. Packets for
468
        // these destinations should still reach us because we are advertising our
469
        // own address as a prefix.
470
        (
471
          prefix.startsWith(this.accounts.getOwnAddress() + '.') &&
472
          route.path.length === 1
473
        ) ||
474

475
        canDragonFilter(
476
          this.forwardingRoutingTable,
477
          this.getAccountRelation,
478
          prefix,
479
          route
480
        )
481
      ) {
482 2
        route = undefined
483
      }
484
    }
485

486 2
    const currentBest = this.forwardingRoutingTable.get(prefix)
487

488 2
    const currentNextHop = currentBest && currentBest.route && currentBest.route.nextHop
489 2
    const newNextHop = route && route.nextHop
490

491 2
    if (currentNextHop !== newNextHop) {
492 2
      const epoch = this.forwardingRoutingTable.currentEpoch++
493 2
      const routeUpdate: RouteUpdate = {
494
        prefix,
495
        route,
496
        epoch
497
      }
498

499 2
      this.forwardingRoutingTable.insert(prefix, routeUpdate)
500

501 2
      log.trace('logging route update. update=%j', routeUpdate)
502

503 2
      if (currentBest) {
504 2
        this.forwardingRoutingTable.log[currentBest.epoch] = null
505
      }
506

507 2
      this.forwardingRoutingTable.log[epoch] = routeUpdate
508

509 2
      if (route) {
510
        // We need to re-check any prefixes that start with this prefix to see
511
        // if we can apply DRAGON filtering.
512
        //
513
        // Note that we do this check *after* we have added the new route above.
514 2
        const subPrefixes = this.forwardingRoutingTable.getKeysStartingWith(prefix)
515

516 2
        for (const subPrefix of subPrefixes) {
517 2
          if (subPrefix === prefix) continue
518

519 0
          const routeUpdate = this.forwardingRoutingTable.get(subPrefix)
520

521 2
          if (!routeUpdate || !routeUpdate.route) continue
522

523 0
          this.updateForwardingRoute(subPrefix, routeUpdate.route)
524
        }
525
      }
526
    }
527
  }
528

529 2
  private getAccountRelation = (accountId: string): Relation => {
530 2
    return accountId ? this.accounts.getInfo(accountId).relation : 'local'
531
  }
532
}

Read our documentation on viewing source code .

Loading