Giter Club home page Giter Club logo

ssb-tunnel's Introduction

ssb-tunnel

Indirectly connect to a peer by tunneling through another connection. If A is connected to B, and C is connected to B, this allows C to connect to A by using B as a proxy or "portal".

With this module, a peer A with an unstable IP address can make a long term connection to a portal B, another peer C can then connect to that portal, and tunnel back up the client connection C->B, giving us a connect through B, C-(B)->A.

,---,      ,---,     ,---,
|   |----->|   |<----|   |
| A |<=====|-B-|<====| C |
|   |----->|   |<----|   |
`---`      `---`     `---`

A connects to B, and waits to receive tunnel connections. C connects to B, and then requests a tunnel through that connection (B-C) to A. B calls A, creating an incoming tunnel, and attaches one end to C's request, C then uses the standard handshake to authenticate A.

Notice that for the tunnel, A is the server and C is the client (client calls, server answers) but B is just the portal. The tunnel is inside the outer connections, which means it is encrypted twice. This means A and C can mutually authenticate each other, and B cannot see the content of their connection.

The arrows represent the direction of the connection - from the client, pointing to the server. Notice the B<=C tunnel is the same direction as the B<-C container, but the A<=B tunnel is the opposite direction as the A->B container.

address

tunnel addresses are multiserver style:

tunnel:<portal_id>:<target_id>:<instance>? for example: tunnel:@7MG1hyfz8SsxlIgansud4LKM57IHIw2Okw/hvOdeJWw=.ed25519:@1b9KP8znF7A4i8wnSevBSK2ZabI/Re4bYF/Vh3hXasQ=.ed25519~shs:1b9KP8znF7A4i8wnSevBSK2ZabI/Re4bYF/Vh3hXasQ= (instance is optional)

It is assumed that a peer who wishes to be a client to target already has a means to connect to portal. The address of the portal is left out, so that the client can use anything, and also, to better preserve the privacy of the portal.

For the protocol portion of the multiserver address, tunnel:portal:target:instance this will include the shs portion for the portal. instance is just an integer that tells the server which ssb-tunnel instance the client wants to connect to if there are multiple.

target is a ssb feed id, which represents the peer. This tells the portal that C wants a connection to A. portal tells A how to connect to B.

config

Assuming this plugin is already installed and enabled on your pub server. You need to configure sbot with an incoming section so that it can receive tunnel connections:

incoming: {
  tunnel: [{scope: 'public', portal: <pub_id>, transform:'shs'}]
}

then, another peer will need to have the outgoing config:

outgoing: {
  tunnel: [{transform:'shs'}]
}

and have an address for pub, can do: sbot.gossip.connect('tunnel:<pub_id>:<your_id>~shs:your_key', function (err, rpc) {...}) and they'll have connection through pub to you!

privacy ideas

Instead of revealing the id of the portal, just use the hmac(portal_id, your_id) so peers that do not know of the portal do not learn about it from your address. That way only friends can connect to you.

how it works behind the scenes

for 3 peers, A, B, and C. A being the client-side server, which will receive the tunnel connection, B being the portal, and C being the client who connects to A via B.

First A connects to B normally, then calls B.tunnel.announce() This informs B that A would like to receive connections tunneled though B. (B puts A into a table of endpoints it can provide tunnels to)

Then C connects to B, and then calls B.tunnel.connect({id: A.id}) B then checks if it can provide a connection to A, which it can, and calls endpoints[A.id].tunnel.connect({id: A}) returning this stream to B (B is now connected to A via C).

B then initiates a secret-handshake through the tunnel, hiding subsequent content from B.

License

MIT

ssb-tunnel's People

Contributors

dominictarr avatar arj03 avatar staltz avatar christianbundy avatar cryptix avatar mixmix avatar

Stargazers

Anatoly Chernov avatar Suri avatar nichoth avatar Osvaldo avatar Stefano Kocka avatar  avatar Andrew NS Yeow avatar  avatar Matt MacGillivray avatar  avatar PrivyERA avatar Connor Turland avatar cuiwm avatar Eli Mallon avatar Mayeu avatar Juri Hahn avatar Kevin Segal avatar Jeswin avatar luandro avatar Timothy avatar  avatar Jonas Mendes avatar

Watchers

 avatar Jan Bölsche avatar  avatar  avatar Osvaldo avatar James Cloos avatar  avatar  avatar

ssb-tunnel's Issues

week 9 doc-drive:

  • reposting from ssb
    canonical 'butt' link: %d4zAqENrO2+Ng1r9X/xgZ0oEbRu5Yw+vy/lHaLzglIw=.sha256

doc drive week 9: ssb-tunnel module triage

  • i'll repost this as an issue on gh later but currently unwilling to deal with the hassles of airport wifi! :-p

