1 | import { randomBytes } from 'crypto'
|
2 | import { Errors } from 'ilp-packet'
|
3 | import { create as createLogger } from '../common/log'
|
4 | const log = createLogger('route-broadcaster')
|
5 | import { find } from 'lodash'
|
6 | import RoutingTable from './routing-table'
|
7 | import ForwardingRoutingTable, { RouteUpdate } from './forwarding-routing-table'
|
8 | import Accounts from './accounts'
|
9 | import Config from './config'
|
10 | import Peer from '../routing/peer'
|
11 | import { canDragonFilter } from '../routing/dragon'
|
12 | import { Relation, getRelationPriority } from '../routing/relation'
|
13 | 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 | import { sha256, hmac } from '../lib/utils'
|
24 | import {
|
25 | CcpRouteControlRequest,
|
26 | CcpRouteUpdateRequest
|
27 | } from 'ilp-protocol-ccp'
|
28 | const { BadRequestError } = Errors
|
29 |
|
30 | export default class RouteBroadcaster {
|
31 | private deps: reduct.Injector
|
32 |
|
33 | private localRoutingTable: RoutingTable
|
34 |
|
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()
|
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 |
|
81 | return
|
82 | }
|
83 |
|
84 | const plugin = this.accounts.getPlugin(accountId)
|
85 |
|
86 | const connectHandler = () => {
|
87 | if (!plugin.isConnected()) {
|
88 |
|
89 |
|
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 |
|
150 |
|
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 |
|
192 |
|
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 |
|
238 |
|
239 |
|
240 | routeUpdate.newRoutes = routeUpdate.newRoutes
|
241 |
|
242 |
|
243 | .filter(route =>
|
244 | route.prefix.startsWith(this.getGlobalPrefix()) &&
|
245 | route.prefix.length > this.getGlobalPrefix().length
|
246 | )
|
247 |
|
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 |
|
260 |
|
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 |
|
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 |
|
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 |
|
363 | const weightA = weight(a)
|
364 | const weightB = weight(b)
|
365 |
|
366 | if (weightA !== weightB) {
|
367 | return weightB - weightA
|
368 | }
|
369 |
|
370 |
|
371 | const pathA = a.path.length
|
372 | const pathB = b.path.length
|
373 |
|
374 | if (pathA !== pathB) {
|
375 | return pathA - pathB
|
376 | }
|
377 |
|
378 |
|
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 |
|
462 | !prefix.startsWith(this.getGlobalPrefix()) ||
|
463 |
|
464 |
|
465 | prefix === this.getGlobalPrefix() ||
|
466 |
|
467 |
|
468 |
|
469 |
|
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 |
|
511 |
|
512 |
|
513 |
|
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 | }
|