├── package.json ├── util.js ├── example.js ├── README.md ├── pptp.js ├── gre.js └── ppp.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-pptp", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "raw-socket": ">=1.2.2", 6 | "pcap": ">=1.0.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | this.ip2long = function(ip) { 3 | return ip.split('.').reduce(function(nn, octet, ii) { 4 | return (nn << 8) | octet; 5 | }, 0) >>> 0; 6 | } 7 | 8 | this.inherits = function(inherits) { 9 | return function(ctor, sup, props) { 10 | inherits(ctor, sup); 11 | for (var ii in props) { 12 | ctor.prototype[ii] = props[ii]; 13 | } 14 | } 15 | }(require('util').inherits); 16 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var PPTP = require('./pptp').PPTP; 3 | var raw = require('raw-socket'); 4 | 5 | function format(buf) { 6 | return buf.toString('hex').replace(/([a-f0-9]{4})/g, '$1 ').replace(/((?:[a-f0-9]{4} ){8})/g, '$1\n'); 7 | } 8 | 9 | var pptp = new PPTP; 10 | pptp.listen(); 11 | pptp.on('tunnel', function(tunnel) { 12 | tunnel.on('message', function(data) { 13 | console.log('> '+ format(data).replace(/\n/g, '\n ')+ '\n'); 14 | if (data[9] === 1 && data[20] === 8) { 15 | var tmp = new Buffer(data.length); 16 | data.copy(tmp); 17 | var src = tmp.readUInt32BE(12); 18 | tmp.writeUInt32BE(tmp.readUInt32BE(16), 12); 19 | tmp.writeUInt32BE(src, 16); 20 | // Did you know that in IPv4 swapping the src/dst does *not* change the header checksum?? 21 | // If it did, this is how you would recalculate it. 22 | // tmp.writeUInt16BE(0, 10); 23 | // raw.writeChecksum(tmp, 10, raw.createChecksum(tmp.slice(0, 20))); 24 | tmp[20] = 0; // Echo reply 25 | tmp.writeUInt16BE(0, 22); // ICMP checksum 26 | raw.writeChecksum(tmp, 22, raw.createChecksum(tmp.slice(20))); 27 | 28 | console.log('< '+ format(data).replace(/\n/g, '\n ')+ '\n'); 29 | tunnel.send(tmp); 30 | } 31 | }); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-pptp 2 | Simple PPTP VPN server I started working on for a project, but ultimately abandoned. Maybe this will 3 | be useful for someone. 4 | 5 | Features: 6 | * No authentication; credentials are not requested. 7 | * No encryption. 8 | * IPv4 only. 9 | * Unreliable handshake. No retransmission is implemented so if there is a single missed packet the 10 | handshake will timeout. 11 | * After established, though, the connection is fairly reliable. 12 | * Only tested with, and probably only works with, OS X clients. 13 | * Runs on OS X & Linux 14 | 15 | Note that the VPN does not create a network device on the server for incoming clients. Instead the 16 | client is simply a NodeJS object that you can send and receive IPv4 frames to directly. If you 17 | want to create a network device for your client (say for IP forwarding via iptables MASQ) you can 18 | use tun/tap fairly easily. All clients are assigned a hardcoded IP address of 10.0.1.2 and expect a 19 | gateway of 10.0.1.1, though these selections are arbitrary. 20 | 21 | Also note that connecting to a VPN server on localhost seems to be broken on OS X. If you connect 22 | through 127.0.0.1/localhost the handshake will fail completely. If you connect through another IP 23 | address the VPN connection is successfully created but the kernel doesn't seem to like routing GRE 24 | frames from a loopback device. I tried very hard to work around this and it seems like a problem 25 | inherent to the Darwin kernel; this was the main reason I abandoned the project. I guess there's 26 | not really many reasons for anyone to connect to a PPTP server on localhost. 27 | 28 | Included in the repository is a VPN server which will send ping responses from any host you ping. 29 | All other frames are ignored. 30 | 31 | # Setup 32 | ## Server 33 | ```sh 34 | git clone https://github.com/laverdet/node-pptp.git 35 | cd node-pptp 36 | npm install # installs raw-socket & pcap npm modules; required for VPN 37 | sudo node example # root is required 38 | ``` 39 | 40 | ## Client 41 | * **OS X PPTP VPN** 42 | * Account Name: _anything_ 43 | * Encryption: _None_ 44 | * **Authentication Settings...** 45 | * Password: _anything_ 46 | 47 | ```txt 48 | marcel@marcel ~ $ ping 1.2.3.4 49 | PING 1.2.3.4 (1.2.3.4): 56 data bytes 50 | 64 bytes from 1.2.3.4: icmp_seq=0 ttl=64 time=71.156 ms 51 | 64 bytes from 1.2.3.4: icmp_seq=1 ttl=64 time=72.612 ms 52 | 64 bytes from 1.2.3.4: icmp_seq=2 ttl=64 time=72.775 ms 53 | ``` 54 | -------------------------------------------------------------------------------- /pptp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Buffer = require('buffer').Buffer; 3 | var net = require('net'); 4 | var GRE = require('./gre').GRE; 5 | var PPP = require('./ppp').PPP; 6 | var ip2long = require('./util').ip2long; 7 | var EventEmitter = require('events').EventEmitter; 8 | var inherits = require('./util').inherits; 9 | 10 | this.PPTP = PPTP; 11 | 12 | function PPTP() { 13 | this.controlServer = net.createServer(function(socket) { 14 | new Client(this, socket); 15 | }.bind(this)); 16 | this.gre = new GRE; 17 | } 18 | 19 | inherits(PPTP, EventEmitter, { 20 | listen: function(addr, cb) { 21 | if (arguments.length === 1) { 22 | cb = addr; 23 | addr = undefined; 24 | } 25 | this.controlServer.listen(1723, addr, cb); 26 | this.gre.listen(); 27 | }, 28 | }); 29 | 30 | function Client(server, controlSocket) { 31 | this.controlSocket = controlSocket; 32 | this.state = 0; 33 | 34 | // Parse control stream 35 | var buffers = []; 36 | controlSocket.on('data', function(data) { 37 | // Get length field 38 | if (data.length) { 39 | buffers.push(data); 40 | } 41 | if (buffers[0].length === 1) { // as if 42 | if (buffers.length > 1) { 43 | var tmp = new Buffer(2); 44 | tmp[0] = buffers[0][0]; 45 | tmp[1] = buffers[1][0]; 46 | buffers[0] = tmp; 47 | buffers[1] = buffers[1].slice(1); 48 | } 49 | } 50 | if (buffers[0].length < 2) { 51 | return; 52 | } 53 | var len = buffers[0].readUInt16BE(0, true); 54 | var message; 55 | for (var ii = 0; ii < buffers.length; ++ii) { 56 | len -= buffers[ii].length; 57 | if (len === 0 && ii === 0) { 58 | // TODO: Reassemble chunks 59 | message = buffers.shift(); 60 | break; 61 | } else { 62 | return this.terminate(); 63 | } 64 | } 65 | 66 | // Got a message 67 | if (message.readUInt16BE(2) !== 1) { 68 | return this.terminate('Unknown control type'); 69 | } 70 | if (message.readUInt32BE(4) !== 0x1a2b3c4d) { 71 | return this.terminate('Invalid magic cookie'); 72 | } 73 | 74 | // Control message type 75 | switch (message.readUInt16BE(8)) { 76 | case 1: // Start-Control-Connection-Request 77 | if (this.state !== 0) { 78 | return this.terminate('Invalid state'); 79 | } else if (message.readUInt16BE(12) !== 256) { 80 | return this.terminate('Unknown version'); 81 | } 82 | this.state = 1; 83 | 84 | var response = new Buffer(156); 85 | response.fill(0); 86 | response.writeUInt16BE(156, 0); 87 | response.writeUInt16BE(1, 2); 88 | response.writeUInt32BE(0x1a2b3c4d, 4); 89 | response.writeUInt16BE(2, 8); // Start-Control-Connection-Reply 90 | response.writeUInt16BE(256, 12); 91 | response.writeUInt8(1, 14); 92 | response.writeUInt16BE(1, 24); 93 | controlSocket.write(response); 94 | break; 95 | 96 | case 7: // Outgoing-Call-Request 97 | if (this.state !== 1) { 98 | return this.terminate('Invalid state'); 99 | } 100 | var peerCallId = message.readUInt16BE(12); 101 | var callId = (Math.random() * 0x100000000) & 0xffff; 102 | var call = server.gre.open(controlSocket.localAddress, controlSocket.remoteAddress, callId, peerCallId); 103 | var ppp = new PPP(call, '10.0.1.1', '10.0.1.2'); 104 | server.emit('tunnel', ppp); 105 | 106 | var response = new Buffer(32); 107 | response.fill(0); 108 | response.writeUInt16BE(32, 0); 109 | response.writeUInt16BE(1, 2); 110 | response.writeUInt32BE(0x1a2b3c4d, 4); 111 | response.writeUInt16BE(8, 8); // Outgoing-Call-Reply 112 | response.writeUInt16BE(callId, 12); 113 | response.writeUInt16BE(peerCallId, 14); 114 | response.writeUInt8(1, 16); 115 | response.writeUInt32BE(100000000, 20); // connection speed 116 | response.writeUInt16BE(64, 24); // window 117 | controlSocket.write(response); 118 | break; 119 | } 120 | }.bind(this)); 121 | } 122 | -------------------------------------------------------------------------------- /gre.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Buffer = require('buffer').Buffer; 3 | var raw = require('raw-socket'); 4 | var pcap = process.platform === 'darwin' ? require('pcap') : undefined; 5 | var util = require('util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | var inherits = require('./util').inherits; 8 | var ip2long = require('./util').ip2long; 9 | var os = require('os'); 10 | 11 | this.GRE = GRE; 12 | 13 | /** 14 | * GRE listener and parser. 15 | */ 16 | function GRE() { 17 | this.sessions = {}; 18 | this.pcaps = Object.create(null); 19 | } 20 | 21 | GRE.prototype = { 22 | listen: function() { 23 | function ondata(packet) { 24 | // pcap returns raw ip layer packets. Parse out GRE payload 25 | var src = packet.readUInt32BE(12); 26 | var data = packet.slice(20, packet.readUInt16BE(2) + 4); 27 | 28 | // Parse GRE packet 29 | var flags = data.readUInt16BE(0); 30 | if ( 31 | (flags & 0x8000) || // checksum 32 | (flags & 0x4000) || // routing 33 | !(flags & 0x2000) || // key 34 | (flags & 0x0800) || // strict source 35 | (flags & 0x0700) || // recursion control (3 bits) 36 | (flags & 0x0078) || // flags (3 bits) 37 | (flags & 0x0007) !== 1 || // version 38 | data.readUInt16BE(2) !== 0x880b // protocol type 39 | ) { 40 | console.error('packet dropped', flags, data); 41 | return; 42 | } 43 | 44 | var hasSeq = flags & 0x1000; 45 | var hasAck = flags & 0x0080; 46 | var length = data.readUInt16BE(4); 47 | var callId = data.readUInt16BE(6); 48 | var session = this.sessions[src * 0x10000 + callId]; 49 | 50 | if (session) { 51 | if (hasSeq) { 52 | session.sendAck(data.readUInt32BE(8)); 53 | var payloadStart = hasAck ? 16 : 12; 54 | session.emit('message', data.slice(payloadStart, payloadStart + length)); 55 | } 56 | } 57 | }; 58 | 59 | this.greSocket = raw.createSocket({ 60 | protocol: 47, 61 | }); 62 | if (process.platform !== 'darwin') { // OS X kernel intercepts GRE packets 63 | this.greSocket.on('message', ondata.bind(this)); 64 | } else { 65 | this._ondata = ondata; 66 | } 67 | }, 68 | 69 | greListen: function(localAddr, remoteAddr) { 70 | if (process.platform !== 'darwin') { 71 | return; 72 | } 73 | 74 | var ifs = os.networkInterfaces(); 75 | for (var ii in ifs) { 76 | for (var jj in ifs[ii]) { 77 | if ( 78 | (ifs[ii][jj].address === localAddr && localAddr !== remoteAddr) || 79 | (ifs[ii][jj].internal && localAddr === remoteAddr) 80 | ) { 81 | // Got device 82 | if (this.pcaps[ii]) { 83 | return; 84 | } 85 | var cap = this.pcaps[ii] = pcap.createSession(ii, 'ip proto gre'); 86 | cap.on('packet', function(packet) { 87 | switch (packet.pcap_header.link_type) { 88 | case 'LINKTYPE_ETHERNET': 89 | return this._ondata.call(this, packet.slice(14)); 90 | case 'LINKTYPE_NULL': 91 | return this._ondata.call(this, packet.slice(4)); 92 | default: 93 | console.log('Unknown pcap header', packet.pcap_header.link_type); 94 | } 95 | }.bind(this)); 96 | return; 97 | } 98 | } 99 | } 100 | throw new Error('Could not listen on '+ localAddr); 101 | }, 102 | 103 | open: function(host, peer, callId, peerCallId, session) { 104 | this.greListen(host, peer); 105 | var session = ip2long(peer) * 0x10000 + callId; 106 | var call = new GRECall(this, peer, peerCallId, session); 107 | this.sessions[session] = call; 108 | return call; 109 | }, 110 | }; 111 | 112 | /** 113 | * A GRE session opened from GRE.open 114 | */ 115 | function GRECall(server, host, callId, session) { 116 | EventEmitter.call(this); 117 | this.server = server; 118 | this.host = host; 119 | this.callId = callId; 120 | this.session = session; 121 | this.seq = 0; 122 | this.ack = -1; 123 | this.nextAck = undefined; 124 | this.ackTick = false; 125 | } 126 | 127 | inherits(GRECall, EventEmitter, { 128 | sendAck: function(seq) { 129 | if (this.nextAck === undefined || seq > this.nextAck) { 130 | this.nextAck = seq; 131 | if (!this.ackTick) { 132 | this.ackTick = true; 133 | process.nextTick(function() { 134 | this.ackTick = false; 135 | if (this.nextAck > this.ack) { 136 | this.send(); 137 | } 138 | }.bind(this)); 139 | } 140 | } 141 | }, 142 | 143 | close: function() { 144 | delete this.server.sessions[this.session]; 145 | }, 146 | 147 | send: function(payload) { 148 | if (payload && !payload.length) { 149 | payload = undefined; 150 | } 151 | var ack; 152 | if (this.nextAck > this.ack) { 153 | ack = this.ack = this.nextAck; 154 | this.nextAck = undefined; 155 | } 156 | var buffer = new Buffer(8 + (payload ? payload.length + 4 : 0) + (ack ? 4 : 0)); 157 | buffer.fill(0); 158 | buffer.writeUInt16BE(0x2000 | 0x0001 | (payload ? 0x1000 : 0) | (ack ? 0x0080 : 0), 0); 159 | buffer.writeUInt16BE(0x880b, 2); 160 | if (payload) { 161 | buffer.writeUInt16BE(payload.length, 4); 162 | } 163 | buffer.writeUInt16BE(this.callId, 6); 164 | if (payload) { 165 | buffer.writeUInt32BE(this.seq++, 8); 166 | payload.copy(buffer, ack ? 16 : 12); 167 | } 168 | if (ack) { 169 | buffer.writeUInt32BE(ack, payload ? 12 : 8); 170 | } 171 | this.server.greSocket.send(buffer, 0, buffer.length, this.host, function(err) { 172 | if (err) console.error('Error sending GRE payload', err); 173 | }); 174 | }, 175 | }); 176 | -------------------------------------------------------------------------------- /ppp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var ip2long = require('./util').ip2long; 3 | var inherits = require('./util').inherits; 4 | var EventEmitter = require('events').EventEmitter; 5 | this.PPP = PPP; 6 | 7 | function parseConfigurationOptions(payload) { 8 | var options = {}; 9 | for (var ii = 0; ii < payload.length;) { 10 | var type = payload[ii], length = payload[ii + 1]; 11 | if (length < 2) throw new Error('Invalid length'); 12 | var option = payload.slice(ii + 2, ii + length); 13 | options[type] = option; 14 | ii += length; 15 | } 16 | return options; 17 | } 18 | 19 | function serializeConfigurationOptions(options) { 20 | var len = 0; 21 | for (var ii in options) { 22 | len += 2 + options[ii].length; 23 | } 24 | var buffer = new Buffer(len); 25 | var pos = 0; 26 | for (var ii in options) { 27 | buffer.writeUInt8(ii, pos); 28 | buffer.writeUInt8(options[ii].length + 2, pos + 1); 29 | options[ii].copy(buffer, pos + 2); 30 | pos += 2 + options[ii].length; 31 | } 32 | return buffer; 33 | } 34 | 35 | function PPP(datagram, hostip, peerip) { 36 | EventEmitter.call(this); 37 | 38 | var magic = ((Math.random() * 0x100000000) & 0xffffffff) >>> 0; 39 | var peerMagic = 0; 40 | var compressFieldAddress = false; 41 | 42 | this.datagram = datagram; 43 | 44 | function sendLCPPayload(code, id, payload, ipcp) { 45 | var buffer = new Buffer(8 + payload.length + (compressFieldAddress ? -2 : 0)); 46 | var offset = compressFieldAddress ? 0 : 2; 47 | if (!compressFieldAddress) { 48 | buffer.writeUInt16BE(0xff03, 0); 49 | } 50 | buffer.writeUInt16BE(ipcp ? 0x8021 : 0xc021, offset); 51 | buffer[offset + 2] = code; 52 | buffer[offset + 3] = id; 53 | buffer.writeUInt16BE(payload.length + 4, offset + 4); 54 | payload.copy(buffer, offset + 6); 55 | datagram.send(buffer); 56 | } 57 | 58 | // Send initial Configure-Request 59 | var mBuffer = new Buffer(4); 60 | mBuffer.writeUInt32BE(magic, 0); 61 | var options = { 62 | 2: new Buffer('00000000', 'hex'), 63 | 5: mBuffer, 64 | 7: new Buffer(0), 65 | 8: new Buffer(0), 66 | }; 67 | setTimeout(function() { 68 | sendLCPPayload(1, 1, serializeConfigurationOptions(options)); 69 | }, 10); 70 | 71 | datagram.on('message', function(message) { 72 | if (message.readUInt16BE(0) === 0xff03) { 73 | // 0xff03 are address & control field. if these are "compressed" they are just omitted 74 | message = message.slice(2); 75 | } 76 | 77 | var LCP = false; 78 | switch (message.readUInt16BE(0)) { 79 | case 0xc021: // LCP 80 | LCP = true; 81 | case 0x8021: // IPCP 82 | var payload = message.slice(6, message.readUInt16BE(4) + 2); 83 | var reqId = message.readUInt8(3); 84 | switch (message.readUInt8(2)) { 85 | case 1: // Configure-Request 86 | var options = parseConfigurationOptions(payload); 87 | var unknownOptions = {}; 88 | var negotiateOptions = {}; 89 | for (var ii in options) { 90 | if (LCP) { 91 | switch (Number(ii)) { 92 | case 2: // Async-Control-Character-Map 93 | break; 94 | 95 | case 5: // Magic-Number 96 | peerMagic = options[ii].readUInt32BE(0); 97 | break; 98 | 99 | case 7: // Protocol-Field-Compression 100 | case 8: // Address-and-Control-Field-Compression 101 | if (!options[7] || !options[8]) { 102 | // Don't trifle with only compressing one 103 | unknownOptions[ii] = options[ii]; 104 | } 105 | break; 106 | 107 | case 3: // Authentication-Protocol 108 | default: 109 | unknownOptions[ii] = options[ii]; 110 | break; 111 | } 112 | } else { 113 | switch (Number(ii)) { 114 | case 3: // IP Address 115 | var ip = options[ii].readUInt32BE(0); 116 | if (ip !== ip2long(peerip)) { 117 | var tmp = new Buffer(4); 118 | tmp.writeUInt32BE(ip2long(peerip), 0); 119 | negotiateOptions[ii] = tmp; 120 | } 121 | break; 122 | 123 | case 129: // Primary DNS 124 | case 131: // Secondary DNS 125 | default: 126 | unknownOptions[ii] = options[ii]; 127 | break; 128 | } 129 | } 130 | } 131 | 132 | // Configure-Reject? 133 | for (var ii in unknownOptions) { 134 | var rejectedOptions = serializeConfigurationOptions(unknownOptions); 135 | sendLCPPayload(4, reqId, rejectedOptions, !LCP); 136 | return; 137 | } 138 | 139 | // Configure-Nak? 140 | for (var ii in negotiateOptions) { 141 | var rejectedOptions = serializeConfigurationOptions(negotiateOptions); 142 | sendLCPPayload(3, reqId, rejectedOptions, !LCP); 143 | return; 144 | } 145 | 146 | // Configure-Ack 147 | sendLCPPayload(2, reqId, payload, !LCP); 148 | if (LCP) { 149 | if (options[7] && options[8]) { 150 | this.compressFieldAddress = compressFieldAddress = true; 151 | } 152 | var tmp = new Buffer(4); 153 | tmp.writeUInt32BE(ip2long(hostip), 0); 154 | var options = { 155 | 3: tmp, 156 | }; 157 | setTimeout(function() { 158 | sendLCPPayload(1, 1, serializeConfigurationOptions(options), true); 159 | }, 100); 160 | } 161 | break; 162 | 163 | case 9: // Echo-Request 164 | if (payload.readUInt32BE(0) === peerMagic) { 165 | var buffer = new Buffer(4); 166 | buffer.writeUInt32BE(magic, 0); 167 | sendLCPPayload(10, reqId, buffer, !LCP); 168 | } 169 | break; 170 | } 171 | break; 172 | 173 | case 0x0021: // IP Packets 174 | this.emit('message', message.slice(2)); 175 | break; 176 | 177 | case 0x2145: // "Compressed" IP packet. 0x45 is start of IP frame 178 | this.emit('message', message.slice(1)); 179 | break; 180 | 181 | default: // Send Prot-Reject 182 | sendLCPPayload(8, 1, message); 183 | break; 184 | } 185 | }.bind(this)); 186 | } 187 | 188 | inherits(PPP, EventEmitter, { 189 | send: function(data) { 190 | var packet = new Buffer(data.length + (this.compressFieldAddress ? 1 : 4)); 191 | if (this.compressFieldAddress) { 192 | packet[0] = 0x21; 193 | } else { 194 | packet.writeUInt32BE(0xff030021, 0); 195 | } 196 | data.copy(packet, this.compressFieldAddress ? 1 : 4); 197 | return this.datagram.send(packet); 198 | }, 199 | }); 200 | --------------------------------------------------------------------------------