rate the current state of the documentation

7/10
the setup details are very clear and a simple example is provided.

is it clear what this is used for? is anything confusing or weird?

there could be some increased clarity on how to initiate a connection through a portal. it looks like you can make rpc calls on the portal (bob) like bob.announce(). is this currently only done via scripts? or will is be included in the CLI? i.e ssb-server tunnel announce ... or ssb-server tunnel connect ...

also some additional details on what happens once a connection through a portal takes place. is it ssb gossip? additional protocol bootstrapping? see %iqXoP//+WnBt3UhTKK2uKjlV4EojMn8IjY5/rSLKdPY=.sha256

is there api documentation?

there are a few api's demonstrated in the examples but no formal api listing.

are there any undocumented methods?

yes.
announce(opts) rpc method called on the portal by the peer "hosting" the tunnel (the server peer). takes an opts arg but does not look like it gets used?

connect(opts) rpc method called on the portal by the "client peer". opts should contain {id: <hosting peer id>}

ping() rpc method that can be called on the portal. returns current date

time spent performing triage

45 mins

Handling duplex termination: unexpected hangup

In my proof of concept, I can connect together two clients of a common tunnel server. However, when I close one of the client apps, the termination is not graceful, instead:

  • tunnel server logs the error below
  • other client crashes, upon receiving this same error from the tunnel server
Error: callback not provided
    at noop (/root/ssb-room/node_modules/ssb-server/node_modules/muxrpc/api.js:14:18)
    at /root/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:18:14
    at /root/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:10:5
    at /root/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:59:17
    at /root/ssb-room/node_modules/ssb-server/node_modules/pull-stream/sinks/drain.js:20:24
    at PacketStreamSubstream.weird.read (/root/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:33:7)
    at PacketStreamSubstream.destroy (/root/ssb-room/node_modules/ssb-server/node_modules/packet-stream/index.js:287:26)
    at PacketStream.destroy (/root/ssb-room/node_modules/ssb-server/node_modules/packet-stream/index.js:81:24)
    at PacketStream.write (/root/ssb-room/node_modules/ssb-server/node_modules/packet-stream/index.js:133:41)
  Error: unexpected hangup
    at Object.cb (/root/ssb-room/node_modules/ssb-server/node_modules/pull-box-stream/index.js:117:44)
    at drain (/root/ssb-room/node_modules/ssb-server/node_modules/pull-reader/index.js:46:23)
    at /root/ssb-room/node_modules/ssb-server/node_modules/pull-reader/index.js:63:18
    at /root/ssb-room/node_modules/ssb-server/node_modules/pull-reader/index.js:114:13
    at drain (/root/ssb-room/node_modules/ssb-server/node_modules/stream-to-pull-stream/index.js:126:18)
    at Socket.<anonymous> (/root/ssb-room/node_modules/ssb-server/node_modules/stream-to-pull-stream/index.js:143:5)
    at Socket.emit (events.js:194:15)
    at endReadableNT (_stream_readable.js:1125:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)

I'm not sure how to fix this, but my initial guess is that the clients don't know how gracefully terminate the duplex streams (using pull-goodbye somehow?) and the tunnel server looks like a MITM to them. Any ideas?

Error: pull-reader: read exceeded timeout

I've been struggling to get a simple test running for two clients and one portal, I keep hitting the error:

node_modules/ssb-server/node_modules/muxrpc/api.js:14
  if (err) throw explain(err, 'callback not provided')
           ^

Error: callback not provided
    at noop (node_modules/ssb-server/node_modules/muxrpc/api.js:14:18)
    at node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:18:14
    at node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:10:5
    at node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:59:17
    at node_modules/ssb-server/node_modules/pull-stream/sinks/drain.js:20:24
    at PacketStreamSubstream.weird.read (node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:33:7)
    at PacketStream._onstream (node_modules/ssb-server/node_modules/packet-stream/index.js:224:13)
    at PacketStream.write (node_modules/ssb-server/node_modules/packet-stream/index.js:135:41)
    at node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:56:15
  Error: pull-reader: read exceeded timeout
    at Timeout._onTimeout (/data/data/se.manyver/files/nodejs-project/index.js:53114:10)
    at ontimeout (timers.js:437:11)
    at tryOnTimeout (timers.js:301:5)
    at listOnTimeout (timers.js:264:5)
    at Timer.processTimers (timers.js:224:10)

on both portal and clients. All three crash, at almost the same time (plus or minus 2 seconds). Is there anything obvious I'm missing here? Here's my setup:


  • Portal @3uQd... running a simple fork of ssb-tunnel
  • Client @Dyora... running a fork of ssb-tunnel that I haven't published yet, called "room-client"
  • Client @Mhxnwib... (similar to the one above)

