1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const crypto_1 = require("crypto");
|
4 | const ilp_packet_1 = require("ilp-packet");
|
5 | const log_1 = require("../common/log");
|
6 | const log = log_1.create('route-broadcaster');
|
7 | const lodash_1 = require("lodash");
|
8 | const routing_table_1 = require("./routing-table");
|
9 | const forwarding_routing_table_1 = require("./forwarding-routing-table");
|
10 | const accounts_1 = require("./accounts");
|
11 | const config_1 = require("./config");
|
12 | const peer_1 = require("../routing/peer");
|
13 | const dragon_1 = require("../routing/dragon");
|
14 | const relation_1 = require("../routing/relation");
|
15 | const utils_1 = require("../routing/utils");
|
16 | const utils_2 = require("../lib/utils");
|
17 | const { BadRequestError } = ilp_packet_1.Errors;
|
18 | class RouteBroadcaster {
|
19 | constructor(deps) {
|
20 | this.untrackCallbacks = new Map();
|
21 | this.getAccountRelation = (accountId) => {
|
22 | return accountId ? this.accounts.getInfo(accountId).relation : 'local';
|
23 | };
|
24 | this.deps = deps;
|
25 | this.localRoutingTable = deps(routing_table_1.default);
|
26 | this.forwardingRoutingTable = deps(forwarding_routing_table_1.default);
|
27 | this.accounts = deps(accounts_1.default);
|
28 | this.config = deps(config_1.default);
|
29 | if (this.config.routingSecret) {
|
30 | log.info('loaded routing secret from config.');
|
31 | this.routingSecret = Buffer.from(this.config.routingSecret, 'base64');
|
32 | }
|
33 | else {
|
34 | log.info('generated random routing secret.');
|
35 | this.routingSecret = crypto_1.randomBytes(32);
|
36 | }
|
37 | this.peers = new Map();
|
38 | this.localRoutes = new Map();
|
39 | }
|
40 | start() {
|
41 | this.reloadLocalRoutes();
|
42 | for (const accountId of this.accounts.getAccountIds()) {
|
43 | this.track(accountId);
|
44 | }
|
45 | }
|
46 | stop() {
|
47 | for (const accountId of this.peers.keys()) {
|
48 | this.remove(accountId);
|
49 | }
|
50 | }
|
51 | track(accountId) {
|
52 | if (this.untrackCallbacks.has(accountId)) {
|
53 | return;
|
54 | }
|
55 | const plugin = this.accounts.getPlugin(accountId);
|
56 | const connectHandler = () => {
|
57 | if (!plugin.isConnected()) {
|
58 | log.error('(!!!) plugin emitted connect, but then returned false for isConnected, broken plugin. account=%s', accountId);
|
59 | setImmediate(() => this.add(accountId));
|
60 | }
|
61 | else {
|
62 | this.add(accountId);
|
63 | }
|
64 | };
|
65 | const disconnectHandler = () => {
|
66 | this.remove(accountId);
|
67 | };
|
68 | plugin.on('connect', connectHandler);
|
69 | plugin.on('disconnect', disconnectHandler);
|
70 | this.untrackCallbacks.set(accountId, () => {
|
71 | plugin.removeListener('connect', connectHandler);
|
72 | plugin.removeListener('disconnect', disconnectHandler);
|
73 | });
|
74 | this.add(accountId);
|
75 | }
|
76 | untrack(accountId) {
|
77 | this.remove(accountId);
|
78 | const callback = this.untrackCallbacks.get(accountId);
|
79 | if (callback) {
|
80 | callback();
|
81 | }
|
82 | }
|
83 | add(accountId) {
|
84 | const accountInfo = this.accounts.getInfo(accountId);
|
85 | let sendRoutes;
|
86 | if (typeof accountInfo.sendRoutes === 'boolean') {
|
87 | sendRoutes = accountInfo.sendRoutes;
|
88 | }
|
89 | else if (accountInfo.relation !== 'child') {
|
90 | sendRoutes = true;
|
91 | }
|
92 | else {
|
93 | sendRoutes = false;
|
94 | }
|
95 | let receiveRoutes;
|
96 | if (typeof accountInfo.receiveRoutes === 'boolean') {
|
97 | receiveRoutes = accountInfo.receiveRoutes;
|
98 | }
|
99 | else if (accountInfo.relation !== 'child') {
|
100 | receiveRoutes = true;
|
101 | }
|
102 | else {
|
103 | receiveRoutes = false;
|
104 | }
|
105 | if (!sendRoutes && !receiveRoutes) {
|
106 | log.warn('not sending/receiving routes for peer, set sendRoutes/receiveRoutes to override. accountId=%s', accountId);
|
107 | return;
|
108 | }
|
109 | const existingPeer = this.peers.get(accountId);
|
110 | if (existingPeer) {
|
111 | const receiver = existingPeer.getReceiver();
|
112 | if (receiver) {
|
113 | receiver.sendRouteControl();
|
114 | }
|
115 | else {
|
116 | log.warn('unable to send route control message, receiver object undefined. peer=%s', existingPeer);
|
117 | }
|
118 | return;
|
119 | }
|
120 | const plugin = this.accounts.getPlugin(accountId);
|
121 | if (plugin.isConnected()) {
|
122 | log.trace('add peer. accountId=%s sendRoutes=%s receiveRoutes=%s', accountId, sendRoutes, receiveRoutes);
|
123 | const peer = new peer_1.default({ deps: this.deps, accountId, sendRoutes, receiveRoutes });
|
124 | this.peers.set(accountId, peer);
|
125 | const receiver = peer.getReceiver();
|
126 | if (receiver) {
|
127 | receiver.sendRouteControl();
|
128 | }
|
129 | this.reloadLocalRoutes();
|
130 | }
|
131 | }
|
132 | remove(accountId) {
|
133 | const peer = this.peers.get(accountId);
|
134 | if (!peer) {
|
135 | return;
|
136 | }
|
137 | const sender = peer.getSender();
|
138 | const receiver = peer.getReceiver();
|
139 | log.trace('remove peer. peerId=' + accountId);
|
140 | if (sender) {
|
141 | sender.stop();
|
142 | }
|
143 | this.peers.delete(accountId);
|
144 | if (receiver) {
|
145 | for (let prefix of receiver.getPrefixes()) {
|
146 | this.updatePrefix(prefix);
|
147 | }
|
148 | }
|
149 | if (this.getAccountRelation(accountId) === 'child') {
|
150 | this.updatePrefix(this.accounts.getChildAddress(accountId));
|
151 | }
|
152 | }
|
153 | handleRouteControl(sourceAccount, routeControl) {
|
154 | const peer = this.peers.get(sourceAccount);
|
155 | if (!peer) {
|
156 | log.debug('received route control message from non-peer. sourceAccount=%s', sourceAccount);
|
157 | throw new BadRequestError('cannot process route control messages from non-peers.');
|
158 | }
|
159 | const sender = peer.getSender();
|
160 | if (!sender) {
|
161 | log.debug('received route control message from peer not authorized to receive routes from us (sendRoutes=false). sourceAccount=%s', sourceAccount);
|
162 | throw new BadRequestError('rejecting route control message, we are configured not to send routes to you.');
|
163 | }
|
164 | sender.handleRouteControl(routeControl);
|
165 | }
|
166 | handleRouteUpdate(sourceAccount, routeUpdate) {
|
167 | const peer = this.peers.get(sourceAccount);
|
168 | if (!peer) {
|
169 | log.debug('received route update from non-peer. sourceAccount=%s', sourceAccount);
|
170 | throw new BadRequestError('cannot process route update messages from non-peers.');
|
171 | }
|
172 | const receiver = peer.getReceiver();
|
173 | if (!receiver) {
|
174 | log.debug('received route update from peer not authorized to advertise routes to us (receiveRoutes=false). sourceAccount=%s', sourceAccount);
|
175 | throw new BadRequestError('rejecting route update, we are configured not to receive routes from you.');
|
176 | }
|
177 | routeUpdate.newRoutes = routeUpdate.newRoutes
|
178 | .filter(route => route.prefix.startsWith(this.getGlobalPrefix()) &&
|
179 | route.prefix.length > this.getGlobalPrefix().length)
|
180 | .filter(route => !route.path.includes(this.accounts.getOwnAddress()));
|
181 | const changedPrefixes = receiver.handleRouteUpdate(routeUpdate);
|
182 | let haveRoutesChanged;
|
183 | for (const prefix of changedPrefixes) {
|
184 | haveRoutesChanged = this.updatePrefix(prefix) || haveRoutesChanged;
|
185 | }
|
186 | if (haveRoutesChanged && this.config.routeBroadcastEnabled) {
|
187 | for (const peer of this.peers.values()) {
|
188 | const sender = peer.getSender();
|
189 | if (sender) {
|
190 | sender.scheduleRouteUpdate();
|
191 | }
|
192 | }
|
193 | }
|
194 | }
|
195 | reloadLocalRoutes() {
|
196 | log.trace('reload local and configured routes.');
|
197 | this.localRoutes = new Map();
|
198 | const localAccounts = this.accounts.getAccountIds();
|
199 | const ownAddress = this.accounts.getOwnAddress();
|
200 | this.localRoutes.set(ownAddress, {
|
201 | nextHop: '',
|
202 | path: [],
|
203 | auth: utils_2.hmac(this.routingSecret, ownAddress)
|
204 | });
|
205 | let defaultRoute = this.config.defaultRoute;
|
206 | if (defaultRoute === 'auto') {
|
207 | defaultRoute = localAccounts.filter(id => this.accounts.getInfo(id).relation === 'parent')[0];
|
208 | }
|
209 | if (defaultRoute) {
|
210 | const globalPrefix = this.getGlobalPrefix();
|
211 | this.localRoutes.set(globalPrefix, {
|
212 | nextHop: defaultRoute,
|
213 | path: [],
|
214 | auth: utils_2.hmac(this.routingSecret, globalPrefix)
|
215 | });
|
216 | }
|
217 | for (let accountId of localAccounts) {
|
218 | if (this.getAccountRelation(accountId) === 'child') {
|
219 | const childAddress = this.accounts.getChildAddress(accountId);
|
220 | this.localRoutes.set(childAddress, {
|
221 | nextHop: accountId,
|
222 | path: [],
|
223 | auth: utils_2.hmac(this.routingSecret, childAddress)
|
224 | });
|
225 | }
|
226 | }
|
227 | const localPrefixes = Array.from(this.localRoutes.keys());
|
228 | const configuredPrefixes = this.config.routes
|
229 | ? this.config.routes.map(r => r.targetPrefix)
|
230 | : [];
|
231 | for (let prefix of localPrefixes.concat(configuredPrefixes)) {
|
232 | this.updatePrefix(prefix);
|
233 | }
|
234 | }
|
235 | updatePrefix(prefix) {
|
236 | const newBest = this.getBestPeerForPrefix(prefix);
|
237 | return this.updateLocalRoute(prefix, newBest);
|
238 | }
|
239 | getBestPeerForPrefix(prefix) {
|
240 | const configuredRoute = lodash_1.find(this.config.routes, { targetPrefix: prefix });
|
241 | if (configuredRoute) {
|
242 | if (this.accounts.exists(configuredRoute.peerId)) {
|
243 | return {
|
244 | nextHop: configuredRoute.peerId,
|
245 | path: [],
|
246 | auth: utils_2.hmac(this.routingSecret, prefix)
|
247 | };
|
248 | }
|
249 | else {
|
250 | log.warn('ignoring configured route, account does not exist. prefix=%s accountId=%s', configuredRoute.targetPrefix, configuredRoute.peerId);
|
251 | }
|
252 | }
|
253 | const localRoute = this.localRoutes.get(prefix);
|
254 | if (localRoute) {
|
255 | return localRoute;
|
256 | }
|
257 | const weight = (route) => {
|
258 | const relation = this.getAccountRelation(route.peer);
|
259 | return relation_1.getRelationPriority(relation);
|
260 | };
|
261 | const bestRoute = Array.from(this.peers.values())
|
262 | .map(peer => peer.getReceiver())
|
263 | .map(receiver => receiver && receiver.getPrefix(prefix))
|
264 | .filter((a) => !!a)
|
265 | .sort((a, b) => {
|
266 | if (!a && !b) {
|
267 | return 0;
|
268 | }
|
269 | else if (!a) {
|
270 | return 1;
|
271 | }
|
272 | else if (!b) {
|
273 | return -1;
|
274 | }
|
275 | const weightA = weight(a);
|
276 | const weightB = weight(b);
|
277 | if (weightA !== weightB) {
|
278 | return weightB - weightA;
|
279 | }
|
280 | const pathA = a.path.length;
|
281 | const pathB = b.path.length;
|
282 | if (pathA !== pathB) {
|
283 | return pathA - pathB;
|
284 | }
|
285 | if (a.peer > b.peer) {
|
286 | return 1;
|
287 | }
|
288 | else if (b.peer > a.peer) {
|
289 | return -1;
|
290 | }
|
291 | else {
|
292 | return 0;
|
293 | }
|
294 | })[0];
|
295 | return bestRoute && {
|
296 | nextHop: bestRoute.peer,
|
297 | path: bestRoute.path,
|
298 | auth: bestRoute.auth
|
299 | };
|
300 | }
|
301 | getGlobalPrefix() {
|
302 | switch (this.config.env) {
|
303 | case 'production':
|
304 | return 'g';
|
305 | case 'test':
|
306 | return 'test';
|
307 | default:
|
308 | throw new Error('invalid value for `env` config. env=' + this.config.env);
|
309 | }
|
310 | }
|
311 | getStatus() {
|
312 | return {
|
313 | routingTableId: this.forwardingRoutingTable.routingTableId,
|
314 | currentEpoch: this.forwardingRoutingTable.currentEpoch,
|
315 | localRoutingTable: utils_1.formatRoutingTableAsJson(this.localRoutingTable),
|
316 | forwardingRoutingTable: utils_1.formatForwardingRoutingTableAsJson(this.forwardingRoutingTable),
|
317 | routingLog: this.forwardingRoutingTable.log
|
318 | .filter(Boolean)
|
319 | .map(entry => (Object.assign({}, entry, { route: entry && entry.route && utils_1.formatRouteAsJson(entry.route) }))),
|
320 | peers: Array.from(this.peers.values()).reduce((acc, peer) => {
|
321 | const sender = peer.getSender();
|
322 | const receiver = peer.getReceiver();
|
323 | acc[peer.getAccountId()] = {
|
324 | send: sender && sender.getStatus(),
|
325 | receive: receiver && receiver.getStatus()
|
326 | };
|
327 | return acc;
|
328 | }, {})
|
329 | };
|
330 | }
|
331 | updateLocalRoute(prefix, route) {
|
332 | const currentBest = this.localRoutingTable.get(prefix);
|
333 | const currentNextHop = currentBest && currentBest.nextHop;
|
334 | const newNextHop = route && route.nextHop;
|
335 | if (newNextHop !== currentNextHop) {
|
336 | if (route) {
|
337 | log.trace('new best route for prefix. prefix=%s oldBest=%s newBest=%s', prefix, currentNextHop, newNextHop);
|
338 | this.localRoutingTable.insert(prefix, route);
|
339 | }
|
340 | else {
|
341 | log.trace('no more route available for prefix. prefix=%s', prefix);
|
342 | this.localRoutingTable.delete(prefix);
|
343 | }
|
344 | this.updateForwardingRoute(prefix, route);
|
345 | return true;
|
346 | }
|
347 | return false;
|
348 | }
|
349 | updateForwardingRoute(prefix, route) {
|
350 | if (route) {
|
351 | route = Object.assign({}, route, { path: [this.accounts.getOwnAddress(), ...route.path], auth: utils_2.sha256(route.auth) });
|
352 | if (!prefix.startsWith(this.getGlobalPrefix()) ||
|
353 | prefix === this.getGlobalPrefix() ||
|
354 | (prefix.startsWith(this.accounts.getOwnAddress() + '.') &&
|
355 | route.path.length === 1) ||
|
356 | dragon_1.canDragonFilter(this.forwardingRoutingTable, this.getAccountRelation, prefix, route)) {
|
357 | route = undefined;
|
358 | }
|
359 | }
|
360 | const currentBest = this.forwardingRoutingTable.get(prefix);
|
361 | const currentNextHop = currentBest && currentBest.route && currentBest.route.nextHop;
|
362 | const newNextHop = route && route.nextHop;
|
363 | if (currentNextHop !== newNextHop) {
|
364 | const epoch = this.forwardingRoutingTable.currentEpoch++;
|
365 | const routeUpdate = {
|
366 | prefix,
|
367 | route,
|
368 | epoch
|
369 | };
|
370 | this.forwardingRoutingTable.insert(prefix, routeUpdate);
|
371 | log.trace('logging route update. update=%j', routeUpdate);
|
372 | if (currentBest) {
|
373 | this.forwardingRoutingTable.log[currentBest.epoch] = null;
|
374 | }
|
375 | this.forwardingRoutingTable.log[epoch] = routeUpdate;
|
376 | if (route) {
|
377 | const subPrefixes = this.forwardingRoutingTable.getKeysStartingWith(prefix);
|
378 | for (const subPrefix of subPrefixes) {
|
379 | if (subPrefix === prefix)
|
380 | continue;
|
381 | const routeUpdate = this.forwardingRoutingTable.get(subPrefix);
|
382 | if (!routeUpdate || !routeUpdate.route)
|
383 | continue;
|
384 | this.updateForwardingRoute(subPrefix, routeUpdate.route);
|
385 | }
|
386 | }
|
387 | }
|
388 | }
|
389 | }
|
390 | exports.default = RouteBroadcaster;
|
391 |
|
\ | No newline at end of file |