UNPKG

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