├── .gitignore ├── .npmignore ├── .vscode └── launch.json ├── test ├── test-live-outgoing-player-state.js ├── test-live-incoming-payload-3.js ├── test-live-incoming-payload-2.js ├── test-live-incoming-chat.js ├── test-live-incoming-player-state.js ├── test-live-incoming-player-state-specific.js ├── test-live-zwift-logger.js ├── test-live-zwift.js ├── test-live-zwift-packet-info.js ├── test-live-zwift-packet-info-debug.js ├── test-outgoing-player-state.js └── test-live-zwift-debug.js ├── package.json ├── README.md ├── zwiftMessages.proto ├── ZwiftPacketMonitor.js ├── ZwiftPacketMonitorDebug.js ├── ZwiftPacketMonitorLogger.js └── ZwiftPacketMonitorSource.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | ZwiftPacketMonitorSource.js 3 | ZwiftPacketMonitorDebug.js 4 | ZwiftPacketMonitorLogger.js 5 | .vscode/ 6 | .gitignore -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\test\\test-live-zwift-packet-info-debug.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /test/test-live-outgoing-player-state.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 31 | console.log(serverWorldTime, playerState) 32 | }) 33 | 34 | 35 | monitor.start() 36 | } 37 | 38 | -------------------------------------------------------------------------------- /test/test-live-incoming-payload-3.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | monitor.on('incomingPayload3', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 31 | console.log(serverWorldTime, dstPort, dstAddr, payload) 32 | }) 33 | 34 | monitor.start() 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/test-live-incoming-payload-2.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | monitor.on('incomingPayload2', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 31 | console.log(serverWorldTime, dstPort, dstAddr, payload) 32 | }) 33 | 34 | monitor.start() 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/test-live-incoming-chat.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | monitor.on('incomingPlayerSentMessage', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 31 | console.log(serverWorldTime, dstPort, dstAddr, payload) 32 | }) 33 | 34 | monitor.start() 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/test-live-incoming-player-state.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | 31 | monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 32 | console.log(serverWorldTime, dstPort, dstAddr, playerState) 33 | }) 34 | 35 | monitor.start() 36 | } 37 | 38 | -------------------------------------------------------------------------------- /test/test-live-incoming-player-state-specific.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | 31 | monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 32 | if (playerState.id == 99999999) { 33 | console.log(serverWorldTime, dstPort, dstAddr, playerState) 34 | } 35 | }) 36 | 37 | monitor.start() 38 | } 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zwfthcks/zwift-packet-monitor", 3 | "version": "0.6.0", 4 | "description": "monitor Zwift UDP and TCP packets and emit events for player state updates", 5 | "main": "ZwiftPacketMonitor.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "test-live": "node test/test-live-zwift.js", 9 | "test-live-outgoing": "node test/test-live-outgoing-player-state.js", 10 | "test-live-debug": "node test/test-live-zwift-debug.js", 11 | "test-live-logger": "node test/test-live-zwift-logger.js", 12 | "preprocess": "npx preprocessor ZwiftPacketMonitorSource.js . > ZwiftPacketMonitor.js", 13 | "preprocess-debug": "npx preprocessor ZwiftPacketMonitorSource.js . -DEBUG=true > ZwiftPacketMonitorDebug.js", 14 | "preprocess-logger": "npx preprocessor ZwiftPacketMonitorSource.js . -LOGGER=true > ZwiftPacketMonitorLogger.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/jeroni7100/zwift-packet-monitor.git" 19 | }, 20 | "keywords": [ 21 | "zwift" 22 | ], 23 | "author": "Christian Wiedmann & Jesper Rosenlund Nielsen", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/jeroni7100/zwift-packet-monitor/issues" 27 | }, 28 | "homepage": "https://github.com/jeroni7100/zwift-packet-monitor#readme", 29 | "dependencies": { 30 | "cap": "^0.2.1", 31 | "protobufjs": "^6.11.2" 32 | }, 33 | "directories": { 34 | "test": "test" 35 | }, 36 | "devDependencies": { 37 | "internal-ip": "^6.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/test-live-zwift-logger.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitorLogger') 3 | var ZwiftPacketMonitorLogger = require('../ZwiftPacketMonitorLogger.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | // try { 10 | // console.log('Require: cap') 11 | // var Cap = require('cap').Cap; 12 | // console.log('cap required') 13 | // // console.log(Cap, Cap.deviceList()) 14 | // } catch(e) { 15 | // console.log(e) 16 | // } 17 | 18 | const ip = require('internal-ip').v4.sync(); 19 | start(ip); 20 | 21 | 22 | function start(ip) { 23 | 24 | // if (ZwiftPacketMonitorLogger && Cap && ip) { 25 | if (ZwiftPacketMonitorLogger && ip) { 26 | 27 | console.log('Listening on: ', ip); //, JSON.stringify(Cap.findDevice(ip),null,4)); 28 | 29 | var now = new Date() 30 | var subdir = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}` 31 | 32 | const monitor = new ZwiftPacketMonitorLogger(ip, { 33 | dir: `c:/temp/${subdir}`, 34 | incoming: true, 35 | outgoing: true, 36 | payload: true 37 | }) 38 | 39 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 40 | console.log('outgoingPlayerState'); 41 | }) 42 | 43 | monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 44 | console.log('incomingPlayerState'); 45 | }) 46 | 47 | monitor.start() 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zwift-packet-monitor 2 | 3 | This module monitors Zwift UDP traffic on port 3022 and TCP traffic on port 3023 (both contains protobuf payloads) and emits events for 4 | 5 | - player state updates (incomingPlayerState and outgoingPlayerState) 6 | - ride ons (incomingPlayerGaveRideOn) 7 | - chat messages (incomingPlayerSentMessage) 8 | - player entered world (incomingPlayerEnteredWorld) 9 | 10 | ## Install 11 | 12 | ### Prerequisites 13 | On Windows this requires Npcap installed with WinPcap API compatibility. On other systems, libpcap should be installed. 14 | 15 | ### Installation 16 | 17 | The fork by jeroni is published as @zwfthcks/zwift-packet-monitor on NPM. 18 | 19 | ``` 20 | npm install @zwfthcks/zwift-packet-monitor 21 | ```` 22 | 23 | Alternatively, install with npm from GitHub 24 | 25 | ``` 26 | npm install https://github.com/jeroni7100/zwift-packet-monitor 27 | ```` 28 | 29 | or download/clone from GitHub and install directly from your local copy, for example like this if the copy resides in a sibling folder to your project: 30 | 31 | ``` 32 | npm install ../zwift-packet-monitor 33 | ``` 34 | 35 | 36 | The original version by wiedmann can be installed from NPM: 37 | 38 | ``` 39 | npm install zwift-packet-monitor 40 | ``` 41 | 42 | ## Usage 43 | 44 | (Assumes installation from NPM) 45 | 46 | ```javascript 47 | const ZwiftPacketMonitor = require('@zwfthcks/zwift-packet-monitor') 48 | 49 | // interface is cap interface name (can be device name or IP address) 50 | const monitor = new ZwiftPacketMonitor(interface) 51 | 52 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 53 | console.log(playerState) 54 | }) 55 | 56 | monitor.on('incomingPlayerState', (playerState, serverWorldTime) => { 57 | console.log(playerState) 58 | }) 59 | 60 | // The Zwift server sends states in batches. This event is emitted at the end of each incoming batch 61 | monitor.on('endOfBatch', () => { 62 | console.log('end of batch') 63 | }) 64 | 65 | monitor.start() 66 | ``` 67 | 68 | 69 | # Relevant links 70 | 71 | Npcap https://nmap.org/npcap/ 72 | 73 | 74 | ## Development tools 75 | 76 | Uses preprocessor.js (https://www.npmjs.com/package/preprocessor) via npx to build ZwiftPacketMonitor.js from ZwiftPacketMonitorSource.js 77 | 78 | Build with 79 | ``` 80 | npm run preprocess 81 | ``` 82 | -------------------------------------------------------------------------------- /test/test-live-zwift.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 31 | console.log(serverWorldTime, dstPort, dstAddr, playerState) 32 | }) 33 | 34 | monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 35 | console.log(serverWorldTime, dstPort, dstAddr, playerState) 36 | }) 37 | 38 | monitor.on('incomingPlayerGaveRideOn', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 39 | console.log(serverWorldTime, dstPort, dstAddr, payload) 40 | }) 41 | 42 | monitor.on('incomingPlayerSentMessage', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 43 | console.log(serverWorldTime, dstPort, dstAddr, payload) 44 | }) 45 | 46 | monitor.on('incomingPlayerEnteredWorld', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 47 | console.log(serverWorldTime, dstPort, dstAddr, payload) 48 | }) 49 | 50 | monitor.on('incomingPayload2', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 51 | console.log(serverWorldTime, dstPort, dstAddr, payload) 52 | }) 53 | 54 | monitor.on('incomingPayload3', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 55 | console.log(serverWorldTime, dstPort, dstAddr, payload) 56 | }) 57 | 58 | // The Zwift server sends states in batches. This event is emitted at the end of each incoming batch 59 | monitor.on('endOfBatch', () => { 60 | console.log('end of batch') 61 | }) 62 | 63 | monitor.start() 64 | } 65 | 66 | -------------------------------------------------------------------------------- /test/test-live-zwift-packet-info.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | // monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 31 | // console.log(serverWorldTime, dstPort, dstAddr, playerState) 32 | // }) 33 | 34 | // monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 35 | // console.log(serverWorldTime, dstPort, dstAddr, playerState) 36 | // }) 37 | 38 | // monitor.on('incomingPlayerGaveRideOn', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 39 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 40 | // }) 41 | 42 | // monitor.on('incomingPlayerSentMessage', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 43 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 44 | // }) 45 | 46 | // monitor.on('incomingPlayerEnteredWorld', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 47 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 48 | // }) 49 | 50 | // monitor.on('incomingPayload2', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 51 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 52 | // }) 53 | 54 | // monitor.on('incomingPayload3', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 55 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 56 | // }) 57 | 58 | // // The Zwift server sends states in batches. This event is emitted at the end of each incoming batch 59 | // monitor.on('endOfBatch', () => { 60 | // console.log('end of batch') 61 | // }) 62 | 63 | monitor.start() 64 | } 65 | 66 | -------------------------------------------------------------------------------- /test/test-live-zwift-packet-info-debug.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitor') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitorDebug.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | var Cap = require('cap').Cap; 11 | } catch(e) { 12 | console.log(e) 13 | } 14 | 15 | const ip = require('internal-ip').v4.sync(); 16 | 17 | 18 | 19 | if (ZwiftPacketMonitor && Cap) { 20 | 21 | console.log('Listening on: ', ip, JSON.stringify(Cap.findDevice(ip),null,4)); 22 | 23 | // determine network interface associated with external IP address 24 | interface = Cap.findDevice(ip); 25 | // ... and setup monitor on that interface: 26 | const monitor = new ZwiftPacketMonitor(interface) 27 | 28 | 29 | 30 | // monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 31 | // console.log(serverWorldTime, dstPort, dstAddr, playerState) 32 | // }) 33 | 34 | // monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 35 | // console.log(serverWorldTime, dstPort, dstAddr, playerState) 36 | // }) 37 | 38 | // monitor.on('incomingPlayerGaveRideOn', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 39 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 40 | // }) 41 | 42 | // monitor.on('incomingPlayerSentMessage', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 43 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 44 | // }) 45 | 46 | // monitor.on('incomingPlayerEnteredWorld', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 47 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 48 | // }) 49 | 50 | // monitor.on('incomingPayload2', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 51 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 52 | // }) 53 | 54 | // monitor.on('incomingPayload3', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 55 | // console.log(serverWorldTime, dstPort, dstAddr, payload) 56 | // }) 57 | 58 | // // The Zwift server sends states in batches. This event is emitted at the end of each incoming batch 59 | // monitor.on('endOfBatch', () => { 60 | // console.log('end of batch') 61 | // }) 62 | 63 | monitor.start() 64 | } 65 | 66 | -------------------------------------------------------------------------------- /test/test-outgoing-player-state.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | // const message = Buffer.from('Some bytes'); 3 | const client = dgram.createSocket('udp4'); 4 | 5 | 6 | const interface = require('internal-ip').v4.sync(); 7 | 8 | console.log('Require: ZwiftPacketMonitor') 9 | const ZwiftPacketMonitor = require('../ZwiftPacketMonitor.js') 10 | console.log('Create monitor') 11 | 12 | 13 | // const monitor = new ZwiftPacketMonitor('127.0.0.1') 14 | // const monitor = new ZwiftPacketMonitor(interface) 15 | // const monitor = new ZwiftPacketMonitor('\\Device\\NPF_{8A803BAC-27E3-4404-9D5B-C2C58315920B}') 16 | const monitor = new ZwiftPacketMonitor('127.0.0.1') 17 | 18 | console.log('Create event listeners') 19 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime) => { 20 | console.log(playerState) 21 | 22 | }) 23 | 24 | 25 | const protobuf = require('protobufjs') 26 | // const zwiftProtoRoot = protobuf.parse(fs.readFileSync(`${__dirname}/../zwiftMessages.proto`), { keepCase: true }).root 27 | 28 | // const buffer = new Buffer(65535) 29 | 30 | 31 | 32 | var LOCALPORT = 30221; 33 | var ZWIFTPORT = 3022; 34 | 35 | var TESTING_OUTGOING_MESSAGES = true; 36 | var TESTING_INCOMING_MESSAGES = false; 37 | 38 | var PORT 39 | 40 | if (TESTING_OUTGOING_MESSAGES) { 41 | PORT = ZWIFTPORT; // test messages are sent TO this port 42 | // the server emulates Zwift 43 | client.bind(LOCALPORT, interface); 44 | // the client emulates the game client 45 | } else { 46 | PORT = LOCALPORT; 47 | // the server emulates the game client 48 | client.bind(ZWIFTPORT); 49 | // the client emulates the Zwift server 50 | } 51 | 52 | 53 | var server = dgram.createSocket('udp4'); 54 | 55 | server.on('listening', function () { 56 | var address = server.address(); 57 | console.log('UDP Server listening on ' + address.address + ":" + address.port); 58 | }); 59 | 60 | server.on('message', function (message, remote) { 61 | console.log(remote.address + ':' + remote.port +' - ' + message); 62 | 63 | }); 64 | 65 | server.bind(PORT, interface); 66 | 67 | 68 | 69 | 70 | protobuf.load(`${__dirname}/../zwiftMessages.proto`, function(err, root) { 71 | if (err) 72 | throw err; 73 | 74 | // Obtain a message type 75 | var ClientToServer = root.lookupType("ClientToServer"); 76 | 77 | // Exemplary payload 78 | var payload = { rider_id: 99999, state: {id: 99999}}; 79 | 80 | // Verify the payload if necessary (i.e. when possibly incomplete or invalid) 81 | var errMsg = ClientToServer.verify(payload); 82 | if (errMsg) 83 | throw Error(errMsg); 84 | 85 | // Create a new message 86 | var message = ClientToServer.create(payload); // or use .fromObject if conversion is necessary 87 | 88 | // Encode a message to an Uint8Array (browser) or Buffer (node) 89 | var buffer = ClientToServer.encode(message).finish(); 90 | // ... do something with buffer 91 | 92 | 93 | client.send(buffer, PORT, interface, (err) => { 94 | // client.close(); 95 | }); 96 | 97 | client.send(buffer, PORT, interface, (err) => { 98 | // client.close(); 99 | }); 100 | 101 | client.send(buffer, PORT, interface, (err) => { 102 | client.close(); 103 | }); 104 | 105 | 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /test/test-live-zwift-debug.js: -------------------------------------------------------------------------------- 1 | try { 2 | console.log('Require: ZwiftPacketMonitorDebug') 3 | var ZwiftPacketMonitor = require('../ZwiftPacketMonitorDebug.js') 4 | console.log('Create monitor') 5 | } catch(e) { 6 | console.log(e) 7 | } 8 | 9 | try { 10 | console.log('Require: cap') 11 | var Cap = require('cap').Cap; 12 | console.log('cap required') 13 | console.log(Cap, Cap.deviceList()) 14 | } catch(e) { 15 | console.log(e) 16 | } 17 | 18 | 19 | /* 20 | var route = require('default-network'); 21 | route.collect(function(error, data) { 22 | var ip 23 | console.log(data); 24 | names = Object.keys(data); 25 | try { 26 | var ifs = os.networkInterfaces()[names[0]] ; 27 | ip = ifs.filter(x => x.family === 'IPv4' && !x.internal)[0].address 28 | } catch (e) { 29 | // 30 | } 31 | start(ip) 32 | }); 33 | */ 34 | 35 | const ip = require('internal-ip').v4.sync(); 36 | start(ip); 37 | 38 | // console.log('require internal-ip') 39 | // const ip = require('internal-ip').v4.sync(); 40 | // console.log('internal-ip required') 41 | 42 | 43 | function start(ip) { 44 | 45 | if (ZwiftPacketMonitor && Cap && ip) { 46 | 47 | console.log('Listening on: ', ip); //, JSON.stringify(Cap.findDevice(ip),null,4)); 48 | 49 | // determine network interface associated with external IP address 50 | // interface = Cap.findDevice(ip); 51 | // ... and setup monitor on that interface: 52 | // const monitor = new ZwiftPacketMonitor(interface) 53 | 54 | const monitor = new ZwiftPacketMonitor(ip) 55 | 56 | 57 | 58 | monitor.on('outgoingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 59 | console.log('outgoingPlayerState'); 60 | console.log(serverWorldTime, dstPort, dstAddr, playerState) 61 | }) 62 | 63 | monitor.on('incomingPlayerState', (playerState, serverWorldTime, dstPort, dstAddr) => { 64 | // console.log('incomingPlayerState'); 65 | // console.log(serverWorldTime, dstPort, dstAddr, playerState) 66 | }) 67 | 68 | monitor.on('incomingPlayerGaveRideOn', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 69 | console.log('incomingPlayerGaveRideOn'); 70 | console.log(serverWorldTime, dstPort, dstAddr, payload) 71 | }) 72 | 73 | monitor.on('incomingPlayerSentMessage', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 74 | console.log('incomingPlayerSentMessage'); 75 | console.log(serverWorldTime, dstPort, dstAddr, payload) 76 | }) 77 | 78 | monitor.on('incomingPlayerEnteredWorld', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 79 | //console.log('incomingPlayerEnteredWorld'); 80 | //console.log(serverWorldTime, dstPort, dstAddr, payload) 81 | }) 82 | 83 | monitor.on('incomingPayload2', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 84 | //console.log('incomingPayload2'); 85 | //console.log(serverWorldTime, dstPort, dstAddr, payload) 86 | }) 87 | 88 | monitor.on('incomingPayload3', (playerUpdate, payload, serverWorldTime, dstPort, dstAddr) => { 89 | //console.log('incomingPayload3'); 90 | //console.log(serverWorldTime, dstPort, dstAddr, payload) 91 | }) 92 | 93 | // The Zwift server sends states in batches. This event is emitted at the end of each incoming batch 94 | monitor.on('endOfBatch', () => { 95 | console.log('end of batch') 96 | }) 97 | 98 | monitor.start() 99 | } 100 | 101 | 102 | } -------------------------------------------------------------------------------- /zwiftMessages.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | 3 | 4 | message PlayerState { 5 | int32 id = 1; 6 | int64 worldTime = 2; 7 | int32 distance = 3; 8 | int32 roadTime = 4; 9 | int32 laps = 5; 10 | int32 speed = 6; 11 | int32 roadPosition = 8; 12 | int32 cadenceUHz = 9; 13 | int32 f10 = 10; 14 | int32 heartrate = 11; 15 | int32 power = 12; 16 | int64 heading = 13; 17 | int32 lean = 14; 18 | int32 climbing = 15; 19 | int32 time = 16; 20 | int32 f19 = 19; 21 | int32 f20 = 20; 22 | int32 progress = 21; 23 | int64 customisationId = 22; 24 | int32 justWatching = 23; 25 | int32 calories = 24; 26 | float x = 25; 27 | float altitude = 26; 28 | float y = 27; 29 | int32 watchingRiderId = 28; 30 | int32 groupId = 29; 31 | int64 sport = 31; 32 | float f34 = 34; // actual distance moved included lateral movement 33 | int32 f35 = 35; 34 | int32 f38 = 38; 35 | } 36 | 37 | message ClientToServer { 38 | int32 connected = 1; 39 | int32 rider_id = 2; 40 | int64 world_time = 3; 41 | PlayerState state = 7; 42 | int32 seqno = 4; 43 | int64 tag8 = 8; 44 | int64 tag9 = 9; 45 | int64 last_update = 10; 46 | int64 tag11 = 11; 47 | int64 last_player_update = 12; 48 | } 49 | 50 | message UnknownMessage1 { 51 | // string firstName=7; 52 | // string lastName=8; 53 | // string timestamp=17; 54 | } 55 | 56 | message UnknownMessage { 57 | // int64 tag1=1; 58 | // UnknownMessage1 tag4=4; 59 | } 60 | 61 | 62 | message PlayerUpdate { 63 | int64 tag1 = 1; 64 | int32 tag2 = 2; 65 | int32 tag3 = 3; 66 | bytes payload = 4; 67 | int64 tag5 = 5; 68 | int64 tag6 = 6; 69 | int64 tag7 = 7; 70 | int64 tag8 = 8; 71 | int64 tag9 = 9; 72 | int64 tag11 = 11; 73 | int64 tag12 = 12; 74 | int64 tag14 = 14; 75 | int64 tag15 = 15; 76 | } 77 | 78 | message Payload105 { // player entered world ? 79 | int64 f1 = 1; 80 | int32 f2 = 2; 81 | int32 f3 = 3; 82 | int64 f4 = 4; // int32? 83 | int64 f5 = 5; 84 | int64 f6 = 6; // int32? 85 | string firstName = 7; 86 | string lastName = 8; 87 | int64 f9 = 9; 88 | int64 f11 = 11; 89 | int32 f12 = 12; 90 | int32 f13 = 13; 91 | int32 f14 = 14; 92 | int32 f15 = 15; 93 | int32 f16 = 16; 94 | string f7date = 17; 95 | int32 f19 = 19; 96 | } 97 | 98 | message Payload5 { // chat message 99 | int32 rider_id = 1; 100 | int32 to_rider_id = 2; // 0 if public message 101 | int32 f3 = 3; // always value 1 ? 102 | string firstName = 4; 103 | string lastName = 5; 104 | string message = 6; 105 | string avatar = 7; 106 | int32 countryCode = 8; 107 | int32 eventSubgroup = 11; 108 | } 109 | 110 | message Payload4 { // ride on 111 | int32 rider_id = 1; 112 | int32 to_rider_id = 2; 113 | string firstName = 3; 114 | string lastName = 4; 115 | int32 countryCode = 5; 116 | } 117 | 118 | message Payload2 { 119 | int32 f1 = 1; 120 | int64 f2 = 2; 121 | } 122 | 123 | message Payload3 { 124 | int32 f1 = 1; 125 | int64 f2 = 2; // worldtime ? 126 | int32 f3 = 3; 127 | } 128 | 129 | message Payload110 { 130 | // format to be determined 131 | } 132 | 133 | message Payload109 { 134 | // format to be determined 135 | } 136 | 137 | 138 | message EventPositions { 139 | int32 position = 1; 140 | message EventRiderPosition { 141 | int32 rider_id = 1; 142 | // ?? float distance_covered = 2; 143 | } 144 | repeated EventRiderPosition eventRiderPosition = 4; 145 | int32 num_riders = 116; 146 | } 147 | 148 | message ServerToClient { 149 | int32 tag1 = 1; 150 | int32 rider_id = 2; 151 | int64 world_time = 3; 152 | int32 seqno = 4; 153 | repeated PlayerState player_states = 8; 154 | repeated PlayerUpdate player_updates = 9; 155 | int64 tag11 = 11; 156 | int64 tag17 = 17; 157 | int32 num_msgs = 18; 158 | int32 msgnum = 19; 159 | EventPositions event_positions = 23; 160 | } 161 | 162 | message WorldAttributes { 163 | int32 world_id = 1; 164 | string name = 2; 165 | int64 tag3 = 3; 166 | int64 tag5 = 4; 167 | int64 world_time = 6; 168 | int64 clock_time = 7; 169 | } 170 | 171 | message WorldAttribute { 172 | int64 world_time = 2; 173 | } 174 | 175 | message EventSubgroupProtobuf { 176 | int32 id = 1; 177 | string name = 2; 178 | int32 rules = 8; 179 | int32 route = 22; 180 | int32 laps = 25; 181 | int32 startLocation = 29; 182 | int32 label = 30; 183 | int32 paceType = 31; 184 | int32 jerseyHash = 36; 185 | } 186 | 187 | message RiderAttributes { 188 | int32 f2 = 2; 189 | int32 f3 = 3; 190 | message AttributeMessage { 191 | int32 myId = 1; 192 | int32 theirId = 2; 193 | string firstName = 3; 194 | string lastName = 4; 195 | int32 countryCode = 5; 196 | } 197 | AttributeMessage attributeMessage = 4; 198 | int32 theirId = 10; 199 | int32 f13 = 13; 200 | } 201 | -------------------------------------------------------------------------------- /ZwiftPacketMonitor.js: -------------------------------------------------------------------------------- 1 | const { time } = require('console'); 2 | const EventEmitter = require('events') 3 | 4 | try { 5 | var Cap = require('cap').Cap; 6 | var decoders=require('cap').decoders, PROTOCOL=decoders.PROTOCOL 7 | } catch (e) { 8 | throw new Error('Probably missing Npcap/libpcap') 9 | } 10 | 11 | const fs = require('fs') 12 | const protobuf = require('protobufjs') 13 | const zwiftProtoRoot = protobuf.parse(fs.readFileSync(`${__dirname}/zwiftMessages.proto`), { keepCase: true }).root 14 | 15 | const buffer = new Buffer.alloc(65535) 16 | const clientToServerPacket = zwiftProtoRoot.lookup('ClientToServer') 17 | const serverToClientPacket = zwiftProtoRoot.lookup('ServerToClient') 18 | 19 | const payload105Packet = zwiftProtoRoot.lookup('Payload105') 20 | const payload5Packet = zwiftProtoRoot.lookup('Payload5') 21 | const payload4Packet = zwiftProtoRoot.lookup('Payload4') 22 | const payload3Packet = zwiftProtoRoot.lookup('Payload3') 23 | const payload2Packet = zwiftProtoRoot.lookup('Payload2') 24 | 25 | class ZwiftPacketMonitor extends EventEmitter { 26 | constructor (interfaceName, options = { }) { 27 | super() 28 | this._options = { 29 | emitDecodeOutgoingError: false, 30 | emitDecodeIncomingError: false, 31 | ...options 32 | } 33 | this._cap = new Cap() 34 | this._linkType = null 35 | this._sequence = 0 36 | // this._tcpSeqNo = 0 37 | this._tcpAssembledLen = 0 38 | this._tcpBuffer = null 39 | if (interfaceName.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { 40 | this._interfaceName = Cap.findDevice(interfaceName) 41 | } else { 42 | this._interfaceName = interfaceName 43 | } 44 | 45 | } 46 | 47 | start () { 48 | try { 49 | this._linkType = this._cap.open(this._interfaceName, 'udp port 3022 or tcp port 3023', 10 * 1024 * 1024, buffer) 50 | this._cap.setMinBytes && this._cap.setMinBytes(0) 51 | this._cap.on('packet', this.processPacket.bind(this)) 52 | } catch (e) { 53 | throw new Error('Error in cap.open - probably insufficient access rights') 54 | } 55 | } 56 | 57 | stop () { 58 | this._cap.close() 59 | } 60 | 61 | static deviceList () { 62 | return Cap.deviceList() 63 | } 64 | 65 | _decodeIncoming(buffer) { 66 | try { 67 | let packet = serverToClientPacket.decode(buffer) 68 | return packet 69 | } catch (err) { 70 | if (this._options.emitDecodeIncomingError) { 71 | this.emit('decodeIncomingError', buffer) 72 | } 73 | } 74 | } 75 | 76 | _decodeOutgoing(buffer) { 77 | try { 78 | let packet = clientToServerPacket.decode(buffer) 79 | return packet 80 | } catch (err) { 81 | if (this._options.emitDecodeOutgoingError) { 82 | this.emit('decodeOutgoingError', buffer) 83 | } 84 | } 85 | } 86 | 87 | _incomingPacketEmit(packet, info) { 88 | if (!packet || !info) return; 89 | 90 | for (let player_state of packet.player_states) { 91 | this.emit('incomingPlayerState', player_state, packet.world_time, info.dstport, info.dstaddr) 92 | } 93 | 94 | for (let player_update of packet.player_updates) { 95 | let payload = {}; 96 | try { 97 | switch (player_update.tag3) { 98 | case 105: // player entered world 99 | payload = payload105Packet.decode(new Uint8Array(player_update.payload)) 100 | this.emit('incomingPlayerEnteredWorld', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 101 | break 102 | case 5: // chat message 103 | payload = payload5Packet.decode(new Uint8Array(player_update.payload)) 104 | this.emit('incomingPlayerSentMessage', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 105 | break 106 | case 4: // ride on 107 | payload = payload4Packet.decode(new Uint8Array(player_update.payload)) 108 | this.emit('incomingPlayerGaveRideOn', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 109 | break 110 | case 2: 111 | // payload = payload2Packet.decode(new Uint8Array(player_update.payload)) 112 | // this.emit('incomingPayload2', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 113 | break 114 | case 3: 115 | // payload = payload3Packet.decode(new Uint8Array(player_update.payload)) 116 | // this.emit('incomingPayload3', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 117 | break 118 | case 109: 119 | // nothing 120 | break 121 | case 110: 122 | // nothing 123 | break 124 | default: 125 | // 126 | } 127 | } catch (ex) { 128 | // most likely an exception during decoding of payload 129 | } 130 | this.emit('incomingPlayerUpdate', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 131 | } 132 | 133 | if (packet.num_msgs === packet.msgnum) { 134 | this.emit('endOfBatch') 135 | } 136 | 137 | } 138 | 139 | processPacket () { 140 | 141 | if (this._linkType === 'ETHERNET') { 142 | let ret = decoders.Ethernet(buffer) 143 | 144 | if (ret.info.type === PROTOCOL.ETHERNET.IPV4) { 145 | ret = decoders.IPV4(buffer, ret.offset) 146 | if (ret.info.protocol === PROTOCOL.IP.UDP) { 147 | ret = decoders.UDP(buffer, ret.offset) 148 | try { 149 | if (ret.info.srcport === 3022) { 150 | // let packet = serverToClientPacket.decode(buffer.slice(ret.offset, ret.offset + ret.info.length)) 151 | let packet = this._decodeIncoming(buffer.slice(ret.offset, ret.offset + ret.info.length)) 152 | /* 153 | if (this._sequence) { 154 | if (packet.seqno > this._sequence + 1) { 155 | console.warn(`Missing packets - expecting ${this._sequence + 1}, got ${packet.seqno}`) 156 | } else if (packet.seqno < this._squence) { 157 | console.warn(`Delayed packet - expecting ${this._sequence + 1}, got ${packet.seqno}`) 158 | return 159 | } 160 | } 161 | this._sequence = packet.seqno 162 | */ 163 | this._incomingPacketEmit(packet, ret.info) 164 | } else if (ret.info.dstport === 3022) { 165 | try { 166 | // 2020-11-14 extra handling added to handle what seems to be extra information preceeding the protobuf 167 | let skip = 5; // uncertain if this number should be fixed or 168 | // ...if the first byte(so far only seen with value 0x06) 169 | // really is the offset where protobuf starts, so add some extra checks just in case: 170 | if (buffer.slice(ret.offset + skip, ret.offset + skip + 1).equals(Buffer.from([0x08]))) { 171 | // protobuf does seem to start after skip bytes 172 | } else if (buffer.slice(ret.offset, ret.offset + 1).equals(Buffer.from([0x08]))) { 173 | // old format apparently, starting directly with protobuf instead of new header 174 | skip = 0 175 | } else { 176 | // use the first byte to determine how many bytes to skip 177 | skip = buffer.slice(ret.offset, ret.offset + 1).readUIntBE(0, 1) - 1 178 | } 179 | let packet = this._decodeOutgoing(buffer.slice(ret.offset + skip, ret.offset + ret.info.length - 4)) 180 | if (packet && packet.state) { 181 | this.emit('outgoingPlayerState', packet.state, packet.world_time, ret.info.srcport, ret.info.srcaddr) 182 | } 183 | } catch (ex) { 184 | } 185 | } 186 | } catch (ex) { 187 | } 188 | } else if (ret.info.protocol === PROTOCOL.IP.TCP) { 189 | var datalen = ret.info.totallen - ret.hdrlen; 190 | ret = decoders.TCP(buffer, ret.offset); 191 | datalen -= ret.hdrlen; 192 | try { 193 | if (ret.info.srcport === 3023 && datalen > 0) { 194 | let packet = null 195 | 196 | let flagPSH = ((ret.info.flags & 0x08) !== 0) 197 | let flagACK = ((ret.info.flags & 0x10) !== 0) 198 | 199 | let flagsPshAck = (ret.info.flags == 0x18) 200 | 201 | let flagsAck = (ret.info.flags == 0x10) 202 | 203 | let tcpPayloadComplete = false 204 | 205 | if (flagsPshAck && !this._tcpBuffer) { 206 | // this TCP packet does not require assembling 207 | this._tcpBuffer = buffer.slice(ret.offset, ret.offset + datalen) 208 | this._tcpAssembledLen = datalen 209 | tcpPayloadComplete = true 210 | } else if (flagsPshAck) { 211 | // This is the last TCP packet in a sequence 212 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 213 | this._tcpAssembledLen += datalen 214 | tcpPayloadComplete = true 215 | } else if (flagsAck && !this._tcpBuffer) { 216 | // This is the first TCP packet in a sequence 217 | this._tcpBuffer = Buffer.concat([buffer.slice(ret.offset, ret.offset + datalen)]) 218 | this._tcpAssembledLen = datalen 219 | } else if (flagsAck) { 220 | // This is an intermediate TCP packet in a sequence 221 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 222 | this._tcpAssembledLen += datalen 223 | } 224 | 225 | if (tcpPayloadComplete) { 226 | // all payloads were assembled, now extract and process all messages in this._tcpBuffer 227 | // The assembled TCP payload contains one or more messages 228 | // [ ]* 229 | 230 | let offset = 0 231 | let l = 0 232 | 233 | while (offset + l < this._tcpAssembledLen) { 234 | let b = this._tcpBuffer.slice(offset, offset + 2) 235 | if (b) { 236 | l = b.readUInt16BE() // total length of the message is stored in first two bytes 237 | } 238 | 239 | try { 240 | packet = this._decodeIncoming(this._tcpBuffer.slice(offset + 2, offset + 2 + l)) 241 | } catch (ex) { 242 | } 243 | 244 | if (packet) { 245 | this._incomingPacketEmit(packet, ret.info) 246 | } 247 | 248 | offset = offset + 2 + l 249 | l = 0 250 | } // end while 251 | // all packets in assembled _tcpBuffer are processed now 252 | 253 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble 254 | this._tcpBuffer = null 255 | this._tcpAssembledLen = 0 256 | } 257 | 258 | 259 | } 260 | } catch (ex) { 261 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble in case of an exception 262 | this._tcpAssembledLen = 0 263 | this._tcpBuffer = null 264 | } 265 | 266 | } 267 | } 268 | } 269 | } 270 | } 271 | 272 | 273 | module.exports = ZwiftPacketMonitor 274 | 275 | -------------------------------------------------------------------------------- /ZwiftPacketMonitorDebug.js: -------------------------------------------------------------------------------- 1 | const { time } = require('console'); 2 | const EventEmitter = require('events') 3 | 4 | try { 5 | var Cap = require('cap').Cap; 6 | var decoders=require('cap').decoders, PROTOCOL=decoders.PROTOCOL 7 | } catch (e) { 8 | throw new Error('Probably missing Npcap/libpcap') 9 | } 10 | 11 | const fs = require('fs') 12 | const protobuf = require('protobufjs') 13 | const zwiftProtoRoot = protobuf.parse(fs.readFileSync(`${__dirname}/zwiftMessages.proto`), { keepCase: true }).root 14 | 15 | const buffer = new Buffer.alloc(65535) 16 | const clientToServerPacket = zwiftProtoRoot.lookup('ClientToServer') 17 | const serverToClientPacket = zwiftProtoRoot.lookup('ServerToClient') 18 | 19 | const payload105Packet = zwiftProtoRoot.lookup('Payload105') 20 | const payload5Packet = zwiftProtoRoot.lookup('Payload5') 21 | const payload4Packet = zwiftProtoRoot.lookup('Payload4') 22 | const payload3Packet = zwiftProtoRoot.lookup('Payload3') 23 | const payload2Packet = zwiftProtoRoot.lookup('Payload2') 24 | 25 | class ZwiftPacketMonitor extends EventEmitter { 26 | constructor (interfaceName, options = { }) { 27 | console.log('ZwiftPacketMonitor: constructor()', interfaceName, options) 28 | super() 29 | this._options = { 30 | emitDecodeOutgoingError: false, 31 | emitDecodeIncomingError: false, 32 | ...options 33 | } 34 | this._cap = new Cap() 35 | this._linkType = null 36 | this._sequence = 0 37 | // this._tcpSeqNo = 0 38 | this._tcpAssembledLen = 0 39 | this._tcpBuffer = null 40 | if (interfaceName.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { 41 | this._interfaceName = Cap.findDevice(interfaceName) 42 | } else { 43 | this._interfaceName = interfaceName 44 | } 45 | 46 | } 47 | 48 | start () { 49 | try { 50 | this._linkType = this._cap.open(this._interfaceName, 'udp port 3022 or tcp port 3023', 10 * 1024 * 1024, buffer) 51 | this._cap.setMinBytes && this._cap.setMinBytes(0) 52 | this._cap.on('packet', this.processPacket.bind(this)) 53 | } catch (e) { 54 | throw new Error('Error in cap.open - probably insufficient access rights') 55 | } 56 | } 57 | 58 | stop () { 59 | this._cap.close() 60 | } 61 | 62 | static deviceList () { 63 | return Cap.deviceList() 64 | } 65 | 66 | _decodeIncoming(buffer) { 67 | try { 68 | let packet = serverToClientPacket.decode(buffer) 69 | return packet 70 | } catch (err) { 71 | if (this._options.emitDecodeIncomingError) { 72 | this.emit('decodeIncomingError', buffer) 73 | } 74 | } 75 | } 76 | 77 | _decodeOutgoing(buffer) { 78 | try { 79 | let packet = clientToServerPacket.decode(buffer) 80 | return packet 81 | } catch (err) { 82 | if (this._options.emitDecodeOutgoingError) { 83 | this.emit('decodeOutgoingError', buffer) 84 | } 85 | } 86 | } 87 | 88 | _incomingPacketEmit(packet, info) { 89 | if (!packet || !info) return; 90 | 91 | for (let player_state of packet.player_states) { 92 | this.emit('incomingPlayerState', player_state, packet.world_time, info.dstport, info.dstaddr) 93 | } 94 | 95 | for (let player_update of packet.player_updates) { 96 | console.log('incomingPlayerUpdate', player_update, packet.world_time) 97 | let payload = {}; 98 | try { 99 | switch (player_update.tag3) { 100 | case 105: // player entered world 101 | payload = payload105Packet.decode(new Uint8Array(player_update.payload)) 102 | this.emit('incomingPlayerEnteredWorld', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 103 | break 104 | case 5: // chat message 105 | payload = payload5Packet.decode(new Uint8Array(player_update.payload)) 106 | this.emit('incomingPlayerSentMessage', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 107 | break 108 | case 4: // ride on 109 | payload = payload4Packet.decode(new Uint8Array(player_update.payload)) 110 | this.emit('incomingPlayerGaveRideOn', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 111 | break 112 | case 2: 113 | // payload = payload2Packet.decode(new Uint8Array(player_update.payload)) 114 | // this.emit('incomingPayload2', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 115 | break 116 | case 3: 117 | // payload = payload3Packet.decode(new Uint8Array(player_update.payload)) 118 | // this.emit('incomingPayload3', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 119 | break 120 | case 109: 121 | // nothing 122 | break 123 | case 110: 124 | // nothing 125 | break 126 | default: 127 | // 128 | // console.log(`unknown type ${player_update.tag3}`) 129 | // console.log(player_update) 130 | // a bit of code to pick up data for analysis of unknown payload types: 131 | // fs.writeFileSync(`/temp/playerupdate_${player_update.tag1}_${player_update.tag3}.raw`, new Uint8Array(player_update.payload)) 132 | } 133 | } catch (ex) { 134 | // most likely an exception during decoding of payload 135 | // fs.writeFileSync(`c:/temp/proto-payload-error.raw`, new Uint8Array(player_update.payload)) 136 | console.log(ex) 137 | } 138 | this.emit('incomingPlayerUpdate', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 139 | } 140 | 141 | if (packet.num_msgs === packet.msgnum) { 142 | this.emit('endOfBatch') 143 | } 144 | 145 | } 146 | 147 | processPacket () { 148 | // console.log('ZwiftPacketMonitor: processPacket()') 149 | 150 | if (this._linkType === 'ETHERNET') { 151 | let ret = decoders.Ethernet(buffer) 152 | 153 | if (ret.info.type === PROTOCOL.ETHERNET.IPV4) { 154 | ret = decoders.IPV4(buffer, ret.offset) 155 | if (ret.info.protocol === PROTOCOL.IP.UDP) { 156 | console.log('Decoding UDP ...'); 157 | ret = decoders.UDP(buffer, ret.offset) 158 | try { 159 | if (ret.info.srcport === 3022) { 160 | // let packet = serverToClientPacket.decode(buffer.slice(ret.offset, ret.offset + ret.info.length)) 161 | let packet = this._decodeIncoming(buffer.slice(ret.offset, ret.offset + ret.info.length)) 162 | /* 163 | if (this._sequence) { 164 | if (packet.seqno > this._sequence + 1) { 165 | console.warn(`Missing packets - expecting ${this._sequence + 1}, got ${packet.seqno}`) 166 | } else if (packet.seqno < this._squence) { 167 | console.warn(`Delayed packet - expecting ${this._sequence + 1}, got ${packet.seqno}`) 168 | return 169 | } 170 | } 171 | this._sequence = packet.seqno 172 | */ 173 | this._incomingPacketEmit(packet, ret.info) 174 | } else if (ret.info.dstport === 3022) { 175 | console.log('Decoding outgoing UDP package ...'); 176 | try { 177 | // 2020-11-14 extra handling added to handle what seems to be extra information preceeding the protobuf 178 | let skip = 5; // uncertain if this number should be fixed or 179 | // ...if the first byte(so far only seen with value 0x06) 180 | // really is the offset where protobuf starts, so add some extra checks just in case: 181 | if (buffer.slice(ret.offset + skip, ret.offset + skip + 1).equals(Buffer.from([0x08]))) { 182 | // protobuf does seem to start after skip bytes 183 | } else if (buffer.slice(ret.offset, ret.offset + 1).equals(Buffer.from([0x08]))) { 184 | // old format apparently, starting directly with protobuf instead of new header 185 | skip = 0 186 | } else { 187 | // use the first byte to determine how many bytes to skip 188 | skip = buffer.slice(ret.offset, ret.offset + 1).readUIntBE(0, 1) - 1 189 | } 190 | let packet = this._decodeOutgoing(buffer.slice(ret.offset + skip, ret.offset + ret.info.length - 4)) 191 | if (packet && packet.state) { 192 | this.emit('outgoingPlayerState', packet.state, packet.world_time, ret.info.srcport, ret.info.srcaddr) 193 | } 194 | } catch (ex) { 195 | // console.log(ret.offset, ret.info.length, ex) 196 | } 197 | } 198 | } catch (ex) { 199 | console.log(ex) 200 | } 201 | } else if (ret.info.protocol === PROTOCOL.IP.TCP) { 202 | var datalen = ret.info.totallen - ret.hdrlen; 203 | console.log('Decoding TCP ...'); 204 | ret = decoders.TCP(buffer, ret.offset); 205 | datalen -= ret.hdrlen; 206 | try { 207 | if (ret.info.srcport === 3023 && datalen > 0) { 208 | let packet = null 209 | 210 | let flagPSH = ((ret.info.flags & 0x08) !== 0) 211 | let flagACK = ((ret.info.flags & 0x10) !== 0) 212 | 213 | let flagsPshAck = (ret.info.flags == 0x18) 214 | 215 | let flagsAck = (ret.info.flags == 0x10) 216 | 217 | let tcpPayloadComplete = false 218 | 219 | if (flagsPshAck && !this._tcpBuffer) { 220 | // this TCP packet does not require assembling 221 | this._tcpBuffer = buffer.slice(ret.offset, ret.offset + datalen) 222 | this._tcpAssembledLen = datalen 223 | tcpPayloadComplete = true 224 | } else if (flagsPshAck) { 225 | // This is the last TCP packet in a sequence 226 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 227 | this._tcpAssembledLen += datalen 228 | tcpPayloadComplete = true 229 | } else if (flagsAck && !this._tcpBuffer) { 230 | // This is the first TCP packet in a sequence 231 | this._tcpBuffer = Buffer.concat([buffer.slice(ret.offset, ret.offset + datalen)]) 232 | this._tcpAssembledLen = datalen 233 | } else if (flagsAck) { 234 | // This is an intermediate TCP packet in a sequence 235 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 236 | this._tcpAssembledLen += datalen 237 | } 238 | 239 | if (tcpPayloadComplete) { 240 | // all payloads were assembled, now extract and process all messages in this._tcpBuffer 241 | // The assembled TCP payload contains one or more messages 242 | // [ ]* 243 | 244 | let offset = 0 245 | let l = 0 246 | 247 | while (offset + l < this._tcpAssembledLen) { 248 | let b = this._tcpBuffer.slice(offset, offset + 2) 249 | if (b) { 250 | l = b.readUInt16BE() // total length of the message is stored in first two bytes 251 | } 252 | 253 | try { 254 | packet = this._decodeIncoming(this._tcpBuffer.slice(offset + 2, offset + 2 + l)) 255 | } catch (ex) { 256 | } 257 | 258 | if (packet) { 259 | console.log('has packet'); 260 | this._incomingPacketEmit(packet, ret.info) 261 | } 262 | 263 | offset = offset + 2 + l 264 | l = 0 265 | } // end while 266 | // all packets in assembled _tcpBuffer are processed now 267 | 268 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble 269 | this._tcpBuffer = null 270 | this._tcpAssembledLen = 0 271 | } 272 | 273 | // primarily for tracking activity during debug: 274 | console.log(`ACK ${((ret.info.flags & 0x10) !== 0)} PSH ${((ret.info.flags & 0x08) !== 0)} datalen ${datalen}`) 275 | 276 | } 277 | } catch (ex) { 278 | console.log(ex) 279 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble in case of an exception 280 | this._tcpAssembledLen = 0 281 | this._tcpBuffer = null 282 | } 283 | 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | 291 | module.exports = ZwiftPacketMonitor 292 | 293 | -------------------------------------------------------------------------------- /ZwiftPacketMonitorLogger.js: -------------------------------------------------------------------------------- 1 | const { time } = require('console'); 2 | const EventEmitter = require('events') 3 | 4 | try { 5 | var Cap = require('cap').Cap; 6 | var decoders=require('cap').decoders, PROTOCOL=decoders.PROTOCOL 7 | } catch (e) { 8 | throw new Error('Probably missing Npcap/libpcap') 9 | } 10 | 11 | const fs = require('fs') 12 | const protobuf = require('protobufjs') 13 | const zwiftProtoRoot = protobuf.parse(fs.readFileSync(`${__dirname}/zwiftMessages.proto`), { keepCase: true }).root 14 | 15 | const buffer = new Buffer.alloc(65535) 16 | const clientToServerPacket = zwiftProtoRoot.lookup('ClientToServer') 17 | const serverToClientPacket = zwiftProtoRoot.lookup('ServerToClient') 18 | 19 | const payload105Packet = zwiftProtoRoot.lookup('Payload105') 20 | const payload5Packet = zwiftProtoRoot.lookup('Payload5') 21 | const payload4Packet = zwiftProtoRoot.lookup('Payload4') 22 | const payload3Packet = zwiftProtoRoot.lookup('Payload3') 23 | const payload2Packet = zwiftProtoRoot.lookup('Payload2') 24 | 25 | class ZwiftPacketMonitor extends EventEmitter { 26 | constructor (interfaceName, options = { }) { 27 | super() 28 | this._options = { 29 | emitDecodeOutgoingError: false, 30 | emitDecodeIncomingError: false, 31 | ...options 32 | } 33 | this._cap = new Cap() 34 | this._linkType = null 35 | this._sequence = 0 36 | // this._tcpSeqNo = 0 37 | this._tcpAssembledLen = 0 38 | this._tcpBuffer = null 39 | if (interfaceName.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { 40 | this._interfaceName = Cap.findDevice(interfaceName) 41 | } else { 42 | this._interfaceName = interfaceName 43 | } 44 | 45 | } 46 | 47 | start () { 48 | try { 49 | this._linkType = this._cap.open(this._interfaceName, 'udp port 3022 or tcp port 3023', 10 * 1024 * 1024, buffer) 50 | this._cap.setMinBytes && this._cap.setMinBytes(0) 51 | this._cap.on('packet', this.processPacket.bind(this)) 52 | } catch (e) { 53 | throw new Error('Error in cap.open - probably insufficient access rights') 54 | } 55 | } 56 | 57 | stop () { 58 | this._cap.close() 59 | } 60 | 61 | static deviceList () { 62 | return Cap.deviceList() 63 | } 64 | 65 | _decodeIncoming(buffer) { 66 | try { 67 | let packet = serverToClientPacket.decode(buffer) 68 | return packet 69 | } catch (err) { 70 | if (this._options.emitDecodeIncomingError) { 71 | this.emit('decodeIncomingError', buffer) 72 | } 73 | } 74 | } 75 | 76 | _decodeOutgoing(buffer) { 77 | try { 78 | let packet = clientToServerPacket.decode(buffer) 79 | return packet 80 | } catch (err) { 81 | if (this._options.emitDecodeOutgoingError) { 82 | this.emit('decodeOutgoingError', buffer) 83 | } 84 | } 85 | } 86 | 87 | _incomingPacketEmit(packet, info) { 88 | if (!packet || !info) return; 89 | 90 | for (let player_state of packet.player_states) { 91 | this.emit('incomingPlayerState', player_state, packet.world_time, info.dstport, info.dstaddr) 92 | } 93 | 94 | for (let player_update of packet.player_updates) { 95 | let payload = {}; 96 | try { 97 | switch (player_update.tag3) { 98 | case 105: // player entered world 99 | payload = payload105Packet.decode(new Uint8Array(player_update.payload)) 100 | this.emit('incomingPlayerEnteredWorld', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 101 | break 102 | case 5: // chat message 103 | payload = payload5Packet.decode(new Uint8Array(player_update.payload)) 104 | this.emit('incomingPlayerSentMessage', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 105 | break 106 | case 4: // ride on 107 | payload = payload4Packet.decode(new Uint8Array(player_update.payload)) 108 | this.emit('incomingPlayerGaveRideOn', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 109 | break 110 | case 2: 111 | // payload = payload2Packet.decode(new Uint8Array(player_update.payload)) 112 | // this.emit('incomingPayload2', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 113 | break 114 | case 3: 115 | // payload = payload3Packet.decode(new Uint8Array(player_update.payload)) 116 | // this.emit('incomingPayload3', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 117 | break 118 | case 109: 119 | // nothing 120 | break 121 | case 110: 122 | // nothing 123 | break 124 | default: 125 | // 126 | } 127 | } catch (ex) { 128 | // most likely an exception during decoding of payload 129 | } 130 | this.emit('incomingPlayerUpdate', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 131 | } 132 | 133 | if (packet.num_msgs === packet.msgnum) { 134 | this.emit('endOfBatch') 135 | } 136 | 137 | } 138 | 139 | processPacket () { 140 | 141 | if (this._linkType === 'ETHERNET') { 142 | let ret = decoders.Ethernet(buffer) 143 | 144 | if (ret.info.type === PROTOCOL.ETHERNET.IPV4) { 145 | ret = decoders.IPV4(buffer, ret.offset) 146 | if (ret.info.protocol === PROTOCOL.IP.UDP) { 147 | ret = decoders.UDP(buffer, ret.offset) 148 | try { 149 | if (ret.info.srcport === 3022) { 150 | // let packet = serverToClientPacket.decode(buffer.slice(ret.offset, ret.offset + ret.info.length)) 151 | let packet = this._decodeIncoming(buffer.slice(ret.offset, ret.offset + ret.info.length)) 152 | /* 153 | if (this._sequence) { 154 | if (packet.seqno > this._sequence + 1) { 155 | console.warn(`Missing packets - expecting ${this._sequence + 1}, got ${packet.seqno}`) 156 | } else if (packet.seqno < this._squence) { 157 | console.warn(`Delayed packet - expecting ${this._sequence + 1}, got ${packet.seqno}`) 158 | return 159 | } 160 | } 161 | this._sequence = packet.seqno 162 | */ 163 | this._incomingPacketEmit(packet, ret.info) 164 | } else if (ret.info.dstport === 3022) { 165 | try { 166 | // 2020-11-14 extra handling added to handle what seems to be extra information preceeding the protobuf 167 | let skip = 5; // uncertain if this number should be fixed or 168 | // ...if the first byte(so far only seen with value 0x06) 169 | // really is the offset where protobuf starts, so add some extra checks just in case: 170 | if (buffer.slice(ret.offset + skip, ret.offset + skip + 1).equals(Buffer.from([0x08]))) { 171 | // protobuf does seem to start after skip bytes 172 | } else if (buffer.slice(ret.offset, ret.offset + 1).equals(Buffer.from([0x08]))) { 173 | // old format apparently, starting directly with protobuf instead of new header 174 | skip = 0 175 | } else { 176 | // use the first byte to determine how many bytes to skip 177 | skip = buffer.slice(ret.offset, ret.offset + 1).readUIntBE(0, 1) - 1 178 | } 179 | let packet = this._decodeOutgoing(buffer.slice(ret.offset + skip, ret.offset + ret.info.length - 4)) 180 | if (packet && packet.state) { 181 | this.emit('outgoingPlayerState', packet.state, packet.world_time, ret.info.srcport, ret.info.srcaddr) 182 | } 183 | } catch (ex) { 184 | } 185 | } 186 | } catch (ex) { 187 | } 188 | } else if (ret.info.protocol === PROTOCOL.IP.TCP) { 189 | var datalen = ret.info.totallen - ret.hdrlen; 190 | ret = decoders.TCP(buffer, ret.offset); 191 | datalen -= ret.hdrlen; 192 | try { 193 | if (ret.info.srcport === 3023 && datalen > 0) { 194 | let packet = null 195 | 196 | let flagPSH = ((ret.info.flags & 0x08) !== 0) 197 | let flagACK = ((ret.info.flags & 0x10) !== 0) 198 | 199 | let flagsPshAck = (ret.info.flags == 0x18) 200 | 201 | let flagsAck = (ret.info.flags == 0x10) 202 | 203 | let tcpPayloadComplete = false 204 | 205 | if (flagsPshAck && !this._tcpBuffer) { 206 | // this TCP packet does not require assembling 207 | this._tcpBuffer = buffer.slice(ret.offset, ret.offset + datalen) 208 | this._tcpAssembledLen = datalen 209 | tcpPayloadComplete = true 210 | } else if (flagsPshAck) { 211 | // This is the last TCP packet in a sequence 212 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 213 | this._tcpAssembledLen += datalen 214 | tcpPayloadComplete = true 215 | } else if (flagsAck && !this._tcpBuffer) { 216 | // This is the first TCP packet in a sequence 217 | this._tcpBuffer = Buffer.concat([buffer.slice(ret.offset, ret.offset + datalen)]) 218 | this._tcpAssembledLen = datalen 219 | } else if (flagsAck) { 220 | // This is an intermediate TCP packet in a sequence 221 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 222 | this._tcpAssembledLen += datalen 223 | } 224 | 225 | if (tcpPayloadComplete) { 226 | // all payloads were assembled, now extract and process all messages in this._tcpBuffer 227 | // The assembled TCP payload contains one or more messages 228 | // [ ]* 229 | 230 | let offset = 0 231 | let l = 0 232 | 233 | while (offset + l < this._tcpAssembledLen) { 234 | let b = this._tcpBuffer.slice(offset, offset + 2) 235 | if (b) { 236 | l = b.readUInt16BE() // total length of the message is stored in first two bytes 237 | } 238 | 239 | try { 240 | packet = this._decodeIncoming(this._tcpBuffer.slice(offset + 2, offset + 2 + l)) 241 | } catch (ex) { 242 | } 243 | 244 | if (packet) { 245 | this._incomingPacketEmit(packet, ret.info) 246 | } 247 | 248 | offset = offset + 2 + l 249 | l = 0 250 | } // end while 251 | // all packets in assembled _tcpBuffer are processed now 252 | 253 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble 254 | this._tcpBuffer = null 255 | this._tcpAssembledLen = 0 256 | } 257 | 258 | 259 | } 260 | } catch (ex) { 261 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble in case of an exception 262 | this._tcpAssembledLen = 0 263 | this._tcpBuffer = null 264 | } 265 | 266 | } 267 | } 268 | } 269 | } 270 | } 271 | 272 | class ZwiftPacketMonitorLogger extends ZwiftPacketMonitor { 273 | constructor(interfaceName, options = {}) { 274 | super(interfaceName) 275 | 276 | this._options = { 277 | dir: '/temp', 278 | incoming: true, 279 | outgoing: true, 280 | payload: true, 281 | ...options 282 | } 283 | this._packetSeqNo = 0 284 | this._payloadSeqNo = 0 285 | 286 | if (this._options.dir) { 287 | try { 288 | fs.mkdirSync(this._options.dir) 289 | } catch (e) { 290 | if (e.code != 'EEXIST') throw e 291 | } 292 | } 293 | } 294 | 295 | _decodeOutgoing(buffer) { 296 | if (buffer && this._options.outgoing) { 297 | fs.writeFile(`${this._options.dir}/packet_${(this._packetSeqNo += 1)}_${Date.now()}_type_outgoing.raw`, new Uint8Array(buffer), (err) => { }) 298 | } 299 | return super._decodeOutgoing(buffer) 300 | } 301 | 302 | _decodeIncoming(buffer) { 303 | if (buffer && this._options.incoming) { 304 | fs.writeFile(`${this._options.dir}/packet_${(this._packetSeqNo += 1)}_${Date.now()}_type_incoming.raw`, new Uint8Array(buffer), (err) => { }) 305 | } 306 | return super._decodeIncoming(buffer) 307 | } 308 | 309 | _incomingPacketEmit(packet, info) { 310 | var result = super._incomingPacketEmit(packet, info) 311 | if (packet && this._options.payload) { 312 | for (let player_update of packet.player_updates) { 313 | fs.writeFile(`${this._options.dir}/playerupdate_payload_${(this._payloadSeqNo += 1)}_${Date.now()}_type_${player_update.tag3}.raw`, new Uint8Array(player_update.payload), (err) => { }) 314 | } 315 | } 316 | return result 317 | } 318 | } 319 | 320 | module.exports = ZwiftPacketMonitorLogger 321 | 322 | -------------------------------------------------------------------------------- /ZwiftPacketMonitorSource.js: -------------------------------------------------------------------------------- 1 | const { time } = require('console'); 2 | const EventEmitter = require('events') 3 | 4 | try { 5 | var Cap = require('cap').Cap; 6 | var decoders=require('cap').decoders, PROTOCOL=decoders.PROTOCOL 7 | } catch (e) { 8 | throw new Error('Probably missing Npcap/libpcap') 9 | } 10 | 11 | const fs = require('fs') 12 | const protobuf = require('protobufjs') 13 | const zwiftProtoRoot = protobuf.parse(fs.readFileSync(`${__dirname}/zwiftMessages.proto`), { keepCase: true }).root 14 | 15 | const buffer = new Buffer.alloc(65535) 16 | const clientToServerPacket = zwiftProtoRoot.lookup('ClientToServer') 17 | const serverToClientPacket = zwiftProtoRoot.lookup('ServerToClient') 18 | 19 | const payload105Packet = zwiftProtoRoot.lookup('Payload105') 20 | const payload5Packet = zwiftProtoRoot.lookup('Payload5') 21 | const payload4Packet = zwiftProtoRoot.lookup('Payload4') 22 | const payload3Packet = zwiftProtoRoot.lookup('Payload3') 23 | const payload2Packet = zwiftProtoRoot.lookup('Payload2') 24 | 25 | class ZwiftPacketMonitor extends EventEmitter { 26 | constructor (interfaceName, options = { }) { 27 | // #ifdef DEBUG 28 | console.log('ZwiftPacketMonitor: constructor()', interfaceName, options) 29 | // #endif 30 | super() 31 | this._options = { 32 | emitDecodeOutgoingError: false, 33 | emitDecodeIncomingError: false, 34 | ...options 35 | } 36 | this._cap = new Cap() 37 | this._linkType = null 38 | this._sequence = 0 39 | // this._tcpSeqNo = 0 40 | this._tcpAssembledLen = 0 41 | this._tcpBuffer = null 42 | if (interfaceName.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { 43 | this._interfaceName = Cap.findDevice(interfaceName) 44 | } else { 45 | this._interfaceName = interfaceName 46 | } 47 | 48 | } 49 | 50 | start () { 51 | try { 52 | this._linkType = this._cap.open(this._interfaceName, 'udp port 3022 or tcp port 3023', 10 * 1024 * 1024, buffer) 53 | this._cap.setMinBytes && this._cap.setMinBytes(0) 54 | this._cap.on('packet', this.processPacket.bind(this)) 55 | } catch (e) { 56 | throw new Error('Error in cap.open - probably insufficient access rights') 57 | } 58 | } 59 | 60 | stop () { 61 | this._cap.close() 62 | } 63 | 64 | static deviceList () { 65 | return Cap.deviceList() 66 | } 67 | 68 | _decodeIncoming(buffer) { 69 | try { 70 | let packet = serverToClientPacket.decode(buffer) 71 | return packet 72 | } catch (err) { 73 | if (this._options.emitDecodeIncomingError) { 74 | this.emit('decodeIncomingError', buffer) 75 | } 76 | } 77 | } 78 | 79 | _decodeOutgoing(buffer) { 80 | try { 81 | let packet = clientToServerPacket.decode(buffer) 82 | return packet 83 | } catch (err) { 84 | if (this._options.emitDecodeOutgoingError) { 85 | this.emit('decodeOutgoingError', buffer) 86 | } 87 | } 88 | } 89 | 90 | _incomingPacketEmit(packet, info) { 91 | if (!packet || !info) return; 92 | 93 | for (let player_state of packet.player_states) { 94 | this.emit('incomingPlayerState', player_state, packet.world_time, info.dstport, info.dstaddr) 95 | } 96 | 97 | for (let player_update of packet.player_updates) { 98 | // #ifdef DEBUG 99 | console.log('incomingPlayerUpdate', player_update, packet.world_time) 100 | // #endif 101 | let payload = {}; 102 | try { 103 | switch (player_update.tag3) { 104 | case 105: // player entered world 105 | payload = payload105Packet.decode(new Uint8Array(player_update.payload)) 106 | this.emit('incomingPlayerEnteredWorld', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 107 | break 108 | case 5: // chat message 109 | payload = payload5Packet.decode(new Uint8Array(player_update.payload)) 110 | this.emit('incomingPlayerSentMessage', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 111 | break 112 | case 4: // ride on 113 | payload = payload4Packet.decode(new Uint8Array(player_update.payload)) 114 | this.emit('incomingPlayerGaveRideOn', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 115 | break 116 | case 2: 117 | // payload = payload2Packet.decode(new Uint8Array(player_update.payload)) 118 | // this.emit('incomingPayload2', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 119 | break 120 | case 3: 121 | // payload = payload3Packet.decode(new Uint8Array(player_update.payload)) 122 | // this.emit('incomingPayload3', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 123 | break 124 | case 109: 125 | // nothing 126 | break 127 | case 110: 128 | // nothing 129 | break 130 | default: 131 | // 132 | // #ifdef DEBUG 133 | // console.log(`unknown type ${player_update.tag3}`) 134 | // console.log(player_update) 135 | // a bit of code to pick up data for analysis of unknown payload types: 136 | // fs.writeFileSync(`/temp/playerupdate_${player_update.tag1}_${player_update.tag3}.raw`, new Uint8Array(player_update.payload)) 137 | // #endif 138 | } 139 | } catch (ex) { 140 | // most likely an exception during decoding of payload 141 | // #ifdef DEBUG 142 | // fs.writeFileSync(`c:/temp/proto-payload-error.raw`, new Uint8Array(player_update.payload)) 143 | console.log(ex) 144 | // #endif 145 | } 146 | this.emit('incomingPlayerUpdate', player_update, payload, packet.world_time, info.dstport, info.dstaddr) 147 | } 148 | 149 | if (packet.num_msgs === packet.msgnum) { 150 | this.emit('endOfBatch') 151 | } 152 | 153 | } 154 | 155 | processPacket () { 156 | // #ifdef DEBUG 157 | // console.log('ZwiftPacketMonitor: processPacket()') 158 | // #endif 159 | 160 | if (this._linkType === 'ETHERNET') { 161 | let ret = decoders.Ethernet(buffer) 162 | 163 | if (ret.info.type === PROTOCOL.ETHERNET.IPV4) { 164 | ret = decoders.IPV4(buffer, ret.offset) 165 | if (ret.info.protocol === PROTOCOL.IP.UDP) { 166 | // #ifdef DEBUG 167 | console.log('Decoding UDP ...'); 168 | // #endif 169 | ret = decoders.UDP(buffer, ret.offset) 170 | try { 171 | if (ret.info.srcport === 3022) { 172 | // let packet = serverToClientPacket.decode(buffer.slice(ret.offset, ret.offset + ret.info.length)) 173 | let packet = this._decodeIncoming(buffer.slice(ret.offset, ret.offset + ret.info.length)) 174 | /* 175 | if (this._sequence) { 176 | if (packet.seqno > this._sequence + 1) { 177 | console.warn(`Missing packets - expecting ${this._sequence + 1}, got ${packet.seqno}`) 178 | } else if (packet.seqno < this._squence) { 179 | console.warn(`Delayed packet - expecting ${this._sequence + 1}, got ${packet.seqno}`) 180 | return 181 | } 182 | } 183 | this._sequence = packet.seqno 184 | */ 185 | this._incomingPacketEmit(packet, ret.info) 186 | } else if (ret.info.dstport === 3022) { 187 | // #ifdef DEBUG 188 | console.log('Decoding outgoing UDP package ...'); 189 | // #endif 190 | try { 191 | // 2020-11-14 extra handling added to handle what seems to be extra information preceeding the protobuf 192 | let skip = 5; // uncertain if this number should be fixed or 193 | // ...if the first byte(so far only seen with value 0x06) 194 | // really is the offset where protobuf starts, so add some extra checks just in case: 195 | if (buffer.slice(ret.offset + skip, ret.offset + skip + 1).equals(Buffer.from([0x08]))) { 196 | // protobuf does seem to start after skip bytes 197 | } else if (buffer.slice(ret.offset, ret.offset + 1).equals(Buffer.from([0x08]))) { 198 | // old format apparently, starting directly with protobuf instead of new header 199 | skip = 0 200 | } else { 201 | // use the first byte to determine how many bytes to skip 202 | skip = buffer.slice(ret.offset, ret.offset + 1).readUIntBE(0, 1) - 1 203 | } 204 | let packet = this._decodeOutgoing(buffer.slice(ret.offset + skip, ret.offset + ret.info.length - 4)) 205 | if (packet && packet.state) { 206 | this.emit('outgoingPlayerState', packet.state, packet.world_time, ret.info.srcport, ret.info.srcaddr) 207 | } 208 | } catch (ex) { 209 | // #ifdef DEBUG 210 | // console.log(ret.offset, ret.info.length, ex) 211 | // #endif 212 | } 213 | } 214 | } catch (ex) { 215 | // #ifdef DEBUG 216 | console.log(ex) 217 | // #endif 218 | } 219 | } else if (ret.info.protocol === PROTOCOL.IP.TCP) { 220 | var datalen = ret.info.totallen - ret.hdrlen; 221 | // #ifdef DEBUG 222 | console.log('Decoding TCP ...'); 223 | // #endif 224 | ret = decoders.TCP(buffer, ret.offset); 225 | datalen -= ret.hdrlen; 226 | try { 227 | if (ret.info.srcport === 3023 && datalen > 0) { 228 | let packet = null 229 | 230 | let flagPSH = ((ret.info.flags & 0x08) !== 0) 231 | let flagACK = ((ret.info.flags & 0x10) !== 0) 232 | 233 | let flagsPshAck = (ret.info.flags == 0x18) 234 | 235 | let flagsAck = (ret.info.flags == 0x10) 236 | 237 | let tcpPayloadComplete = false 238 | 239 | if (flagsPshAck && !this._tcpBuffer) { 240 | // this TCP packet does not require assembling 241 | this._tcpBuffer = buffer.slice(ret.offset, ret.offset + datalen) 242 | this._tcpAssembledLen = datalen 243 | tcpPayloadComplete = true 244 | } else if (flagsPshAck) { 245 | // This is the last TCP packet in a sequence 246 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 247 | this._tcpAssembledLen += datalen 248 | tcpPayloadComplete = true 249 | } else if (flagsAck && !this._tcpBuffer) { 250 | // This is the first TCP packet in a sequence 251 | this._tcpBuffer = Buffer.concat([buffer.slice(ret.offset, ret.offset + datalen)]) 252 | this._tcpAssembledLen = datalen 253 | } else if (flagsAck) { 254 | // This is an intermediate TCP packet in a sequence 255 | this._tcpBuffer = Buffer.concat([this._tcpBuffer, buffer.slice(ret.offset, ret.offset + datalen)]) 256 | this._tcpAssembledLen += datalen 257 | } 258 | 259 | if (tcpPayloadComplete) { 260 | // all payloads were assembled, now extract and process all messages in this._tcpBuffer 261 | // The assembled TCP payload contains one or more messages 262 | // [ ]* 263 | 264 | let offset = 0 265 | let l = 0 266 | 267 | while (offset + l < this._tcpAssembledLen) { 268 | let b = this._tcpBuffer.slice(offset, offset + 2) 269 | if (b) { 270 | l = b.readUInt16BE() // total length of the message is stored in first two bytes 271 | } 272 | 273 | try { 274 | packet = this._decodeIncoming(this._tcpBuffer.slice(offset + 2, offset + 2 + l)) 275 | } catch (ex) { 276 | // #ifdef DEBUG 277 | // #endif 278 | } 279 | 280 | if (packet) { 281 | // #ifdef DEBUG 282 | console.log('has packet'); 283 | // #endif 284 | this._incomingPacketEmit(packet, ret.info) 285 | } 286 | 287 | offset = offset + 2 + l 288 | l = 0 289 | } // end while 290 | // all packets in assembled _tcpBuffer are processed now 291 | 292 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble 293 | this._tcpBuffer = null 294 | this._tcpAssembledLen = 0 295 | } 296 | 297 | // #ifdef DEBUG 298 | // primarily for tracking activity during debug: 299 | console.log(`ACK ${((ret.info.flags & 0x10) !== 0)} PSH ${((ret.info.flags & 0x08) !== 0)} datalen ${datalen}`) 300 | // #endif 301 | 302 | } 303 | } catch (ex) { 304 | // #ifdef DEBUG 305 | console.log(ex) 306 | // #endif 307 | // reset _tcpAssembledLen and _tcpBuffer for next sequence to assemble in case of an exception 308 | this._tcpAssembledLen = 0 309 | this._tcpBuffer = null 310 | } 311 | 312 | } 313 | } 314 | } 315 | } 316 | } 317 | 318 | // #ifdef LOGGER 319 | class ZwiftPacketMonitorLogger extends ZwiftPacketMonitor { 320 | constructor(interfaceName, options = {}) { 321 | super(interfaceName) 322 | 323 | this._options = { 324 | dir: '/temp', 325 | incoming: true, 326 | outgoing: true, 327 | payload: true, 328 | ...options 329 | } 330 | this._packetSeqNo = 0 331 | this._payloadSeqNo = 0 332 | 333 | if (this._options.dir) { 334 | try { 335 | fs.mkdirSync(this._options.dir) 336 | } catch (e) { 337 | if (e.code != 'EEXIST') throw e 338 | } 339 | } 340 | } 341 | 342 | _decodeOutgoing(buffer) { 343 | if (buffer && this._options.outgoing) { 344 | fs.writeFile(`${this._options.dir}/packet_${(this._packetSeqNo += 1)}_${Date.now()}_type_outgoing.raw`, new Uint8Array(buffer), (err) => { }) 345 | } 346 | return super._decodeOutgoing(buffer) 347 | } 348 | 349 | _decodeIncoming(buffer) { 350 | if (buffer && this._options.incoming) { 351 | fs.writeFile(`${this._options.dir}/packet_${(this._packetSeqNo += 1)}_${Date.now()}_type_incoming.raw`, new Uint8Array(buffer), (err) => { }) 352 | } 353 | return super._decodeIncoming(buffer) 354 | } 355 | 356 | _incomingPacketEmit(packet, info) { 357 | var result = super._incomingPacketEmit(packet, info) 358 | if (packet && this._options.payload) { 359 | for (let player_update of packet.player_updates) { 360 | fs.writeFile(`${this._options.dir}/playerupdate_payload_${(this._payloadSeqNo += 1)}_${Date.now()}_type_${player_update.tag3}.raw`, new Uint8Array(player_update.payload), (err) => { }) 361 | } 362 | } 363 | return result 364 | } 365 | } 366 | // #endif 367 | 368 | // #ifdef LOGGER 369 | module.exports = ZwiftPacketMonitorLogger 370 | // #else 371 | module.exports = ZwiftPacketMonitor 372 | // #endif --------------------------------------------------------------------------------