1 | # multiserver
|
2 |
|
3 | A single interface that can work with multiple protocols,
|
4 | and multiple transforms of those protocols (eg, security layer)
|
5 |
|
6 | ## motivation
|
7 |
|
8 | Developing a p2p system is hard. Especially hard is upgrading protocol layers.
|
9 | The contemporary approach is to [update code via a backdoor](https://whispersystems.org/blog/the-ecosystem-is-moving/),
|
10 | but as easily as security can be added, [it can be taken away](https://nakamotoinstitute.org/trusted-third-parties/).
|
11 |
|
12 | Before you can have a protocol, you need a connection between peers.
|
13 | That connection is over some form of network transport,
|
14 | probably encrypted with some encryption scheme, possibly
|
15 | compression or other layers too.
|
16 |
|
17 | Usually, two peers connect over a standard networking transport
|
18 | (probably tcp) then they have a negotiation to decide
|
19 | what the next layer (of encryption, for example) should be.
|
20 | This allows protocol implementators to roll out improved
|
21 | versions of the encryption protocol. However, it does
|
22 | not allow them to upgrade the negotiation protocol!
|
23 | If a negotiation protocol has a vulnerability it's much
|
24 | harder to fix, and since the negotiation needs to be unencrypted,
|
25 | it tends to reveal a lot about program the server is running.
|
26 | [in my opinion, it's time to try a different way.](https://github.com/ipfs/go-ipfs/pull/34)
|
27 |
|
28 | Some HTTP APIs provide upgradability in a better, simpler way by
|
29 | putting a version number within the url. A new version of
|
30 | the API can then be used without touching the old one at all.
|
31 |
|
32 | multiserver adapts this approach to lower level protocols.
|
33 | Instead of negotiating which protocol to use, run multiple
|
34 | protocols side by side, and consider the protocol part of the address.
|
35 |
|
36 | Most network systems have some sort of address look up,
|
37 | there is peer identifier (such it's domain) and then
|
38 | a system that is queried to map that domain to the lower level
|
39 | network address (such as it's ip address, retrieved via a DNS (Domain Name System) request)
|
40 | To connect to a website secured with https, first
|
41 | you look up the domain via DNS, then connect to the server.
|
42 | Then start a tls connection to that server, in which
|
43 | a cyphersuite is negotiated, and a certificate is provided
|
44 | by the server. (this certifies that the server really
|
45 | owns that domain)
|
46 |
|
47 | If it was using multiserver, DNS would respond with a list of cyphersuites,
|
48 | (encoded as multiserver addresses) and then you'd connect directly to a server and start using the protocol, without negotiation.
|
49 | p2p systems like scuttlebutt also usually have a lookup,
|
50 | but usually mapping from a public key to an ip address.
|
51 | Since a look up is needed anyway, it's a good place
|
52 | to provide information about the protocol that server speaks!
|
53 |
|
54 | This enables you to do two things, upgrade and bridging.
|
55 |
|
56 | ### upgrade
|
57 |
|
58 | If a peer wants to upgrade from *weak* protocol
|
59 | to a *strong* one, they simply start serving *strong* via another port,
|
60 | and advertise that in the lookup system.
|
61 | Now peers that have support for *strong* can connect via that protocol.
|
62 |
|
63 | Once most peers have upgraded to strong, support for *weak* can be discontinued.
|
64 |
|
65 | This is just how some services (eg, github) have an API version
|
66 | in their URL scheme. It is now easy to use two different
|
67 | versions in parallel. later, they can close down the old API.
|
68 |
|
69 | ``` js
|
70 | var MultiServer = require('multiserver')
|
71 | var chloride = require('chloride')
|
72 | var keys = chloride.crypto_sign_keypair()
|
73 | var appKey = "dTuPysQsRoyWzmsK6iegSV4U3Qu912vPpkOyx6bPuEk="
|
74 |
|
75 | function accept_all (id, cb) {
|
76 | cb(null, true)
|
77 | }
|
78 | var ms = MultiServer([
|
79 | [ //net + secret-handshake
|
80 | require('multiserver/plugins/net')({port: 3333}),
|
81 | require('multiserver/plugins/shs')({
|
82 | keys: keys,
|
83 | appKey: appKey, //application key
|
84 | auth: accept_all
|
85 | }),
|
86 | ],
|
87 | [ //net + secret-handshake2
|
88 | //(not implemented yet, but incompatible with shs)
|
89 | require('multiserver/plugins/net')({port: 4444}),
|
90 | //this protocol doesn't exist yet, but it could.
|
91 | require('secret-handshake2')({
|
92 | keys: keys,
|
93 | appKey: appKey, //application key
|
94 | auth: accept_all
|
95 | }),
|
96 | ]
|
97 | ])
|
98 |
|
99 | console.log(ms.stringify())
|
100 |
|
101 | //=> net:<host>:3333~shs:<key>;net:<host>:4444~shs2:<key>
|
102 |
|
103 | //run two servers on two ports.
|
104 | //newer peers can connect directly to 4444 and use shs2.
|
105 | //this means the protocol can be _completely_ upgraded.
|
106 | ms.server(function (stream) {
|
107 | console.log('connection from', stream.address)
|
108 | })
|
109 |
|
110 | //connect to legacy protocol
|
111 | ms.client('net:<host>:3333~shs:<key>', function (err, stream) {
|
112 | //...
|
113 | })
|
114 |
|
115 | //connect to modern protocol
|
116 | ms.client('net:<host>:4444~shs2:<key>', function (err, stream) {
|
117 | //...
|
118 | })
|
119 |
|
120 | ```
|
121 |
|
122 | ### bridging
|
123 |
|
124 | By exposing multiple network transports as part of
|
125 | the same address, you can allow connections from
|
126 | peers that wouldn't have been able to connect otherwise.
|
127 |
|
128 | Regular servers can do TCP. Desktop clients can speak TCP,
|
129 | but can't create TCP servers that other desktop computers can connect to reliably.
|
130 | Browsers can use WebSockets and WebRTC.
|
131 | WebRTC gives you p2p, but needs an introducer.
|
132 | Another option is [utp](https://github.com/mafintosh/utp-native)
|
133 | - probably the most convenient, because it doesn't need an introducer
|
134 | on _every connection_ (but it does require some bootstrapping),
|
135 | but that doesn't work in the browser either.
|
136 |
|
137 | ``` js
|
138 | var MultiServer = require('multiserver')
|
139 |
|
140 | var ms = MultiServer([
|
141 | require('multiserver/plugins/net')({port: 1234}),
|
142 | require('multiserver/plugins/ws')({port: 2345})
|
143 | ])
|
144 |
|
145 | //start a server (for both protocols!)
|
146 | //returns function to close the server.
|
147 | var close = ms.server(function (stream) {
|
148 | //handle incoming connection
|
149 | })
|
150 |
|
151 | //connect to a protocol. uses whichever
|
152 | //handler understands the address (in this case, websockets)
|
153 | var abort = ms.client('ws://localhost:1234', function (err, stream) {
|
154 | //...
|
155 | })
|
156 |
|
157 | //at any time abort() can be called to cancel the connection attempt.
|
158 | //if it's called after the connection is established, it will
|
159 | //abort the stream.
|
160 | ```
|
161 |
|
162 | ## address format
|
163 |
|
164 | Addresses describe everything needed to connect to a peer.
|
165 | each address is divided into protocol sections separated by `~`.
|
166 | Each protocol section is divided itself by `:`. A protocol section
|
167 | starts with a name for that protocol, and then whatever arguments
|
168 | that protocol needs. The syntax of the address format is defined by [multiserver-address](https://github.com/ssbc/multiserver-address)
|
169 |
|
170 | For example, the address for my ssb pubserver is:
|
171 | ```
|
172 | net:wx.larpa.net:8008~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=
|
173 | ```
|
174 | That says use the `net` protocol (TCP) to connect to the domain `wx.larpa.net`
|
175 | on port `8008`, and then encrypt the session using `shs` ([secret-handshake](https://github.com/auditdrivencrypto/secret-handshake))
|
176 | to the public key `DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=`.
|
177 |
|
178 | Usually, the first section is a network protocol, and the rest are transforms,
|
179 | such as encryption or compression.
|
180 |
|
181 | Multiserver makes it easy to use multiple protocols at once. For example,
|
182 | my pub server _also_ supports `shs` over websockets.
|
183 |
|
184 | So, this is another way to connect:
|
185 |
|
186 | ```
|
187 | wss://wx.larpa.net~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=
|
188 | ```
|
189 |
|
190 | if your server supports multiple protocols, you can concatenate addresses with `;`
|
191 | and multiserver will connect to the first address it understands.
|
192 |
|
193 | ```
|
194 | net:wx.larpa.net:8008~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=;wss://wx.larpa.net~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=
|
195 | ```
|
196 | This means use net, or wss. In some contexts, you might have a peer that understands
|
197 | websockets but not net (for example a browser), as long as a server speaks at least
|
198 | one protocol that a peer can understand, then they can communicate.
|
199 |
|
200 | ## scopes
|
201 |
|
202 | address also have a scope. This relates to where they
|
203 | can be connected to. Default supported scopes are:
|
204 |
|
205 | * device - can connect only if on the same device
|
206 | * local - can connect from same wifi (local network)
|
207 | * public - can connect from public global internet.
|
208 |
|
209 | some transport plugins work only on particular scopes.
|
210 |
|
211 | when `stringify(scope)` is called, it will return
|
212 | just the accessible addresses in that scope.
|
213 |
|
214 | ## plugins
|
215 |
|
216 | A multiserver instance is set up by composing a selection
|
217 | of plugins that construct the networking transports,
|
218 | and transforms that instance supports.
|
219 |
|
220 | There are two types of plugins, transports and transforms.
|
221 |
|
222 | ### `net({port,host,scope})`
|
223 |
|
224 | TCP is a `net:{host}:{port}` port is not optional.
|
225 |
|
226 | ``` js
|
227 | var Net = require('multiserver/plugins/net')`
|
228 | Net({port: 8889, host: 'mydomain.com'}).stringify() => 'net:mydomain.com:8889'
|
229 | Net({port: 8889, host: 'fe80::1065:74a4:4016:6266:4849'}).stringify() => 'net:fe80::1065:74a4:4016:6266:4849:8889'
|
230 | Net({port: 8889, host: 'fe80::1065:74a4:4016:6266:4849', scope: 'device'}).stringify() => 'net:fe80::1065:74a4:4016:6266:4849:8889'
|
231 | ```
|
232 |
|
233 | ### `WebSockets({host,port,scope,handler?,key?,cert?})`
|
234 |
|
235 | create a websocket server. Since websockets are
|
236 | just a special mode of http, this also creates a http
|
237 | server. If `opts.handler` is provided, requests
|
238 | to the http server can be handled, this is optional.
|
239 |
|
240 | WebSockets `ws://{host}:{port}?` port defaults to 80 if not provided.
|
241 |
|
242 | WebSockets over https is `wss://{host}:{port}?` where port is
|
243 | 443 if not provided.
|
244 |
|
245 | If `opts.key` and `opts.cert` are provided as paths, a https server
|
246 | will be spawned.
|
247 |
|
248 | ``` js
|
249 | var WebSockets = require('multiserver/plugins/ws`)
|
250 |
|
251 | var ws = WebSockets({
|
252 | port: 1234,
|
253 | host: 'mydomain.com',
|
254 | handler: function (req, res) {
|
255 | res.end('<h1>hello</h1>')
|
256 | },
|
257 | scope:...
|
258 | })
|
259 |
|
260 | ws.stringify() => 'ws://mydomain.com:1234'
|
261 | ```
|
262 |
|
263 | ### `Onion()`
|
264 |
|
265 | Connect over tor using local proxy to dæmon (9050) or tor browser (9150).
|
266 | Both will be tried to find a suitable tor instance.
|
267 | The tor ports are unconfigurable. The standard
|
268 | tor ports are always used.
|
269 |
|
270 | This plugin does not support creating a server.
|
271 | You should use tor's configuration files to send incoming connections to a `net`
|
272 | instance as a hidden service.
|
273 |
|
274 | An accepted onion address looks like: `onion:{host}:{port}`
|
275 | port is not optional. This plugin does not return
|
276 | an address, so you must construct this address manually.
|
277 |
|
278 | ``` js
|
279 | var Onion = require('multiserver/plugins/onion`)
|
280 |
|
281 |
|
282 | var onion = WebSockets({
|
283 | //no config is needed except scope, but you
|
284 | //surely will use this with "public" which is the default
|
285 | //scope:'public'
|
286 | })
|
287 |
|
288 | ws.stringify() => null
|
289 | ```
|
290 |
|
291 |
|
292 | ### `Bluetooth({bluetoothManager})`
|
293 |
|
294 | The [multiserver-bluetooth](https://github.com/Happy0/multiserver-bluetooth) module implements a multiserver protocol for to communicate over Bluetooth Serial port.
|
295 |
|
296 | ### `reactnative = require('multiserver-rn-channel')`
|
297 |
|
298 | The [multiserver-rn-channel](http://npm.im/multiserver-rn-channel) module implementes
|
299 | a multiserver protocol for use inbetween the reactnative nodejs process and browser process.
|
300 |
|
301 | ### `SHS({keys,timeout?,appKey,auth})`
|
302 |
|
303 | Secret-handshake is `shs:{public_key}:{seed}?`. `seed` is used to create
|
304 | a one-time shared private key, that may enable a special access.
|
305 | For example, you'll see that ssb invite codes have shs with two sections
|
306 | following. Normally, only a single argument (the remote public key) is necessary.
|
307 |
|
308 | ``` js
|
309 | var SHS = require('multiserver/plugins/shs')
|
310 |
|
311 | var shs = SHS({
|
312 | keys: keys,
|
313 | timeout: //set handshake timeout, if unset falls through to secret-handshake default
|
314 | appKey: //sets an appkey
|
315 | auth: function (id, cb) {
|
316 | if(isNotAuthorized(id))
|
317 | cb(new Error())
|
318 | else
|
319 | cb(null, authenticationDetails)
|
320 | }
|
321 | })
|
322 | shs.stringify() => 'shs:{keys.publicKey.toString('base64')}
|
323 | ```
|
324 |
|
325 | note, if the `auth` function calls back a truthy value,
|
326 | it is considered authenticated. The value called back
|
327 | may be an object that represents details of the authentication.
|
328 | when a successful connection goes through `shs` plugin,
|
329 | the stream will have an `auth` property, which is the value called back from `auth`,
|
330 | and a `remote` property (the id of remote key).
|
331 |
|
332 | ### `Noauth({keys})`
|
333 |
|
334 | This authenticates any connection without any encryption.
|
335 | This should only be used on `device` scoped connections,
|
336 | such as if net is bound strictly to localhost,
|
337 | or a unix-socket. Do not use with ws or net bound to public addresses.
|
338 |
|
339 | ``` js
|
340 | var Noauth = require('multiserver/plugins/noauth')
|
341 |
|
342 | var noauth = Noauth({
|
343 | keys: keys
|
344 | })
|
345 | shs.stringify() => 'shs:{keys.publicKey.toString('base64')}
|
346 |
|
347 | ```
|
348 |
|
349 | streams passing through this will look like an authenticated shs connection.
|
350 |
|
351 | ### `Unix = require('multiserver/plugins/unix-socket')`
|
352 |
|
353 | network transport is unix socket. to connect to this
|
354 | you must have access to the same file system as the server.
|
355 |
|
356 | ``` js
|
357 | var Unix = require('multiserver/plugins/unix-socket')
|
358 |
|
359 | var unix = Unix({
|
360 | path: where_to_put_socket,
|
361 | scope: ... //defaults to device
|
362 | })
|
363 |
|
364 | unix.stringify() => "unix:{where_to_put_socket}"
|
365 | ```
|
366 |
|
367 |
|
368 | ### createMultiServer([[transport,transforms...],...])
|
369 |
|
370 | A server that runs multiple protocols on different ports can simply join them
|
371 | with `;` and clients should connect to their preferred protocol.
|
372 | clients may try multiple protocols on the same server before giving up,
|
373 | but generally it's unlikely that protocols should not fail independently
|
374 | (unless there is a bug in one protocol).
|
375 |
|
376 | an example of a valid multiprotocol:
|
377 | `net:{host}:{port}~shs:{key};ws:{host}:{port}~shs:{key}`
|
378 |
|
379 | ``` js
|
380 | var MultiServer = require('multiserver')
|
381 |
|
382 | var ms = MultiServer([
|
383 | [net, shs],
|
384 | [ws, shs],
|
385 | [unix, noauth]
|
386 | ])
|
387 |
|
388 | ms.stringify('public') => "net:mydomain.com:8889~shs:<key>;ws://mydomain.com:1234~shs:<key>"
|
389 | ms.stringify('device') => "unix:{where_to_put_socket}"
|
390 |
|
391 | ms.server(function (stream) {
|
392 | //now that all the plugins are combined,
|
393 | //ready to use as an actual server.
|
394 | })
|
395 | ```
|
396 |
|
397 | ## interfaces
|
398 |
|
399 | To construct a useful multiserver instance,
|
400 | one or more transport is each connected with zero
|
401 | or more transforms. The combine function is
|
402 | the default export from the `multiserver` module.
|
403 |
|
404 | ``` js
|
405 | var MultiServer = require('multiserver')
|
406 |
|
407 | var ms = MultiServer([
|
408 | [transport1, transform1],
|
409 | [transport2, transform2, transform3],
|
410 | ])
|
411 |
|
412 | var close = ms.server(function (stream) {
|
413 | //called when a stream connects
|
414 | }, onError, onListening)
|
415 | ```
|
416 |
|
417 | ```
|
418 | createMultiServer([[Transform, Transports*,...]], *]) => MultiServer
|
419 | ```
|
420 |
|
421 | a MultiServer has the same interface as a Transport,
|
422 | but using a combined multiserver instance as a transport
|
423 | is **not** supported.
|
424 |
|
425 | ## createTransport(Options) => Transport
|
426 |
|
427 | The transport exposes a name and the ability to
|
428 | create and connect to servers running that transport.
|
429 |
|
430 | ``` js
|
431 | Transport => {
|
432 | // that describes the sub protocols
|
433 | name,
|
434 | // connect to server with address addr.
|
435 | client (addr, cb),
|
436 | // start the server
|
437 | server (onConnect, onError, onListening),
|
438 | // return string describing how to connect to the server, aka, "the address"
|
439 | // the address applies to a `scope`.
|
440 | stringify(scope),
|
441 | // parse the addr,
|
442 | // normally this would probably return the
|
443 | // Options used to create the transport.
|
444 | parse(string) => Options
|
445 | }
|
446 | ```
|
447 |
|
448 | ## createTransform(options) => Transform
|
449 |
|
450 | ``` js
|
451 | Transform => {
|
452 | name: string,
|
453 | create(Options) => (stream, cb(null, transformed_stream)),
|
454 | parse (str) => Options,
|
455 | stringify() => string,
|
456 | }
|
457 | ```
|
458 |
|
459 | note the create method on a Transform takes Options,
|
460 | and returns a function that takes a stream and a callback,
|
461 | and then calls back the transformed stream.
|
462 | In all cases the stream is a [duplex stream](https://github.com/pull-stream/pull-stream)
|
463 |
|
464 | ## License
|
465 |
|
466 | MIT
|
467 |
|
468 |
|
469 |
|
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|