Portal @3uQd... logs

19:47:12.968Z multiserver:net Listening on 0.0.0.0:8008
19:47:12.984Z ssb:conn-db Loaded conn.json into ConnDB in memory
19:47:27.607Z ssb:conn-hub RPC client @Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519 connected to us, but not via conn-hub
19:47:27      ssb:room:tunnel received endpoint announcement from: @Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519
19:48:00.347Z ssb:conn-hub RPC client @Mhxnwib9J7WE/b9V44dJQIx3uTMl8eK6knb/ZQwaCp4=.ed25519 connected to us, but not via conn-hub
19:48:00      ssb:room:tunnel received endpoint announcement from: @Mhxnwib9J7WE/b9V44dJQIx3uTMl8eK6knb/ZQwaCp4=.ed25519
19:48:02      ssb:room:tunnel received tunnel request for target @Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519 from @Mhxnwib9J7WE/b9V44dJQIx3uTMl8eK6knb/ZQwaCp4=.ed25519
/home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/api.js:14
  if (err) throw explain(err, 'callback not provided')
           ^

Error: callback not provided
    at noop (/home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/api.js:14:18)
    at /home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:18:14
    at /home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:10:5
    at /home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:59:17
    at /home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/pull-stream/sinks/drain.js:20:24
    at PacketStreamSubstream.weird.read (/home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:33:7)
    at PacketStream._onstream (/home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/packet-stream/index.js:224:13)
    at PacketStream.write (/home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/packet-stream/index.js:135:41)
    at /home/node/.npm-global/lib/node_modules/ssb-room/node_modules/ssb-server/node_modules/muxrpc/pull-weird.js:56:15
  Error: pull-reader: read exceeded timeout
    at Timeout._onTimeout (/data/data/se.manyver/files/nodejs-project/index.js:53114:10)
    at ontimeout (timers.js:437:11)
    at tryOnTimeout (timers.js:301:5)
    at listOnTimeout (timers.js:264:5)
    at Timer.processTimers (timers.js:224:10)

Client @Dyora... logs

19:47:26.713 multiserver:net Listening on 192.168.43.252:26831
19:47:27.098 ssb:conn-db Loaded conn.json into ConnDB in memory
19:47:27.579 ssb:conn-hub connecting to net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:47:27.637 ssb:conn-hub disconnecting from net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:47:27.640 ssb:conn-hub disconnected from net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:47:27.924 ssb:conn-hub connected to net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:47:27.928 room-client will try to call tunnel.endpoints() at new gossip peer: @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:47:28.101 ssb:conn-hub connected to net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:47:28.104 room-client will try to call tunnel.endpoints() at new gossip peer: @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:47:28.145 room-client got endpoints from @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519: []
19:47:28.147 room-client server setupPortal(@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519)
19:47:28.149 room-client server setting up handler for @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:47:28.171 room-client server announcing to portal:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:47:28.215 room-client got endpoints from @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519: []
19:47:28.215 room-client server WOULD redundantly setupPortal(@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519) but cancelled that
19:47:28.267 room-client server - SUCCESS establishing portal:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:02.998 room-client received incoming connect({"target":"@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519","portal":"@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519"})
19:48:03.008 room-client connect() will resolve because handler exists
19:48:03.013 room-client server HANDLER will call onConnect for the stream.address: tunnel:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519
19:48:07.934 server error, from tunnel:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519~shs:
19:48:07.934 Error: pull-reader: read exceeded timeout
19:48:07.934     at Timeout._onTimeout (/data/data/se.manyver/files/nodejs-project/index.js:53114:10)
19:48:07.934     at ontimeout (timers.js:437:11)
19:48:07.934     at tryOnTimeout (timers.js:301:5)
19:48:07.934     at listOnTimeout (timers.js:264:5)
19:48:07.934     at Timer.processTimers (timers.js:224:10)
19:48:07.951 ssb:conn-hub disconnected from net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=

Client @Mhxnwib... logs

