UNPKG

16.3 kBPlain TextView Raw
1import { randomBytes } from 'crypto'
2import { Errors } from 'ilp-packet'
3import { create as createLogger } from '../common/log'
4const log = createLogger('route-broadcaster')
5import { find } from 'lodash'
6import RoutingTable from './routing-table'
7import ForwardingRoutingTable, { RouteUpdate } from './forwarding-routing-table'
8import Accounts from './accounts'
9import Config from './config'
10import Peer from '../routing/peer'
11import { canDragonFilter } from '../routing/dragon'
12import { Relation, getRelationPriority } from '../routing/relation'
13import {
14 formatRoutingTableAsJson,
15 formatRouteAsJson,
16 formatForwardingRoutingTableAsJson
17} from '../routing/utils'
18import {
19 Route,
20 IncomingRoute
21} from '../types/routing'
22import reduct = require('reduct')
23import { sha256, hmac } from '../lib/utils'
24import {
25 CcpRouteControlRequest,
26 CcpRouteUpdateRequest
27} from 'ilp-protocol-ccp'
28const { BadRequestError } = Errors
29
30export 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 private untrackCallbacks: Map<string, () => void> = new Map()
44
45 constructor (deps: reduct.Injector) {
46 this.deps = deps
47 this.localRoutingTable = deps(RoutingTable)
48 this.forwardingRoutingTable = deps(ForwardingRoutingTable)
49 this.accounts = deps(Accounts)
50 this.config = deps(Config)
51
52 if (this.config.routingSecret) {
53 log.info('loaded routing secret from config.')
54 this.routingSecret = Buffer.from(this.config.routingSecret, 'base64')
55 } else {
56 log.info('generated random routing secret.')
57 this.routingSecret = randomBytes(32)
58 }
59
60 this.peers = new Map() // peerId:string -> peer:Peer
61 this.localRoutes = new Map()
62 }
63
64 start () {
65 this.reloadLocalRoutes()
66
67 for (const accountId of this.accounts.getAccountIds()) {
68 this.track(accountId)
69 }
70 }
71
72 stop () {
73 for (const accountId of this.peers.keys()) {
74 this.remove(accountId)
75 }
76 }
77
78 track (accountId: string) {
79 if (this.untrackCallbacks.has(accountId)) {
80 // Already tracked
81 return
82 }
83
84 const plugin = this.accounts.getPlugin(accountId)
85
86 const connectHandler = () => {
87 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 log.error('(!!!) plugin emitted connect, but then returned false for isConnected, broken plugin. account=%s', accountId)
91 setImmediate(() => this.add(accountId))
92 } else {
93 this.add(accountId)
94 }
95 }
96 const disconnectHandler = () => {
97 this.remove(accountId)
98 }
99
100 plugin.on('connect', connectHandler)
101 plugin.on('disconnect', disconnectHandler)
102
103 this.untrackCallbacks.set(accountId, () => {
104 plugin.removeListener('connect', connectHandler)
105 plugin.removeListener('disconnect', disconnectHandler)
106 })
107
108 this.add(accountId)
109 }
110
111 untrack (accountId: string) {
112 this.remove(accountId)
113
114 const callback = this.untrackCallbacks.get(accountId)
115
116 if (callback) {
117 callback()
118 }
119 }
120
121 add (accountId: string) {
122 const accountInfo = this.accounts.getInfo(accountId)
123
124 let sendRoutes
125 if (typeof accountInfo.sendRoutes === 'boolean') {
126 sendRoutes = accountInfo.sendRoutes
127 } else if (accountInfo.relation !== 'child') {
128 sendRoutes = true
129 } else {
130 sendRoutes = false
131 }
132
133 let receiveRoutes
134 if (typeof accountInfo.receiveRoutes === 'boolean') {
135 receiveRoutes = accountInfo.receiveRoutes
136 } else if (accountInfo.relation !== 'child') {
137 receiveRoutes = true
138 } else {
139 receiveRoutes = false
140 }
141
142 if (!sendRoutes && !receiveRoutes) {
143 log.warn('not sending/receiving routes for peer, set sendRoutes/receiveRoutes to override. accountId=%s', accountId)
144 return
145 }
146
147 const existingPeer = this.peers.get(accountId)
148 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 const receiver = existingPeer.getReceiver()
152
153 if (receiver) {
154 receiver.sendRouteControl()
155 } else {
156 log.warn('unable to send route control message, receiver object undefined. peer=%s', existingPeer)
157 }
158
159 return
160 }
161
162 const plugin = this.accounts.getPlugin(accountId)
163
164 if (plugin.isConnected()) {
165 log.trace('add peer. accountId=%s sendRoutes=%s receiveRoutes=%s', accountId, sendRoutes, receiveRoutes)
166 const peer = new Peer({ deps: this.deps, accountId, sendRoutes, receiveRoutes })
167 this.peers.set(accountId, peer)
168 const receiver = peer.getReceiver()
169 if (receiver) {
170 receiver.sendRouteControl()
171 }
172 this.reloadLocalRoutes()
173 }
174 }
175
176 remove (accountId: string) {
177 const peer = this.peers.get(accountId)
178
179 if (!peer) {
180 return
181 }
182
183 const sender = peer.getSender()
184 const receiver = peer.getReceiver()
185
186 log.trace('remove peer. peerId=' + accountId)
187 if (sender) {
188 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 this.peers.delete(accountId)
194 if (receiver) {
195 for (let prefix of receiver.getPrefixes()) {
196 this.updatePrefix(prefix)
197 }
198 }
199 if (this.getAccountRelation(accountId) === 'child') {
200 this.updatePrefix(this.accounts.getChildAddress(accountId))
201 }
202 }
203
204 handleRouteControl (sourceAccount: string, routeControl: CcpRouteControlRequest) {
205 const peer = this.peers.get(sourceAccount)
206
207 if (!peer) {
208 log.debug('received route control message from non-peer. sourceAccount=%s', sourceAccount)
209 throw new BadRequestError('cannot process route control messages from non-peers.')
210 }
211
212 const sender = peer.getSender()
213
214 if (!sender) {
215 log.debug('received route control message from peer not authorized to receive routes from us (sendRoutes=false). sourceAccount=%s', sourceAccount)
216 throw new BadRequestError('rejecting route control message, we are configured not to send routes to you.')
217 }
218
219 sender.handleRouteControl(routeControl)
220 }
221
222 handleRouteUpdate (sourceAccount: string, routeUpdate: CcpRouteUpdateRequest) {
223 const peer = this.peers.get(sourceAccount)
224
225 if (!peer) {
226 log.debug('received route update from non-peer. sourceAccount=%s', sourceAccount)
227 throw new BadRequestError('cannot process route update messages from non-peers.')
228 }
229
230 const receiver = peer.getReceiver()
231
232 if (!receiver) {
233 log.debug('received route update from peer not authorized to advertise routes to us (receiveRoutes=false). sourceAccount=%s', sourceAccount)
234 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 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 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 !route.path.includes(this.accounts.getOwnAddress())
250 )
251
252 const changedPrefixes = receiver.handleRouteUpdate(routeUpdate)
253
254 let haveRoutesChanged
255 for (const prefix of changedPrefixes) {
256 haveRoutesChanged = this.updatePrefix(prefix) || haveRoutesChanged
257 }
258 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 for (const peer of this.peers.values()) {
262 const sender = peer.getSender()
263 if (sender) {
264 sender.scheduleRouteUpdate()
265 }
266 }
267 }
268 }
269
270 reloadLocalRoutes () {
271 log.trace('reload local and configured routes.')
272
273 this.localRoutes = new Map()
274 const localAccounts = this.accounts.getAccountIds()
275
276 // Add a route for our own address
277 const ownAddress = this.accounts.getOwnAddress()
278 this.localRoutes.set(ownAddress, {
279 nextHop: '',
280 path: [],
281 auth: hmac(this.routingSecret, ownAddress)
282 })
283
284 let defaultRoute = this.config.defaultRoute
285 if (defaultRoute === 'auto') {
286 defaultRoute = localAccounts.filter(id => this.accounts.getInfo(id).relation === 'parent')[0]
287 }
288 if (defaultRoute) {
289 const globalPrefix = this.getGlobalPrefix()
290 this.localRoutes.set(globalPrefix, {
291 nextHop: defaultRoute,
292 path: [],
293 auth: hmac(this.routingSecret, globalPrefix)
294 })
295 }
296
297 for (let accountId of localAccounts) {
298 if (this.getAccountRelation(accountId) === 'child') {
299 const childAddress = this.accounts.getChildAddress(accountId)
300 this.localRoutes.set(childAddress, {
301 nextHop: accountId,
302 path: [],
303 auth: hmac(this.routingSecret, childAddress)
304 })
305 }
306 }
307
308 const localPrefixes = Array.from(this.localRoutes.keys())
309 const configuredPrefixes = this.config.routes
310 ? this.config.routes.map(r => r.targetPrefix)
311 : []
312
313 for (let prefix of localPrefixes.concat(configuredPrefixes)) {
314 this.updatePrefix(prefix)
315 }
316 }
317
318 updatePrefix (prefix: string) {
319 const newBest = this.getBestPeerForPrefix(prefix)
320
321 return this.updateLocalRoute(prefix, newBest)
322 }
323
324 getBestPeerForPrefix (prefix: string): Route | undefined {
325 // configured routes have highest priority
326 const configuredRoute = find(this.config.routes, { targetPrefix: prefix })
327 if (configuredRoute) {
328 if (this.accounts.exists(configuredRoute.peerId)) {
329 return {
330 nextHop: configuredRoute.peerId,
331 path: [],
332 auth: hmac(this.routingSecret, prefix)
333 }
334 } else {
335 log.warn('ignoring configured route, account does not exist. prefix=%s accountId=%s', configuredRoute.targetPrefix, configuredRoute.peerId)
336 }
337 }
338
339 const localRoute = this.localRoutes.get(prefix)
340 if (localRoute) {
341 return localRoute
342 }
343
344 const weight = (route: IncomingRoute) => {
345 const relation = this.getAccountRelation(route.peer)
346 return getRelationPriority(relation)
347 }
348
349 const bestRoute = Array.from(this.peers.values())
350 .map(peer => peer.getReceiver())
351 .map(receiver => receiver && receiver.getPrefix(prefix))
352 .filter((a): a is IncomingRoute => !!a)
353 .sort((a?: IncomingRoute, b?: IncomingRoute) => {
354 if (!a && !b) {
355 return 0
356 } else if (!a) {
357 return 1
358 } else if (!b) {
359 return -1
360 }
361
362 // First sort by peer weight
363 const weightA = weight(a)
364 const weightB = weight(b)
365
366 if (weightA !== weightB) {
367 return weightB - weightA
368 }
369
370 // Then sort by path length
371 const pathA = a.path.length
372 const pathB = b.path.length
373
374 if (pathA !== pathB) {
375 return pathA - pathB
376 }
377
378 // Finally, tie-break by accountId
379 if (a.peer > b.peer) {
380 return 1
381 } else if (b.peer > a.peer) {
382 return -1
383 } else {
384 return 0
385 }
386 })[0]
387
388 return bestRoute && {
389 nextHop: bestRoute.peer,
390 path: bestRoute.path,
391 auth: bestRoute.auth
392 }
393 }
394
395 getGlobalPrefix () {
396 switch (this.config.env) {
397 case 'production':
398 return 'g'
399 case 'test':
400 return 'test'
401 default:
402 throw new Error('invalid value for `env` config. env=' + this.config.env)
403 }
404 }
405
406 getStatus () {
407 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 .map(entry => ({
415 ...entry,
416 route: entry && entry.route && formatRouteAsJson(entry.route)
417 })),
418 peers: Array.from(this.peers.values()).reduce((acc, peer) => {
419 const sender = peer.getSender()
420 const receiver = peer.getReceiver()
421 acc[peer.getAccountId()] = {
422 send: sender && sender.getStatus(),
423 receive: receiver && receiver.getStatus()
424 }
425 return acc
426 }, {})
427 }
428 }
429
430 private updateLocalRoute (prefix: string, route?: Route) {
431 const currentBest = this.localRoutingTable.get(prefix)
432 const currentNextHop = currentBest && currentBest.nextHop
433 const newNextHop = route && route.nextHop
434
435 if (newNextHop !== currentNextHop) {
436 if (route) {
437 log.trace('new best route for prefix. prefix=%s oldBest=%s newBest=%s', prefix, currentNextHop, newNextHop)
438 this.localRoutingTable.insert(prefix, route)
439 } else {
440 log.trace('no more route available for prefix. prefix=%s', prefix)
441 this.localRoutingTable.delete(prefix)
442 }
443
444 this.updateForwardingRoute(prefix, route)
445
446 return true
447 }
448
449 return false
450 }
451
452 private updateForwardingRoute (prefix: string, route?: Route) {
453 if (route) {
454 route = {
455 ...route,
456 path: [this.accounts.getOwnAddress(), ...route.path],
457 auth: sha256(route.auth)
458 }
459
460 if (
461 // Routes must start with the global prefix
462 !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 route = undefined
483 }
484 }
485
486 const currentBest = this.forwardingRoutingTable.get(prefix)
487
488 const currentNextHop = currentBest && currentBest.route && currentBest.route.nextHop
489 const newNextHop = route && route.nextHop
490
491 if (currentNextHop !== newNextHop) {
492 const epoch = this.forwardingRoutingTable.currentEpoch++
493 const routeUpdate: RouteUpdate = {
494 prefix,
495 route,
496 epoch
497 }
498
499 this.forwardingRoutingTable.insert(prefix, routeUpdate)
500
501 log.trace('logging route update. update=%j', routeUpdate)
502
503 if (currentBest) {
504 this.forwardingRoutingTable.log[currentBest.epoch] = null
505 }
506
507 this.forwardingRoutingTable.log[epoch] = routeUpdate
508
509 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 const subPrefixes = this.forwardingRoutingTable.getKeysStartingWith(prefix)
515
516 for (const subPrefix of subPrefixes) {
517 if (subPrefix === prefix) continue
518
519 const routeUpdate = this.forwardingRoutingTable.get(subPrefix)
520
521 if (!routeUpdate || !routeUpdate.route) continue
522
523 this.updateForwardingRoute(subPrefix, routeUpdate.route)
524 }
525 }
526 }
527 }
528
529 private getAccountRelation = (accountId: string): Relation => {
530 return accountId ? this.accounts.getInfo(accountId).relation : 'local'
531 }
532}