19:47:43.997 multiserver:net Listening on 192.168.1.100:26831
19:47:44.193 ssb:conn-db Loaded conn.json into ConnDB in memory
19:48:01.141 ssb:conn-hub connecting to net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:48:01.316 ssb:conn-hub connected to net:165.22.204.212:8008~shs:3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=
19:48:01.319 room-client will try to call tunnel.endpoints() at new gossip peer: @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:01.437 room-client got endpoints from @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519: [{"id":"@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519","address":"tunnel:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519"}]
19:48:01.438 room-client server setupPortal(@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519)
19:48:01.441 room-client server setting up handler for @3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:01.462 room-client server announcing to portal:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:01.535 room-client server - SUCCESS establishing portal:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:03.443 room-client will gossip.connect(tunnel:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519)
19:48:03.495 ssb:conn-hub connecting to tunnel:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519
19:48:03.507 room-client client will retrieve portal peer:@3uQdTntgfgUZKEMlWsKerARTUxQjSWQuaxu/BtnZsaw=.ed25519
19:48:03.507 room-client client has portal connected, will tunnel to target:@Dyora4YFZeLAo1DYx4VWbUmMpMoTDg1adm/5siXxVQQ=.ed25519
19:48:08.827 Error: callback not provided
19:48:08.827     at noop (/data/data/se.manyver/files/nodejs-project/index.js:58808:18)
19:48:08.827     at /data/data/se.manyver/files/nodejs-project/index.js:88100:14
19:48:08.827     at /data/data/se.manyver/files/nodejs-project/index.js:88092:5
19:48:08.827     at source (/data/data/se.manyver/files/nodejs-project/index.js:88126:20)
19:48:08.827     at Function.reader.abort (/data/data/se.manyver/files/nodejs-project/index.js:53183:7)
19:48:08.827     at Object.abort (/data/data/se.manyver/files/nodejs-project/index.js:23219:16)
19:48:08.827     at abort (/data/data/se.manyver/files/nodejs-project/index.js:84421:39)
19:48:08.827     at Object.cb (/data/data/se.manyver/files/nodejs-project/index.js:84426:24)
19:48:08.827     at drain (/data/data/se.manyver/files/nodejs-project/index.js:53144:23)
19:48:08.827   Error: pull-reader: read exceeded timeout
19:48:08.827     at Timeout._onTimeout (/data/data/se.manyver/files/nodejs-project/index.js:53114:10)
19:48:08.827     at ontimeout (timers.js:437:11)
19:48:08.827     at tryOnTimeout (timers.js:301:5)
19:48:08.827     at listOnTimeout (timers.js:264:5)
19:48:08.827     at Timer.processTimers (timers.js:224:10)

Multi-hop tunneling?

Since this plugin is the same for both portals and clients, it seems like clients could also become portals, so there is the prospect of doing:

,---,      ,---,     ,---,     ,---,
|   |----->|   |<----|   |<----|   |
| A |<=====|-B-|<====| C |<====| D |
|   |----->|   |<----|   |<----|   |
`---`      `---`     `---`     `---`

And effectively having a tunnel D-(C)-(B)->A. Is this a real possibility? Is this a good idea or a bad idea? I can imagine it being possible in Bluetooth, at least.

I don't intend to build this, but I stumbled upon this theoretical possibility and it made me wonder that there is no way I can prevent it from happening. Or is there?

Error: "shs.client: error when expecting server to accept challenge (phase 1). possibly the server is busy, does not speak shs or uses a different application cap"

Browser code example is reporting this error when trying to establish a connection to the target, but server to server tunneling works fine, just having issues with websocket/browser to server tunneling.

Error: "shs.client: error when expecting server to accept challenge (phase 1). 
possibly the server is busy, does not speak shs or uses a different application cap"

The portal connection seems to be working fine though, just not tunneling to the target. With config.tunnel.logging turned on, I get the following output on the pub when tunnel.connect gets called from the browser.

tunnel:portal - received tunnel request for target:@<target public key>=.ed25519, from:@CyJtmMEk9+vdm8NUQsky9ZuTU5axRaRKH+qP3X2puRM=.ed25519

pub/portal details

node: 10.14
ssb-server: 15.1.1
ssb-tunnel: 2.0.0

target details

node: 10.15.1
ssb-server: 14.2.1

target config.connections

  "connections": {
    "incoming": {
       "net": [
         { "scope": "private", "transform": "shs", "port": 8008, "host": "localhost" }
       ],
       "tunnel": [{
         "scope": "public",
         "transform": "shs",
         "portal": "@<pub/portal public key=.ed25519"
       }]
    }
  }

`stream.address` definition is wrong

When a connection stream is created and it is passed to onConnection(stream), we calculate the address as:

ssb-tunnel/index.js

Lines 129 to 132 in 1afa107

handlers[instance] = function (stream, id) {
stream.address = 'tunnel:'+portal+':'+id
onConnect(stream)
}

If Alice receives a connection from Bob through a Portal, then that address is supposed to be tunnel:${portalid}:${bobid}, where bobid is provided as the 2nd argument in

handlers[opts.port](streams[0], this.id)

Here's the problem: when Portal calls Alice's connect(), the RPC this.id refers to the Portal, so the address turns out to be tunnel:${portalid}:${portalid}.


The solution should be something like: when the Portal forwards the connect to opts.target, it should also define opts.origin. Then, once the target handles it, it will have info on the origin. What do you think @dominictarr ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.