├── .github ├── FUNDING.yml └── stale.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── images ├── IP_address.jpg ├── WPS.png ├── finished.jpg ├── ipconfig.jpg ├── press_WPS.jpg ├── screen1.jpg ├── screen2.jpg ├── tch-exploit-win.jpg └── tftp.png ├── package-lock.json ├── package.json └── src ├── README.md ├── args.coffee ├── ask.coffee ├── dhcp ├── constants.coffee ├── index.coffee ├── lease.coffee ├── options.coffee ├── protocol.coffee ├── seqbuffer.coffee ├── server.coffee └── tools.coffee ├── dns.coffee ├── get-port.coffee ├── http ├── cwmp │ ├── index.coffee │ └── xml.coffee ├── file.coffee ├── index.coffee └── router.coffee ├── index.coffee ├── ips.coffee ├── ntp ├── client.coffee ├── index.coffee ├── packet.coffee ├── server.coffee └── server2.coffee └── tftp.coffee /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: paypal.me/seud0nym -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 28 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - pending release 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tch-exploit-linux.zip 2 | tch-exploit-macos.zip 3 | tch-exploit-win.zip 4 | dist 5 | node_modules 6 | release -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "node-tftp"] 2 | path = node-tftp 3 | url = https://github.com/seud0nym/node-tftp.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nathan Bolam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/github/license/seud0nym/tch-exploit.svg?style=flat)](https://github.com/seud0nym/tch-exploit/blob/master/LICENSE) 2 | ![Languages](https://img.shields.io/github/languages/count/seud0nym/tch-exploit) 3 | ![Top Language](https://img.shields.io/github/languages/top/seud0nym/tch-exploit) 4 | ![Total Downloads](https://img.shields.io/github/downloads/seud0nym/tch-exploit/total) 5 | [![Latest Release](https://img.shields.io/github/release/seud0nym/tch-exploit/all.svg?style=flat&label=latest)](https://github.com/seud0nym/tch-exploit/releases) 6 | ![Latest Release Downloads](https://img.shields.io/github/downloads/seud0nym/tch-exploit/latest/total) 7 | 8 | # Technicolor OpenWRT Shell Unlocker 9 | 10 | ## Instructions 11 | 12 | These instructions have been tested on various Telstra branded devices. They should also work for other branded Technicolor devices. 13 | 14 | 1. [Before You Start](#before-you-start) 15 | 1. [Preparation](#preparation) 16 | * [Modem](#modem) 17 | * [Computer](#computer) 18 | 1. [Type 3 (Non-Rootable) Firmware](#type-3-non-rootable-firmware) 19 | * [Loading Firmware using BOOTP and TFTP](#loading-firmware-using-bootp-and-tftp) 20 | * [TFTP Server](#tftp-server) 21 | * [BOOTP Mode](#bootp-mode) 22 | * [Restore Computer Network Interface](#restore-computer-network-interface) 23 | * [Confirm Type 2 Firmware Successfully Booted](#confirm-type-2-firmware-successfully-booted) 24 | * [If Type 2 Firmware Did NOT Boot](#if-type-2-firmware-did-not-boot) 25 | * [Firmware Banks Explained](#firmware-banks-explained) 26 | 1. [Type 2 (Rootable) Firmware](#type-2-rootable-firmware) 27 | * [Running tch-exploit](#running-tch-exploit) 28 | * [After Root Access Acquired](#after-root-access-acquired) 29 | 1. [Extract tch-gui-unhide Scripts](#1-extract-tch-gui-unhide-scripts) 30 | 1. [Optimal Bank Planning](#2-optimal-bank-planning) 31 | 1. [Optionally Upgrade Firmware](#3-upgrade-firmware-optional) 32 | 1. [Harden Root Access](#4-harden-root-access) 33 | 1. [Unlock GUI Features](#5-unlock-gui-features) 34 | 35 | ## Before You Start 36 | 37 | To acquire root access on a Technicolor device, you need the following: 38 | 39 | * A computer with either an Ethernet port OR a USB Ethernet adapter 40 | * A USB Stick 41 | * An ethernet cable 42 | * An SSH client: 43 | * **Windows**: Download [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) 44 | * **Mac/Linux**: You can use the `ssh` command provided by your distribution 45 | * A basic understanding of running commands in your operating system's terminal program 46 | 47 | ## Preparation 48 | 49 | ### Modem 50 | 51 | * Remove the cable that connects the device to the internet. This will either be the cable into the red WAN port or the DSL/phone port. 52 | * If the Technicolor device is 4G backup capable, remove the SIM card. 53 | * Determine the current firmware version installed on the device: 54 | 1. Log into your modem through your web browser, usually at http://192.168.0.1/ or http://10.0.0.138/ 55 | 1. Click on `Advanced` in the top right corner of the page (skip this step on the Smart Modem Gen 3, as it will automatically take you into the Advanced view). 56 | 1. Click the first box on the top left, usually called “Gateway” or “Modem”. 57 | 1. Record the “Firmware Version” shown on the screen. 58 | 1. Click on the “Reset” button to restore the device to factory defaults. 59 | * Determine your firmware version **Type** from https://hack-technicolor.readthedocs.io/en/latest/Repository/ 60 | 1. Click on your device name in the right side panel menu. 61 | 1. Find your version number in the table and make note of the **Type** (1, 2,or 3) from the first column. 62 | 1. If your device has a **Type 3** firmware, download any **Type 2** firmware for your device. 63 | 64 | ### Computer 65 | 66 | * Download the [latest release](https://github.com/seud0nym/tch-exploit/releases/latest) of `tch-exploit` for your operating system. 67 | * Extract the contents of the zip file into a directory on your computer. 68 | * If you have a Telstra branded device running firmware 17.2 or later, download the [latest release](https://github.com/seud0nym/tch-gui-unhide/releases/latest) of `tch-gui-unhide` for your firmware version (e.g. for a firmware beginning with 20.3.c, download the `20.3.c.tar.gz` file). 69 | * Copy that file to the USB stick. 70 | * For other branded devices or Telstra devices with firmware older than 17.2, download the following scripts and copy them to the USB stick: 71 | * https://raw.githubusercontent.com/seud0nym/tch-gui-unhide/master/utilities/show-bank-plan 72 | * https://raw.githubusercontent.com/seud0nym/tch-gui-unhide/master/utilities/set-optimal-bank-plan 73 | * Disable or turn off _all_ network connections, including Wi-Fi, VPN, 4G USB devices, other ethernet connections, etc. 74 | * Disable or turn off _all_ virus and malware scanners (including Windows Defender). 75 | * Find the name of your ethernet connection: 76 | * **Windows**: Open a command prompt and use the `ipconfig | find "Ethernet adapter"` command to list your ethernet interfaces. 77 | * **Mac**: Open a terminal window and use either the `networksetup -listallhardwareports` or `ifconfig` command. 78 | * **Linux**: Open a terminal window and use either the `ip link` or `ifconfig` command, depending on your distribution. 79 | 80 | ## Type 3 (Non-Rootable) Firmware 81 | 82 | You _cannot_ acquire root access to a Type 3 firmware. 83 | 84 | The only known method for acquiring root access on a device with Type 3 firmware is to downgrade the device to a Type 2 firmware first. 85 | 86 | The first thing to try is loading the Type 2 firmware via TFTP. 87 | 88 | ### Loading Firmware using BOOTP and TFTP 89 | 90 | There are 2 steps to loading firmware via TFTP: 91 | 92 | 1. Setup a TFTP server that will download the firmware to the device; and 93 | 2. Start your device in BOOTP mode so that it will automatically request the firmware from the TFTP server and install it on the device. 94 | 95 | #### TFTP Server 96 | 97 | You should already have acquired `tch-exploit` for your operating system. If you have Telstra Smart Modem Gen 3, you **must** use the `seud0nym` [version](https://github.com/seud0nym/tch-exploit/releases) - the original `BoLaMN` version will fail. The `seud0nym` version also has better prompts and some bug fixes. However, one user has reported "Transfer cancelled" messages from a DJA0230 on MacOS. If affected, use the original `BoLaMN` [version](https://github.com/BoLaMN/tch-exploit/releases/tag/2.0.1-rc7). 98 | 99 | 1. Connect an Ethernet cable into the Ethernet port on your computer, and the other end into any **yellow LAN port** on your device. 100 | * On the Smart Modem Gen 3 _only_, you can use _either_ a yellow LAN port or the red WAN port 101 | 1. Copy the firmware file (ending with .rbi for all devices prior to the Smart Modem Gen 3, or ending with .pkgtb for the Gen 3) to the `tch-exploit` directory. 102 | 1. Configure networking and start the TFTP server: 103 | * **Windows**: Run the following commands in an _elevated_ (Administrator) command prompt: 104 | * _Replace `C:\Users\user\Downloads\release` with the name of the directory containing tch-exploit-win_ 105 | * _Replace `Ethernet` with the name of your ethernet connection_ 106 | * _Replace `` with the name of the Type 2 firmware you downloaded for your device_ 107 | * _Each line is one command. Run them separately._ 108 | ```dos 109 | cd C:\Users\user\Downloads\release 110 | netsh interface ipv4 set address name="Ethernet" static 192.168.0.254 255.255.255.0 192.168.0.1 111 | tch-exploit-win --ip=192.168.0.254 --tftp= 112 | ``` 113 | * **Mac**: Run the following commands: 114 | * _Replace `release` with the name of the directory containing tch-exploit-macos_ 115 | * _Replace `en0` with the name of your ethernet connection_ 116 | * _Replace `` with the name of the Type 2 firmware you downloaded for your device_ 117 | * _Each line is one command. Run them separately._ 118 | ```shell 119 | cd release 120 | sudo ifconfig set en0 INFORM 192.168.0.254 121 | sudo ./tch-exploit-macos --ip=192.168.0.254 --tftp= 122 | ``` 123 | * **IMPORTANT:** Disable the MacOS firewall, or ensure that it will allow DHCP/TFTP requests! 124 | * **Linux**: Run the following commands: 125 | * _Replace `release` with the name of the directory containing tch-exploit-linux_ 126 | * _Replace `eth0` with the name of your ethernet connection_ 127 | * _Replace `` with the name of the Type 2 firmware you downloaded for your device_ 128 | * _Each line is one command. Run them separately._ 129 | ```shell 130 | cd release 131 | sudo ip addr add 192.168.0.254/24 dev eth0 132 | sudo ./tch-exploit-linux --ip=192.168.0.254 --tftp= 133 | ``` 134 | 135 | #### BOOTP Mode 136 | 137 | 1. Unplug the power from the modem. 138 | 1. Hold in the Reset button using a paper-clip, bamboo skewer, etc. 139 | 1. Power on the modem. **DO NOT RELEASE THE RESET BUTTON YET!** 140 | 1. The power/status LED on the front of the modem will start slowly flashing white on and off. 141 | 1. Release the Reset button. 142 | 143 | Your computer screen will show the progress of the firmware being downloaded. 144 | 145 | ![TFTP Example](images/tftp.png) 146 | 147 | One the firmware download is complete, the power/status LED on the front of the modem will start to flash more quickly. This means that it is applying the downloaded firmware. When it has completed, the modem will reboot. 148 | 149 | #### Restore Computer Network Interface 150 | 151 | If you are using the `seud0nym` version of `tch-exploit`, it will automatically exit when it has finished downloading the firmware. If you are using the `BoLaMN` version, you will need to press `Ctrl-C` to exit. 152 | 153 | * **Windows**: Run the following command in an _elevated_ (Administrator) command prompt: 154 | * _Replace `Ethernet` with the name of your ethernet connection_ 155 | ```dos 156 | netsh interface ipv4 set address name="Ethernet" dhcp 157 | ``` 158 | * **Mac**: Run the following commands: 159 | * _Replace `en0` with the name of your ethernet connection_ 160 | * _Each line is one command. Run them separately._ 161 | ```shell 162 | sudo ipconfig set en0 DHCP 163 | sudo ifconfig en0 down 164 | sudo ifconfig en0 up 165 | ``` 166 | * **IMPORTANT:** Re-enable the MacOS firewall! 167 | * **Linux**: Run the following commands: 168 | * _Replace `eth0` with the name of your ethernet connection_ 169 | * _Each line is one command. Run them separately._ 170 | ```shell 171 | sudo ip addr del 192.168.0.254/24 dev eth0 172 | sudo ip link set eth0 down 173 | sudo ip link set eth0 up 174 | ``` 175 | 176 | #### Confirm Type 2 Firmware Successfully Booted 177 | 178 | 1. Log into your modem, usually at http://192.168.0.1/ or http://10.0.0.138/ 179 | 1. Click on `Advanced` in the top right corner of the page (skip this step on the Smart Modem Gen 3, as it will automatically take you into the Advanced view). 180 | 1. Click the first box on the top left, usually called “Gateway” or “Modem”. 181 | 1. Check the “Firmware Version”: 182 | * If the firmware version matches the Type 2 firmware you just loaded via BOOTP/TFTP, you can [proceed](#type2) to acquire root access. 183 | * If the firmware version is still the original Type 3 firmware, life has just become difficult. 184 | 185 | #### If Type 2 Firmware Did NOT Boot 186 | 187 | If the TFTP appeared to complete successfully, then the problem is more than likely that the device is booting from the wrong firmware bank. This is explained in more detail [below](#banks). 188 | 189 | In this case, you need to force the device to switch banks to the firmware you just loaded. This is a very hit-and-miss procedure that involved forcing the device to fail to boot three times, in which case it will automatically switch to the alternate bank, containing the firmware you just loaded. 190 | 191 | The various boot-fail bank switching techniques are explained [here](https://hack-technicolor.readthedocs.io/en/latest/Recovery/#change-booted-bank). 192 | 193 | There is another bank switching technique that is not discussed, and is only applicable to current devices that still receive firmware updates over-the-air from Telstra. If the device receives a new firmware over the internet, it will always load that new firmware in the _passive_ bank, never the _active_ bank. Once it reboots, it will be running from the _other_ bank, and you can redo the BOOTP/TFTP procedure to load the Type 2 firmware. 194 | 195 | 196 | 197 | >[*Firmware Banks Explained*](https://hack-technicolor.readthedocs.io/en/latest/Recovery/#check-booted-bank) 198 | > --- 199 | > The Technicolor modems are **dual-bank** devices. They work in a very similar fashion to a dual-boot computer system. For example, the computer might have a data partition with personal data and two Operating System partitions that share that data. The Technicolor devices have a data partition and two firmware banks. 200 | > 201 | > When you power on your device it starts loading the firmware from the so-called _active_ bank. With no surprise, the other one gets called _passive_ bank. Of course, only one bank at time can be used. 202 | > 203 | > BOOTP flashing via TFTP writes into *bank 1* **only**, and will do so *even if* the active bank is currently bank 2. The problem occurs because BOOTP/TFTP will _not_ set bank 1 as active. (This is not true if you have a Telstra Smart Modem Gen3 - that device _will_ switch banks to bank 1 after loading a new firmware via TFTP.) 204 | 205 | 206 | 207 | ## Type 2 (Rootable) Firmware 208 | 209 | If you are booting a Type 2 firmware (either by default or by loading one via BOOTP/TFTP), then you can acquire root access. 210 | 211 | ### Running tch-exploit 212 | 213 | 1. Connect an Ethernet cable into the Ethernet port on your computer, and the other end into the **RED WAN port** on your device. 214 | 1. Extract the contents of the `tch-exploit` zip file into a directory on your computer. 215 | 1. Configure networking and start `tch-exploit`: 216 | * **Windows**: Run the following commands in an _elevated_ (Administrator) command prompt: 217 | * _Replace `C:\Users\user\Downloads\release` with the name of the directory containing tch-exploit-win_ 218 | * _Replace `Ethernet` with the name of your ethernet connection_ 219 | * _Each line is one command. Run them separately._ 220 | ```dos 221 | cd C:\Users\user\Downloads\release 222 | netsh interface ipv4 set address name="Ethernet" static 58.162.0.1 255.255.255.0 58.162.0.1 223 | tch-exploit-win 224 | ``` 225 | * **Linux**: Run the following commands: 226 | * _Replace `release` with the name of the directory containing tch-exploit-linux_ 227 | * _Replace `eth0` with the name of your ethernet connection_ 228 | * _Each line is one command. Run them separately._ 229 | ```shell 230 | cd release 231 | sudo ip addr add 58.162.0.1/24 dev eth0 232 | sudo ./tch-exploit-linux 233 | ``` 234 | * **Mac**: Run the following commands: 235 | * _Replace `release` with the name of the directory containing tch-exploit-macos_ 236 | * _Replace `eth0` with the name of your ethernet connection_ 237 | * _Each line is one command. Run them separately._ 238 | ```shell 239 | cd release 240 | sudo ip addr add 58.162.0.1/24 dev eth0 241 | sudo ./tch-exploit-macos 242 | ``` 243 | The screen will look similar to this: 244 | ![tch-exploit-win](images/tch-exploit-win.jpg) 245 | 1. At this point you have to wait a bit. It can be quick, but can also take several minutes. Eventually, the screen will start to fill up like so: 246 | ![DHCP Messages](images/screen1.jpg) 247 | 1. Wait another 40-50 seconds, and the screen then fills up more with green text: 248 | ![CWMP Messages](images/screen2.jpg) 249 | * *IMPORTANT!* If you fail to see the green text after 10 minutes, you probably have a **Type 3** firmware. 250 |

251 | 1. After another 5-6 sec or so it will prompt you to press the WPS button: 252 | ![Press WPS Button](images/press_WPS.jpg) 253 | 1. Press and hold the WPS button for around 3 sec before releasing. On the modem it is the **PAIR** button with two arrows (![WPS Button](images/WPS.png)). The button should start to flash and within a couple of seconds the screen says everything is done: 254 | ![Finished](images/finished.jpg) 255 | 1. Restore the computer network interface: 256 | * **Windows**: Run the following command in an _elevated_ (Administrator) command prompt: 257 | * _Replace `Ethernet` with the name of your ethernet connection_ 258 | ```dos 259 | netsh interface ipv4 set address name="Ethernet" dhcp 260 | ``` 261 | * **Linux/Mac**: Run the following commands: 262 | * _Replace `eth0` with the name of your ethernet connection_ 263 | * _Each line is one command. Run them separately._ 264 | ```shell 265 | sudo ip addr del 58.162.0.1/24 dev eth0 266 | sudo ip link set eth0 down 267 | sudo ip link set eth0 up 268 | ``` 269 | 270 | You can now log in to your device using your SSH client, using a username of `root` and password `root`. 271 | 272 | ### After Root Access Acquired 273 | 274 | Do these steps, in order, _before_ reconnecting your device to the internet after step 4. 275 | 276 | #### 1. Extract tch-gui-unhide Scripts 277 | 1. Insert the USB stick into your device and ensure it is the current directory by executing:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 278 | 1. Extract the scripts by executing (replace `firmware_version` with your firmware version - e.g. 20.3.c):
`tar -zxvf firmware_version.tar.gz` 279 | 280 | #### 2. Optimal Bank Planning 281 | 282 | Optimal bank planning configures the device bank layout to give you the greatest chance of recovery in case you lose root access. This involves leaving bank 1 empty, but marked as active. The passive bank (bank 2) contains the bootable firmware. When the device boots, it fails to find a valid firmware in the active bank, and fails over to the passive bank. If you encounter a situation where you lose root access, but the device has the optimal bank plan, then you can always TFTP in a valid Type 2 firmware and the device will always boot into that firmware (because TFTP firmware downloads are always written to bank 1, and bank 1 is marked as active.) 283 | 284 | *NOTE: The Telstra Smart Modem Gen 3 uses a different bank layout to previous devices, and the above technique is not compatible. The optimal configuration is to keep a rootable firmware in bank 1, and the firmware you use in bank 2. You should always switch back to bank 1 before updating bank 2 with new firmware. This will have a similar effect to the true optimal bank plan that can be implemented on previous generation devices.* 285 | 286 | See [Firmware Banks Explained](#firmware-banks-explained) for more information. 287 | 288 | ##### *All devices except Telstra Smart Modem Gen 3* 289 | 290 | 1. Make sure you are in the USB directory:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 291 | 1. Check whether your bank planning is optimal by executing:
`sh show-bank-plan` 292 | 1. If script reports that your bank plan is not optimal, run:
`sh set-optimal-bank-plan`
(*WARNING: This will reboot your device*) 293 | 294 | ##### *Telstra Smart Modem Gen 3* 295 | 296 | 1. Make sure you are in the USB directory:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 297 | 1. Check whether your bank planning is optimal by executing:
`sh show-bank-plan` 298 | 1. If script reports that your bank plan is not optimal, then: 299 | * If you want to run on the firmware already in bank 2, *AND* you have previously loaded a new firmware into bank 2, run:
`sh reset-to-factory-defaults-with-root -s`
(*WARNING: This will switch banks and reboot your device*) 300 | * If you want to run on a _different_ firmware, *OR* you have never loaded a new firmware into bank 2, follow the [Upgrade Firmware](#3-upgrade-firmware-optional) instructions below. On a Gen 3 device, this will automatically switch banks and make your bank plan optimal. 301 | 302 | *NOTE: Some users have reported bricking the Gen 3 when doing a bank switch without loading firmware into the target bank at least once. The current suggestion is to use `safe-firmware-upgrade` and load a new firmware, even if it is the same version as is currently reported for the target bank. Loading new firmware onto a Gen 3 will **always** switch banks.* 303 | 304 | #### 3. Upgrade Firmware (Optional) 305 | 306 | You can optionally upgrade the firmware at this point. You can even install a Type 3 firmware, because the [safe firmware upgrade](https://github.com/seud0nym/tch-gui-unhide/tree/master/utilities#safe-firmware-upgrade) process retains root access through a different mechanism than that used by `tch-exploit` to initially gain root access. 307 | 308 | *NOTE: To keep an optimal bank plan on a Telstra Smart Modem Gen 3, you should always switch back to bank 1 before updating bank 2 with new firmware. You can switch banks with the command:*
`sh reset-to-factory-defaults-with-root -s`
*However, see the note above about switching to a bank that into which you have **not** previously loaded a new firmware* 309 | 310 | 1. Download the required firmware version for your device from https://hack-technicolor.readthedocs.io/en/latest/Repository/ and save it to your USB stick. 311 | 1. Make sure you are in the USB directory:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 312 | 1. Run `sh safe-firmware-upgrade -?` to see available options. 313 | 314 | #### 4. Harden Root Access 315 | 316 | Hardening root access involves removing the ability for the device to automatically download and apply new firmware when it becomes available, because when firmware is automatically updated in that way, you will *always* lose root access. 317 | 318 | After you have hardened root access, you can reconnect WAN and 4G SIM access. 319 | 320 | ##### *Telstra Branded Devices with Firmware 17.2 and later* 321 | 322 | The [`de-telstra`](https://github.com/seud0nym/tch-gui-unhide/tree/master/utilities#de-telstra) script will harden your root access, and can also disable unwanted services, and apply other configuration options. 323 | 324 | 1. Change the root password by executing:
`passwd` 325 | 1. Make sure you are in the USB directory:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 326 | 1. Run `sh de-telstra -?` to see available options. 327 | 1. For some sensible settings, just execute: `sh de-telstra -A` 328 | 329 | ##### *Other Devices* 330 | 331 | 1. Change the root password by executing:
`passwd` 332 | 1. Follow the instructions at https://hack-technicolor.readthedocs.io/en/latest/Hardening/. 333 | - If you have a Telstra branded device, you do **not** need to follow those instructions. The [`de-telstra`](https://github.com/seud0nym/tch-gui-unhide/tree/master/utilities#de-telstra) script implements all those recommendations, plus other Telstra-specific hardening. 334 | 335 | #### 5. Unlock Features 336 | 337 | ##### *Telstra Branded Devices with Firmware 17.2 and later* 338 | 339 | 1. Make sure you are in the USB directory:
`cd /mnt/usb/USB-A1/` (your mount directory *may* differ). 340 | 1. Optionally, download any [extra feature scripts](https://github.com/seud0nym/tch-gui-unhide/tree/master/extras) you want to install into the same directory as the scripts.
(*IMPORTANT: Make sure you have installed all pre-requisites as well*) 341 | 1. Optionally create your *ipv4-DNS-Servers* and/or *ipv6-DNS-Servers* files in the same directory as the scripts. (See [Optionally Configure Additional DNS Servers](https://github.com/seud0nym/tch-gui-unhide/wiki/Before-Installing#dns)) 342 | 1. Apply the GUI changes.
Run `sh tch-gui-unhide -?` to see available options, or just execute: `sh tch-gui-unhide` 343 | 1. Optionally run `sh tch-gui-unhide-cards` to change card sequence and visibility (card visibility can also be changed from the `Management` card) 344 | 345 | ##### *Other Devices* 346 | 347 | - https://hack-technicolor.readthedocs.io/en/latest/Unlock/ contains tips to unlock functionality on your device. 348 | - If you have a Telstra branded device, you do **not** need to follow those instructions. The `tch-gui-unhide` GUI modification allows you to access most of the functionality listed, plus other features not listed there. -------------------------------------------------------------------------------- /images/IP_address.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/IP_address.jpg -------------------------------------------------------------------------------- /images/WPS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/WPS.png -------------------------------------------------------------------------------- /images/finished.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/finished.jpg -------------------------------------------------------------------------------- /images/ipconfig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/ipconfig.jpg -------------------------------------------------------------------------------- /images/press_WPS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/press_WPS.jpg -------------------------------------------------------------------------------- /images/screen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/screen1.jpg -------------------------------------------------------------------------------- /images/screen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/screen2.jpg -------------------------------------------------------------------------------- /images/tch-exploit-win.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/tch-exploit-win.jpg -------------------------------------------------------------------------------- /images/tftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seud0nym/tch-exploit/2951b8c8e0ede0c4f2e8b4aba08a4fd7b7159e5e/images/tftp.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tch-exploit", 3 | "version": "2.0.2-seud0nym", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tch-exploit", 9 | "version": "2.0.2-seud0nym", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tftp": "file:../node-tftp" 13 | }, 14 | "bin": { 15 | "tch-exploit": "dist/index.js" 16 | } 17 | }, 18 | "../node-tftp": { 19 | "name": "tftp", 20 | "version": "0.1.3-seudonym", 21 | "license": "MIT", 22 | "dependencies": { 23 | "argp": "1.0.x", 24 | "status-bar": "2.0.x" 25 | }, 26 | "bin": { 27 | "ntftp": "bin/ntftp.js" 28 | }, 29 | "engines": { 30 | "node": ">=0.10" 31 | } 32 | }, 33 | "../node-tftp/node_modules/argp": { 34 | "version": "1.0.4", 35 | "resolved": "https://registry.npmjs.org/argp/-/argp-1.0.4.tgz", 36 | "integrity": "sha512-dBwgZOneFCvOmuPsde14fGvdgv8eEGlFPSbHlcPb+6i0491pbnbnlp9SVxF745hlK9akq9MGtNFLO0gl6HFxLA==" 37 | }, 38 | "../node-tftp/node_modules/progress-bar-formatter": { 39 | "version": "2.0.1", 40 | "resolved": "https://registry.npmjs.org/progress-bar-formatter/-/progress-bar-formatter-2.0.1.tgz", 41 | "integrity": "sha512-sQ5mPzh8FdliwxVqdZihcX8O6JJHZjg2ZS2L2DR89dp+Jc+lnxHduJRuYIygAj+It30w29Ke7D0nqPE455LfQQ==" 42 | }, 43 | "../node-tftp/node_modules/status-bar": { 44 | "version": "2.0.3", 45 | "resolved": "https://registry.npmjs.org/status-bar/-/status-bar-2.0.3.tgz", 46 | "integrity": "sha512-OfZfcnCB+q/QrcgUfgMlarzfcQqdeiOLRjiL3OccOR1o4TSDYcwgocDZGzR4s6UtuACz/f4gbTCaovUMyyTLCA==", 47 | "dependencies": { 48 | "progress-bar-formatter": "^2.0.1" 49 | } 50 | }, 51 | "node_modules/tftp": { 52 | "resolved": "../node-tftp", 53 | "link": true 54 | } 55 | }, 56 | "dependencies": { 57 | "tftp": { 58 | "version": "file:../node-tftp", 59 | "requires": { 60 | "argp": "1.0.x", 61 | "status-bar": "2.0.x" 62 | }, 63 | "dependencies": { 64 | "argp": { 65 | "version": "1.0.4", 66 | "resolved": "https://registry.npmjs.org/argp/-/argp-1.0.4.tgz", 67 | "integrity": "sha512-dBwgZOneFCvOmuPsde14fGvdgv8eEGlFPSbHlcPb+6i0491pbnbnlp9SVxF745hlK9akq9MGtNFLO0gl6HFxLA==" 68 | }, 69 | "progress-bar-formatter": { 70 | "version": "2.0.1", 71 | "resolved": "https://registry.npmjs.org/progress-bar-formatter/-/progress-bar-formatter-2.0.1.tgz", 72 | "integrity": "sha512-sQ5mPzh8FdliwxVqdZihcX8O6JJHZjg2ZS2L2DR89dp+Jc+lnxHduJRuYIygAj+It30w29Ke7D0nqPE455LfQQ==" 73 | }, 74 | "status-bar": { 75 | "version": "2.0.3", 76 | "resolved": "https://registry.npmjs.org/status-bar/-/status-bar-2.0.3.tgz", 77 | "integrity": "sha512-OfZfcnCB+q/QrcgUfgMlarzfcQqdeiOLRjiL3OccOR1o4TSDYcwgocDZGzR4s6UtuACz/f4gbTCaovUMyyTLCA==", 78 | "requires": { 79 | "progress-bar-formatter": "^2.0.1" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tch-exploit", 3 | "version": "2.0.2-seud0nym", 4 | "main": "dist/index.js", 5 | "bin": "dist/index.js", 6 | "scripts": { 7 | "clean": "rm -fr release/* dist/* tch-exploit*.zip", 8 | "compile": "coffee --no-header --bare --compile --output dist src", 9 | "package": "npm run compile && rm -fr release/* && pkg --out-path release/ .", 10 | "compress": "zip -r -X tch-exploit-win.zip release/tch-exploit-win.exe && zip -r -X tch-exploit-macos.zip release/tch-exploit-macos && zip -r -X tch-exploit-linux.zip release/tch-exploit-linux" 11 | }, 12 | "pkg": { 13 | "scripts": "dist/*{,*/}*.js" 14 | }, 15 | "author": "BoLaMN", 16 | "license": "MIT", 17 | "description": "", 18 | "dependencies": { 19 | "tftp": "file:node-tftp" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Building (if you’re extending functionality) 2 | 3 | ``` 4 | npm install pkg coffee-script -g 5 | npm install 6 | git submodule init 7 | git submodule update 8 | npm run compile 9 | npm run package 10 | ``` 11 | -------------------------------------------------------------------------------- /src/args.coffee: -------------------------------------------------------------------------------- 1 | snakeToCamel = (s) -> 2 | s.replace /(\-\w)/g, (m) -> 3 | m[1].toUpperCase() 4 | 5 | module.exports = process.argv 6 | .slice 2, process.argv.length 7 | .reduce (args, str) -> 8 | arg = str.split '=' 9 | flag = arg[0].slice 2, arg[0].length 10 | value = arg[1] or true 11 | 12 | args[snakeToCamel(flag)] = value 13 | args 14 | , {} 15 | -------------------------------------------------------------------------------- /src/ask.coffee: -------------------------------------------------------------------------------- 1 | { networkInterfaces } = require 'os' 2 | 3 | ips = require './ips' 4 | 5 | readline = require 'readline' 6 | .createInterface 7 | input: process.stdin 8 | output: process.stdout 9 | 10 | module.exports = (ip) -> 11 | interval = null 12 | 13 | addr = [] 14 | 15 | check = -> 16 | addr = ips() 17 | 18 | addr.find ({ address }) -> address is ip 19 | 20 | p = new Promise (resolve, reject) -> 21 | 22 | ask = -> 23 | add = check() 24 | 25 | if add then return resolve add 26 | 27 | console.log "Could not find interface with ip: #{ ip }" 28 | console.table addr 29 | 30 | readline.question "Select network interface: ", (idx) -> 31 | if not addr[idx]? 32 | console.log 'Unknown index: ' + idx 33 | return ask() 34 | 35 | if addr[idx].address isnt ip 36 | return ask() 37 | 38 | resolve addr[idx] 39 | 40 | interval = setInterval -> 41 | add = check() 42 | 43 | if add then resolve add 44 | , 2000 45 | 46 | ask() 47 | 48 | p.then (add) -> 49 | console.log "using interface", add 50 | 51 | clearInterval interval 52 | readline.close() 53 | 54 | p 55 | -------------------------------------------------------------------------------- /src/dhcp/constants.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | DHCPDISCOVER: 1 4 | DHCPOFFER: 2 5 | DHCPREQUEST: 3 6 | DHCPDECLINE: 4 7 | DHCPACK: 5 8 | DHCPNAK: 6 9 | DHCPRELEASE: 7 10 | DHCPINFORM: 8 11 | 12 | SERVER_PORT: 67 13 | CLIENT_PORT: 68 14 | 15 | INADDR_ANY: '0.0.0.0' 16 | INADDR_BROADCAST: '255.255.255.255' 17 | 18 | BOOTREQUEST: 1 19 | BOOTREPLY: 2 20 | 21 | DHCPV6: 22 | 23 | MESSAGETYPE: 24 | 1: 'SOLICIT' 25 | 2: 'ADVERTISE' 26 | 3: 'REQUEST' 27 | 4: 'CONFIRM' 28 | 5: 'RENEW' 29 | 6: 'REBIND' 30 | 7: 'REPLY' 31 | 8: 'RELEASE' 32 | 9: 'DECLINE' 33 | 10: 'RECONFIGURE' 34 | 11: 'INFORMATION_REQUEST' 35 | 12: 'RELAY_FORW' 36 | 13: 'RELAY_REPL' 37 | 38 | OPTIONS: 39 | 1: 'CLIENTID' 40 | 2: 'SERVERID' 41 | 3: 'IA_NA' 42 | 4: 'IA_TA' 43 | 5: 'IAADDR' 44 | 6: 'ORO' 45 | 7: 'PREFERENCE' 46 | 8: 'ELAPSED_TIME' 47 | 9: 'RELAY_MSG' 48 | 11: 'AUTH' 49 | 12: 'UNICAST' 50 | 13: 'STATUS_CODE' 51 | 14: 'RAPID_COMMIT' 52 | 15: 'USER_CLASS' 53 | 16: 'VENDOR_CLASS' 54 | 17: 'VENDOR_OPTS' 55 | 18: 'INTERFACE_ID' 56 | 19: 'RECONF_MSG' 57 | 20: 'RECONF_ACCEPT' 58 | 21: 'SIP_SERVER_D' 59 | 22: 'SIP_SERVER_A' 60 | 23: 'DNS_SERVERS' 61 | 24: 'DOMAIN_LIST' 62 | 25: 'IA_PD' 63 | 26: 'IAPREFIX' 64 | 27: 'NIS_SERVERS' 65 | 28: 'NISP_SERVERS' 66 | 29: 'NIS_DOMAIN_NAME' 67 | 30: 'NISP_DOMAIN_NAME' 68 | 31: 'SNTP_SERVERS' 69 | 32: 'INFORMATION_REFRESH_TIME' 70 | 33: 'BCMCS_SERVER_D' 71 | 34: 'BCMCS_SERVER_A' 72 | 36: 'GEOCONF_CIVIC' 73 | 37: 'REMOTE_ID' 74 | 38: 'SUBSCRIBER_ID' 75 | 39: 'CLIENT_FQDN' 76 | 40: 'PANA_AGENT' 77 | 41: 'NEW_POSIX_TIMEZONE' 78 | 42: 'NEW_TZDB_TIMEZONE' 79 | 43: 'ERO' 80 | 44: 'LQ_QUERY' 81 | 45: 'CLIENT_DATA' 82 | 46: 'CLT_TIME' 83 | 47: 'LQ_RELAY_DATA' 84 | 48: 'LQ_CLIENT_LINK' 85 | 49: 'MIP6_HNIDF' 86 | 50: 'MIP6_VDINF' 87 | 51: 'V6_LOST' 88 | 52: 'CAPWAP_AC_V6' 89 | 53: 'RELAY_ID' 90 | 54: 'IPv6AddressMoS' 91 | 55: 'IPv6FQDNMoS' 92 | 56: 'NTP_SERVER' 93 | 57: 'V6_ACCESS_DOMAIN' 94 | 58: 'SIP_UA_CS_LIST' 95 | 59: 'BOOTFILE_URL' 96 | 79: 'CLIENT_LINKLAYER_ADDR' 97 | -------------------------------------------------------------------------------- /src/dhcp/index.coffee: -------------------------------------------------------------------------------- 1 | server = require './server' 2 | 3 | path = require 'path' 4 | 5 | toHexArray = (str) -> 6 | str 7 | .split '' 8 | .map (d, i) -> str.charCodeAt i 9 | 10 | module.exports = (ip, acsurl, acspass) -> 11 | ip = ip.split '.' 12 | ip.pop() 13 | ip = ip.join '.' 14 | 15 | acsurl = toHexArray acsurl 16 | acspass = if acspass then toHexArray(acspass) else [ 84, 101, 108, 115, 116, 114, 97 ] 17 | 18 | vendor = [ 1, acsurl.length ].concat acsurl.concat [ 2, acspass.length ].concat acspass 19 | 20 | server 21 | .createServer 22 | range: [ 23 | ip + '.10' 24 | ip + '.15' 25 | ] 26 | forceOptions: [ 'router', 'hostname', 'vendor' ] 27 | randomIP: true 28 | vendor: vendor 29 | netmask: '255.255.255.0' 30 | router: [ ip + '.1' ] 31 | hostname: 'second.gateway' 32 | broadcast: ip + '.255' 33 | server: ip + '.1' 34 | .on 'listening', (sock, type) -> 35 | { address, port } = sock.address() 36 | 37 | console.log "Waiting for DHCP#{type} request... #{ address }:#{ port }" 38 | .on 'message', (data) -> 39 | console.log '### MESSAGE', JSON.stringify data 40 | .on 'bound', (state, ans) -> 41 | console.log '### BOUND', JSON.stringify state 42 | .on 'error', (err, data) -> 43 | return unless data 44 | 45 | console.log '!!! ERROR', err, data 46 | .listen 67 47 | 48 | server 49 | -------------------------------------------------------------------------------- /src/dhcp/lease.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Lease 3 | 4 | bindTime: null 5 | leasePeriod: 86400 6 | renewPeriod: 1440 7 | rebindPeriod: 14400 8 | state: null 9 | server: null 10 | address: null 11 | options: null 12 | tries: 0 13 | xid: 1 14 | 15 | module.exports = Lease 16 | -------------------------------------------------------------------------------- /src/dhcp/options.coffee: -------------------------------------------------------------------------------- 1 | Tools = require './tools' 2 | 3 | opts = 4 | 1: 5 | name: 'Subnet Mask' 6 | type: 'IP' 7 | config: 'netmask' 8 | default: -> 9 | range = @config 'range' 10 | net = Tools.netmaskFromRange range[0], range[1] 11 | 12 | Tools.formatIp net 13 | 2: 14 | name: 'Time Offset' 15 | type: 'Int32' 16 | config: 'timeOffset' 17 | 3: 18 | name: 'Router' 19 | type: 'IPs' 20 | config: 'router' 21 | default: -> 22 | range = @config 'range' 23 | range[0] 24 | 4: 25 | name: 'Time Server' 26 | type: 'IPs' 27 | config: 'timeServer' 28 | 5: 29 | name: 'Name Server' 30 | type: 'IPs' 31 | config: 'nameServer' 32 | 6: 33 | name: 'Domain Name Server' 34 | type: 'IPs' 35 | config: 'dns' 36 | default: [ 37 | '8.8.8.8' 38 | '8.8.4.4' 39 | ] 40 | 7: 41 | name: 'Log Server' 42 | type: 'IPs' 43 | config: 'logServer' 44 | 8: 45 | name: 'Cookie Server' 46 | type: 'IPs' 47 | config: 'cookieServer' 48 | 9: 49 | name: 'LPR Server' 50 | type: 'IPs' 51 | config: 'lprServer' 52 | 10: 53 | name: 'Impress Server' 54 | type: 'IPs' 55 | config: 'impressServer' 56 | 11: 57 | name: 'Resource Location Server' 58 | type: 'IPs' 59 | config: 'rscServer' 60 | 12: 61 | name: 'Host Name' 62 | type: 'ASCII' 63 | config: 'hostname' 64 | 13: 65 | name: 'Boot File Size' 66 | type: 'UInt16' 67 | config: 'bootFileSize' 68 | 14: 69 | name: 'Merit Dump File' 70 | type: 'ASCII' 71 | config: 'dumpFile' 72 | 15: 73 | name: 'Domain Name' 74 | type: 'ASCII' 75 | config: 'domainName' 76 | 16: 77 | name: 'Swap Server' 78 | type: 'IP' 79 | config: 'swapServer' 80 | 17: 81 | name: 'Root Path' 82 | type: 'ASCII' 83 | config: 'rootPath' 84 | 18: 85 | name: 'Extension Path' 86 | type: 'ASCII' 87 | config: 'extensionPath' 88 | 19: 89 | name: 'IP Forwarding' 90 | type: 'UInt8' 91 | config: 'ipForwarding' 92 | enum: 93 | 0: 'Disabled' 94 | 1: 'Enabled' 95 | 20: 96 | name: 'Non-Local Source Routing' 97 | type: 'Bool' 98 | config: 'nonLocalSourceRouting' 99 | 21: 100 | name: 'Policy Filter' 101 | type: 'IPs' 102 | config: 'policyFilter' 103 | 22: 104 | name: 'Maximum Datagram Reassembly Size' 105 | type: 'UInt16' 106 | config: 'maxDatagramSize' 107 | 23: 108 | name: 'Default IP Time-to-live' 109 | type: 'UInt8' 110 | config: 'datagramTTL' 111 | 24: 112 | name: 'Path MTU Aging Timeout' 113 | type: 'UInt32' 114 | config: 'mtuTimeout' 115 | 25: 116 | name: 'Path MTU Plateau Table' 117 | type: 'UInt16s' 118 | config: 'mtuSizes' 119 | 26: 120 | name: 'Interface MTU' 121 | type: 'UInt16' 122 | config: 'mtuInterface' 123 | 27: 124 | name: 'All Subnets are Local' 125 | type: 'UInt8' 126 | config: 'subnetsAreLocal' 127 | enum: 128 | 0: 'Disabled' 129 | 1: 'Enabled' 130 | 28: 131 | name: 'Broadcast Address' 132 | type: 'IP' 133 | config: 'broadcast' 134 | default: -> 135 | range = @config 'range' 136 | ip = range[0] 137 | 138 | cidr = Tools.CIDRFromNetmask @config('netmask') 139 | 140 | Tools.formatIp Tools.broadcastFromIpCIDR ip, cidr 141 | 29: 142 | name: 'Perform Mask Discovery' 143 | type: 'UInt8' 144 | config: 'maskDiscovery' 145 | enum: 146 | 0: 'Disabled' 147 | 1: 'Enabled' 148 | 30: 149 | name: 'Mask Supplier' 150 | type: 'UInt8' 151 | config: 'maskSupplier' 152 | enum: 153 | 0: 'Disabled' 154 | 1: 'Enabled' 155 | 31: 156 | name: 'Perform Router Discovery' 157 | type: 'UInt8' 158 | config: 'routerDiscovery' 159 | enum: 160 | 0: 'Disabled' 161 | 1: 'Enabled' 162 | 32: 163 | name: 'Router Solicitation Address' 164 | type: 'IP' 165 | config: 'routerSolicitation' 166 | 33: 167 | name: 'Static Route' 168 | type: 'IPs' 169 | config: 'staticRoutes' 170 | 34: 171 | name: 'Trailer Encapsulation' 172 | type: 'Bool' 173 | config: 'trailerEncapsulation' 174 | 35: 175 | name: 'ARP Cache Timeout' 176 | type: 'UInt32' 177 | config: 'arpCacheTimeout' 178 | 36: 179 | name: 'Ethernet Encapsulation' 180 | type: 'Bool' 181 | config: 'ethernetEncapsulation' 182 | 37: 183 | name: 'TCP Default TTL' 184 | type: 'UInt8' 185 | config: 'tcpTTL' 186 | 38: 187 | name: 'TCP Keepalive Interval' 188 | type: 'UInt32' 189 | config: 'tcpKeepalive' 190 | 39: 191 | name: 'TCP Keepalive Garbage' 192 | type: 'Bool' 193 | config: 'tcpKeepaliveGarbage' 194 | 40: 195 | name: 'Network Information Service Domain' 196 | type: 'ASCII' 197 | config: 'nisDomain' 198 | 41: 199 | name: 'Network Information Servers' 200 | type: 'IPs' 201 | config: 'nisServer' 202 | 42: 203 | name: 'Network Time Protocol Servers' 204 | type: 'IPs' 205 | config: 'ntpServer' 206 | 43: 207 | name: 'Vendor Specific Information' 208 | type: 'UInt8s' 209 | config: 'vendor' 210 | 44: 211 | name: 'NetBIOS over TCP/IP Name Server' 212 | type: 'IPs' 213 | config: 'nbnsServer' 214 | 45: 215 | name: 'NetBIOS over TCP/IP Datagram Distribution Server' 216 | type: 'IP' 217 | config: 'nbddServer' 218 | 46: 219 | name: 'NetBIOS over TCP/IP Node Type' 220 | type: 'UInt8' 221 | enum: 222 | 0x1: 'B-node' 223 | 0x2: 'P-node' 224 | 0x4: 'M-node' 225 | 0x8: 'H-node' 226 | config: 'nbNodeType' 227 | 47: 228 | name: 'NetBIOS over TCP/IP Scope' 229 | type: 'ASCII' 230 | config: 'nbScope' 231 | 48: 232 | name: 'X Window System Font Server' 233 | type: 'IPs' 234 | config: 'xFontServer' 235 | 49: 236 | name: 'X Window System Display Manager' 237 | type: 'IPs' 238 | config: 'xDisplayManager' 239 | 50: 240 | name: 'Requested IP Address' 241 | type: 'IP' 242 | attr: 'requestedIpAddress' 243 | 51: 244 | name: 'IP Address Lease Time' 245 | type: 'UInt32' 246 | config: 'leaseTime' 247 | default: 86400 248 | 52: 249 | name: 'Option Overload' 250 | type: 'UInt8' 251 | enum: 252 | 1: 'file' 253 | 2: 'sname' 254 | 3: 'both' 255 | 53: 256 | name: 'DHCP Message Type' 257 | type: 'UInt8' 258 | enum: 259 | 1: 'DHCPDISCOVER' 260 | 2: 'DHCPOFFER' 261 | 3: 'DHCPREQUEST' 262 | 4: 'DHCPDECLINE' 263 | 5: 'DHCPACK' 264 | 6: 'DHCPNAK' 265 | 7: 'DHCPRELEASE' 266 | 8: 'DHCPINFORM' 267 | 54: 268 | name: 'Server Identifier' 269 | type: 'IP' 270 | config: 'server' 271 | 55: 272 | name: 'Parameter Request List' 273 | type: 'UInt8s' 274 | attr: 'requestParameter' 275 | 56: 276 | name: 'Message' 277 | type: 'ASCII' 278 | 57: 279 | name: 'Maximum DHCP Message Size' 280 | type: 'UInt16' 281 | config: 'maxMessageSize' 282 | default: 1500 283 | 58: 284 | name: 'Renewal (T1) Time Value' 285 | type: 'UInt32' 286 | config: 'renewalTime' 287 | default: 3600 288 | 59: 289 | name: 'Rebinding (T2) Time Value' 290 | type: 'UInt32' 291 | config: 'rebindingTime' 292 | default: 14400 293 | 60: 294 | name: 'Vendor Class-Identifier' 295 | type: 'ASCII' 296 | attr: 'vendorClassId' 297 | config: 'vendorClassId' 298 | 61: 299 | name: 'Client-Identifier' 300 | type: 'ASCII' 301 | attr: 'clientId' 302 | 64: 303 | name: 'Network Information Service+ Domain' 304 | type: 'ASCII' 305 | config: 'nisPlusDomain' 306 | 65: 307 | name: 'Network Information Service+ Servers' 308 | type: 'IPs' 309 | config: 'nisPlusServer' 310 | 66: 311 | name: 'TFTP server name' 312 | type: 'ASCII' 313 | config: 'tftpServer' 314 | 67: 315 | name: 'Bootfile name' 316 | type: 'ASCII' 317 | config: 'bootFile' 318 | 68: 319 | name: 'Mobile IP Home Agent' 320 | type: 'ASCII' 321 | config: 'homeAgentAddresses' 322 | 69: 323 | name: 'Simple Mail Transport Protocol (SMTP) Server' 324 | type: 'IPs' 325 | config: 'smtpServer' 326 | 70: 327 | name: 'Post Office Protocol (POP3) Server' 328 | type: 'IPs' 329 | config: 'pop3Server' 330 | 71: 331 | name: 'Network News Transport Protocol (NNTP) Server' 332 | type: 'IPs' 333 | config: 'nntpServer' 334 | 72: 335 | name: 'Default World Wide Web (WWW) Server' 336 | type: 'IPs' 337 | config: 'wwwServer' 338 | 73: 339 | name: 'Default Finger Server' 340 | type: 'IPs' 341 | config: 'fingerServer' 342 | 74: 343 | name: 'Default Internet Relay Chat (IRC) Server' 344 | type: 'IPs' 345 | config: 'ircServer' 346 | 75: 347 | name: 'StreetTalk Server' 348 | type: 'IPs' 349 | config: 'streetTalkServer' 350 | 76: 351 | name: 'StreetTalk Directory Assistance (STDA) Server' 352 | type: 'IPs' 353 | config: 'streetTalkDAServer' 354 | 80: 355 | name: 'Rapid Commit' 356 | type: 'Bool' 357 | attr: 'rapidCommit' 358 | 93: 359 | name: 'Client System' 360 | type: 'HEX' 361 | config: 'clientSystem' 362 | 94: 363 | name: 'Client NDI' 364 | type: 'HEX' 365 | attr: 'clientNdi' 366 | config: 'clientNdi' 367 | 97: 368 | name: 'UUID/GUID' 369 | type: 'HEX', 370 | config: 'uuidGuid' 371 | 116: 372 | name: 'Auto-Configure' 373 | type: 'UInt8' 374 | enum: 375 | 0: 'DoNotAutoConfigure' 376 | 1: 'AutoConfigure' 377 | attr: 'autoConfigure' 378 | 118: 379 | name: 'Subnet Selection' 380 | type: 'IP' 381 | config: 'subnetSelection' 382 | 119: 383 | name: 'Domain Search List' 384 | type: 'ASCII' 385 | config: 'domainSearchList' 386 | 121: 387 | name: 'Classless Route Option Format' 388 | type: 'IPs' 389 | config: 'classlessRoute' 390 | 145: 391 | name: 'Forcerenew Nonce' 392 | type: 'UInt8s' 393 | attr: 'renewNonce' 394 | 252: 395 | name: 'Web Proxy Auto-Discovery' 396 | type: 'ASCII' 397 | config: 'wpad' 398 | 1001: 399 | name: 'Static' 400 | config: 'static' 401 | 1002: 402 | name: 'Random IP' 403 | type: 'Bool' 404 | config: 'randomIP' 405 | default: true 406 | 407 | conf = {} 408 | attr = {} 409 | 410 | for i, v of opts 411 | if v.config 412 | conf[v.config] = parseInt(i, 10) 413 | else if v.attr 414 | conf[v.attr] = parseInt(i, 10) 415 | 416 | module.exports = { opts, conf, attr } 417 | -------------------------------------------------------------------------------- /src/dhcp/protocol.coffee: -------------------------------------------------------------------------------- 1 | SeqBuffer = require './seqbuffer' 2 | 3 | module.exports = 4 | parse: (buf) -> 5 | if buf.length < 230 6 | throw new Error 'Received data is too short' 7 | 8 | sb = new SeqBuffer buf 9 | 10 | op: sb.getUInt8() 11 | htype: htype = sb.getUInt8() 12 | hlen: hlen = sb.getUInt8() 13 | hops: sb.getUInt8() 14 | xid: sb.getUInt32() 15 | secs: sb.getUInt16() 16 | flags: sb.getUInt16() 17 | ciaddr: sb.getIP() 18 | yiaddr: sb.getIP() 19 | siaddr: sb.getIP() 20 | giaddr: sb.getIP() 21 | chaddr: sb.getMAC(htype, hlen) 22 | sname: sb.getUTF8(64) 23 | file: sb.getUTF8(128) 24 | magicCookie: sb.getUInt32() 25 | options: sb.getOptions() 26 | 27 | format: (data) -> 28 | sb = new SeqBuffer 29 | 30 | sb.addUInt8 data.op 31 | sb.addUInt8 data.htype 32 | sb.addUInt8 data.hlen 33 | sb.addUInt8 data.hops 34 | sb.addUInt32 data.xid 35 | sb.addUInt16 data.secs 36 | sb.addUInt16 data.flags 37 | sb.addIP data.ciaddr 38 | sb.addIP data.yiaddr 39 | sb.addIP data.siaddr 40 | sb.addIP data.giaddr 41 | sb.addMac data.chaddr 42 | sb.addUTF8Pad data.sname, 64 43 | sb.addUTF8Pad data.file, 128 44 | sb.addUInt32 0x63825363 45 | sb.addOptions data.options 46 | sb.addUInt8 255 47 | 48 | sb 49 | 50 | parseIpv6: (msg, rinfo) -> 51 | options = { op: msg.readUInt8(0) } 52 | 53 | offset = 0 54 | 55 | readAddressRaw = (msg, offset, len) -> 56 | addr = '' 57 | 58 | while len-- > 0 59 | b = msg.readUInt8(offset++) 60 | addr += (b + 0x100).toString(16).substr(-2) 61 | 62 | if len > 0 63 | addr += ':' 64 | 65 | addr 66 | 67 | if options.op is 12 68 | hopCount = msg.readUInt8(1) 69 | linkAddress = '' 70 | 71 | i = 2 72 | 73 | while i < 18 74 | if i != 2 75 | linkAddress = linkAddress + ':' 76 | 77 | linkHex = msg.readUInt16BE(i).toString(16) 78 | 79 | linkPre = switch linkHex.length 80 | when 3 then '0' 81 | when 2 then '00' 82 | when 1 then '000' 83 | else '' 84 | 85 | linkAddress = linkAddress + linkPre + linkHex 86 | 87 | i = i + 2 88 | 89 | linkAddress = linkAddress 90 | peerAddress = '' 91 | 92 | o = 18 93 | 94 | while o < 34 95 | if o != 18 96 | peerAddress = peerAddress + ':' 97 | 98 | peerHex = msg.readUInt16BE(o).toString(16) 99 | 100 | peerPre = switch peerHex.length 101 | when 3 then '0' 102 | when 2 then '00' 103 | when 1 then '000' 104 | else '' 105 | 106 | peerAddress = peerAddress + peerPre + peerHex 107 | 108 | o = o + 2 109 | 110 | peerAddress = peerAddress 111 | 112 | offset = 33 113 | else 114 | xid = msg.readUInt8(1).toString(16) + msg.readUInt8(2).toString(16) + msg.readUInt8(3).toString(16) 115 | 116 | options.xid = switch xid.length 117 | when 1 then '00000' + xid 118 | when 2 then '0000' + xid 119 | when 3 then '000' + xid 120 | when 4 then '00' + xid 121 | when 5 then '0' + xid 122 | 123 | offset = 3 124 | 125 | while offset < msg.length - 1 126 | optionCode = msg.readInt16BE(offset + 1) 127 | optionLen = msg.readInt16BE(offset + 3) 128 | 129 | offset = offset + 4 130 | 131 | switch optionCode 132 | when 1 133 | DUIDType = msg.readUInt16BE(offset + 1) 134 | iBuf = new Buffer(optionLen) 135 | 136 | msg.copy iBuf, 0, offset + 1, offset + 1 + optionLen 137 | 138 | options.clientIdentifierOption = switch DUIDType 139 | when 1 140 | buffer: iBuf 141 | DUIDType: DUIDType 142 | hardwareType: msg.readUInt16BE(offset + 3) 143 | time: msg.readUInt32BE(offset + 5) 144 | linkLayerAddress: readAddressRaw(msg, offset + 9, optionLen - 8) 145 | when 2 146 | buffer: iBuf 147 | DUIDType: DUIDType 148 | enterpriseNumber: undefined 149 | enterpriseNumberContd: undefined 150 | identifier: undefined 151 | when 3 152 | buffer: iBuf 153 | DUIDType: DUIDType 154 | hardwareType: msg.readUInt16BE(offset + 3) 155 | linkLayerAddress: readAddressRaw(msg, offset + 5, optionLen - 4) 156 | 157 | offset += optionLen 158 | 159 | break 160 | when 2 161 | DUIDType = msg.readUInt16BE(offset + 1) 162 | iBuf = new Buffer(optionLen) 163 | 164 | msg.copy iBuf, 0, offset + 1, offset + 1 + optionLen 165 | 166 | options.serverIdentifierOption = switch DUIDType 167 | when 1 168 | buffer: iBuf 169 | DUIDType: DUIDType 170 | hardwareType: msg.readUInt16BE(offset + 3) 171 | time: msg.readUInt32BE(offset + 5) 172 | linkLayerAddress: readAddressRaw(msg, offset + 9, optionLen - 8) 173 | when 2 174 | buffer: iBuf 175 | DUIDType: DUIDType 176 | enterpriseNumber: undefined 177 | enterpriseNumberContd: undefined 178 | identifier: undefined 179 | when 3 180 | buffer: iBuf 181 | DUIDType: DUIDType 182 | hardwareType: msg.readUInt16BE(offset + 3) 183 | linkLayerAddress: readAddressRaw(msg, offset + 5, optionLen - 4) 184 | 185 | offset += optionLen 186 | 187 | break 188 | when 3 189 | options.IA_NA = 190 | IAID: msg.readUInt32BE(offset + 1) 191 | T1: msg.readUInt32BE(offset + 5) 192 | T2: msg.readUInt32BE(offset + 9) 193 | 194 | offset += optionLen 195 | 196 | break 197 | when 6 198 | options.request = [] 199 | options.requestDesc = [] 200 | 201 | i6 = 1 202 | 203 | while i6 < optionLen 204 | num = msg.readUInt16BE(offset + i6) 205 | prot = protocol.DHCPv6OptionsCode.get(num) 206 | 207 | if prot and prot.name 208 | options.requestDesc.push prot.name 209 | 210 | options.request.push num 211 | 212 | i6 = i6 + 2 213 | 214 | offset += optionLen 215 | 216 | break 217 | when 8 218 | options.elapsedTime = msg.readUInt16BE(offset + 1) 219 | 220 | offset += optionLen 221 | 222 | break 223 | when 9 224 | relayBuf = new Buffer(optionLen) 225 | msg.copy relayBuf, 0, offset + 1, offset + 1 + optionLen 226 | 227 | options.dhcpRelayMessage = parser6.parse(relayBuf, rinfo) 228 | 229 | offset += optionLen 230 | 231 | break 232 | when 18 233 | interfaceID = new Buffer(optionLen) 234 | 235 | msg.copy interfaceID, 0, offset + 1, offset + 1 + optionLen 236 | 237 | options.interfaceID = 238 | hex: msg.toString('hex', offset + 1, offset + 1 + optionLen) 239 | buffer: interfaceID 240 | 241 | offset += optionLen 242 | 243 | break 244 | when 25 245 | options.IA_PD = 246 | IAID: msg.readUInt32BE(offset + 1) 247 | T1: msg.readUInt32BE(offset + 5) 248 | T2: msg.readUInt32BE(offset + 9) 249 | 250 | offset += optionLen 251 | 252 | break 253 | when 37 254 | options.relayAgentRemoteID = 255 | enterpriseNumber: msg.readUInt32BE(offset + 1) 256 | remoteId: msg.toString('hex', offset + 5, offset + 1 + optionLen) 257 | 258 | offset += optionLen 259 | 260 | break 261 | when 39 262 | options.clientFQDN = 263 | flags: msg.readUInt8(offset + 1) 264 | domainName: msg.toString('utf8', offset + 2, offset + 1 + optionLen) 265 | 266 | offset += optionLen 267 | 268 | break 269 | else 270 | codeName = DHCPV6.OPTIONSCODE[optionCode] 271 | 272 | console.log 'Unhandled DHCPv6 option ' + optionCode + ' (' + codeName + ')/' + optionLen + 'b' 273 | 274 | offset += optionLen 275 | 276 | break 277 | 278 | options 279 | -------------------------------------------------------------------------------- /src/dhcp/seqbuffer.coffee: -------------------------------------------------------------------------------- 1 | { opts } = require './options' 2 | 3 | trimZero = (str) -> 4 | pos = str.indexOf('\u0000') 5 | 6 | if pos == -1 then str else str.substr(0, pos) 7 | 8 | class SeqBuffer 9 | constructor: (buf, len) -> 10 | @_data = buf or Buffer.alloc(len or 1500) 11 | 12 | _data: null 13 | _r: 0 14 | _w: 0 15 | 16 | addUInt8: (val) -> 17 | @_w = @_data.writeUInt8(val, @_w, true) 18 | 19 | getUInt8: -> 20 | @_data.readUInt8 @_r++, true 21 | 22 | addInt8: (val) -> 23 | @_w = @_data.writeInt8(val, @_w, true) 24 | 25 | getInt8: -> 26 | @_data.readInt8 @_r++, true 27 | 28 | addUInt16: (val) -> 29 | @_w = @_data.writeUInt16BE(val, @_w, true) 30 | 31 | getUInt16: -> 32 | @_data.readUInt16BE (@_r += 2) - 2, true 33 | 34 | addInt16: (val) -> 35 | @_w = @_data.writeInt16BE(val, @_w, true) 36 | 37 | getInt16: -> 38 | @_data.readInt16BE (@_r += 2) - 2, true 39 | 40 | addUInt32: (val) -> 41 | @_w = @_data.writeUInt32BE(val, @_w, true) 42 | 43 | getUInt32: -> 44 | @_data.readUInt32BE (@_r += 4) - 4, true 45 | 46 | addInt32: (val) -> 47 | @_w = @_data.writeInt32BE(val, @_w, true) 48 | 49 | getInt32: -> 50 | @_data.readInt32BE (@_r += 4) - 4, true 51 | 52 | addUTF8: (val) -> 53 | @_w += @_data.write(val, @_w, 'utf8') 54 | 55 | addUTF8Pad: (val, fixLen) -> 56 | len = Buffer.from(val, 'utf8').length 57 | n = 0 58 | 59 | while len > fixLen 60 | val = val.slice(0, fixLen - n) 61 | len = Buffer.from(val, 'utf8').length 62 | 63 | n++ 64 | 65 | @_data.fill 0, @_w, @_w + fixLen 66 | @_data.write val, @_w, 'utf8' 67 | 68 | @_w += fixLen 69 | 70 | getUTF8: (len) -> 71 | trimZero @_data.toString('utf8', @_r, @_r += len) 72 | 73 | addASCII: (val) -> 74 | @_w += @_data.write(val, @_w, 'ascii') 75 | 76 | addASCIIPad: (val, fixLen) -> 77 | @_data.fill 0, @_w, @_w + fixLen 78 | @_data.write val.slice(0, fixLen), @_w, 'ascii' 79 | 80 | @_w += fixLen 81 | 82 | getASCII: (len) -> 83 | trimZero @_data.toString('ascii', @_r, @_r += len) 84 | 85 | addIP: (ip) -> 86 | return unless typeof ip is 'string' 87 | 88 | octs = ip.split('.') 89 | 90 | if octs.length != 4 91 | throw new Error('Invalid IP address ' + ip) 92 | 93 | for val in octs 94 | val = parseInt(val, 10) 95 | 96 | if 0 <= val and val < 256 97 | @addUInt8 val 98 | else 99 | throw new Error('Invalid IP address ' + ip) 100 | 101 | getIP: -> 102 | @getUInt8() + '.' + @getUInt8() + '.' + @getUInt8() + '.' + @getUInt8() 103 | 104 | addIPs: (ips) -> 105 | if ips instanceof Array 106 | for ip in ips 107 | @addIP ip 108 | else 109 | @addIP ips 110 | 111 | getIPs: (len) -> 112 | ret = [] 113 | i = 0 114 | 115 | while i < len 116 | ret.push @getIP() 117 | i += 4 118 | 119 | ret 120 | 121 | addHex: (val, fixLen) -> 122 | if fixLen 123 | @_data.fill 0, @_w, @_w + fixLen 124 | @_data.write val.slice(0, fixLen), @_w, 'hex' 125 | 126 | @_w += fixLen 127 | else 128 | @_w += @_data.write(val, @_w, 'hex') 129 | 130 | getHEX: (hlen) -> 131 | @_data.toString 'hex', @_r, @_r += hlen 132 | 133 | addMac: (mac) -> 134 | octs = mac.split(/[-:]/) 135 | 136 | if octs.length != 6 137 | throw new Error 'Invalid Mac address ' + mac 138 | 139 | for val in octs 140 | val = parseInt(val, 16) 141 | 142 | if 0 <= val and val < 256 143 | @addUInt8 val 144 | else 145 | throw new Error 'Invalid Mac address ' + mac 146 | 147 | @addUInt32 0 148 | @addUInt32 0 149 | @addUInt16 0 150 | 151 | getMAC: (htype, hlen) -> 152 | mac = @_data.toString 'hex', @_r, @_r += hlen 153 | 154 | if htype != 1 or hlen != 6 155 | throw new Error "Invalid hardware address (len=#{ hlen }, type=#{ htype })" 156 | 157 | @_r += 10 158 | 159 | mac.toUpperCase().match(/../g).join '-' 160 | 161 | addBool: -> 162 | return 163 | 164 | getBool: -> 165 | true 166 | 167 | addOptions: (ops) -> 168 | 169 | for own i, val of ops 170 | opt = opts[i] 171 | len = 0 172 | 173 | if val == null 174 | i += 4 175 | continue 176 | 177 | switch opt.type 178 | when 'UInt8', 'Int8' 179 | len = 1 180 | when 'UInt16', 'Int16' 181 | len = 2 182 | when 'UInt32', 'Int32', 'IP' 183 | len = 4 184 | when 'IPs' 185 | len = if val instanceof Array then 4 * val.length else 4 186 | when 'ASCII' 187 | len = val.length 188 | 189 | if len == 0 190 | i += 4 191 | continue 192 | 193 | if len > 255 194 | console.error val + ' too long, truncating...' 195 | val = val.slice 0, 255 196 | len = 255 197 | when 'HEX' 198 | len = val.length 199 | 200 | if len is 0 201 | continue 202 | 203 | if len > 255 204 | console.error val + ' too long, truncating...' 205 | 206 | val = val.slice 0, 255 207 | len = 255 208 | 209 | when 'UTF8' 210 | len = Buffer.from(val, 'utf8').length 211 | 212 | if len == 0 213 | i += 4 214 | continue 215 | 216 | n = 0 217 | 218 | while len > 255 219 | val = val.slice(0, 255 - n) 220 | len = Buffer.from(val, 'utf8').length 221 | 222 | n++ 223 | when 'Bool' 224 | if !(val == true or val == 1 or val == '1' or val == 'true' or val == 'TRUE' or val == 'True') 225 | n++ 226 | continue 227 | when 'UInt8s' 228 | len = if val instanceof Array then val.length else 1 229 | when 'UInt16s' 230 | len = if val instanceof Array then 2 * val.length else 2 231 | else 232 | throw new Error('No such type ' + opt.type) 233 | 234 | @addUInt8 i 235 | @addUInt8 len 236 | 237 | @['add' + opt.type] val 238 | 239 | getOptions: -> 240 | options = {} 241 | 242 | buf = @_data 243 | 244 | while @_r < buf.length 245 | opt = @getUInt8() 246 | 247 | if opt == 0xff 248 | break 249 | else if opt == 0x00 250 | @_r++ 251 | else 252 | len = @getUInt8() 253 | 254 | if opt of opts 255 | options[opt] = @['get' + opts[opt].type](len) 256 | else 257 | @_r += len 258 | 259 | console.error "Option #{ opt } not known" 260 | 261 | options 262 | 263 | addUInt8s: (arr) -> 264 | if arr instanceof Array 265 | i = 0 266 | 267 | while i < arr.length 268 | @addUInt8 arr[i] 269 | i++ 270 | else 271 | @addUInt8 arr 272 | 273 | getUInt8s: (len) -> 274 | ret = [] 275 | i = 0 276 | 277 | while i < len 278 | ret.push @getUInt8() 279 | i++ 280 | 281 | ret 282 | 283 | addUInt16s: (arr) -> 284 | if arr instanceof Array 285 | i = 0 286 | 287 | while i < arr.length 288 | @addUInt16 arr[i] 289 | i++ 290 | else 291 | @addUInt16 arr 292 | 293 | getUInt16s: (len) -> 294 | ret = [] 295 | i = 0 296 | 297 | while i < len 298 | ret.push @getUInt16() 299 | i += 2 300 | 301 | ret 302 | 303 | getHex: (len) -> 304 | @_data.toString 'hex', @_r, @_r += len 305 | 306 | module.exports = SeqBuffer 307 | -------------------------------------------------------------------------------- /src/dhcp/server.coffee: -------------------------------------------------------------------------------- 1 | dgram = require 'dgram' 2 | os = require 'os' 3 | 4 | { EventEmitter } = require 'events' 5 | 6 | Lease = require './lease' 7 | SeqBuffer = require './seqbuffer' 8 | Options = require './options' 9 | Protocol = require './protocol' 10 | Tools = require './tools' 11 | Ips = require '../ips' 12 | 13 | { 14 | DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPDECLINE 15 | DHCPACK, DHCPNAK, DHCPRELEASE, DHCPINFORM 16 | SERVER_PORT, CLIENT_PORT 17 | INADDR_ANY, INADDR_BROADCAST 18 | BOOTREQUEST, BOOTREPLY, DHCPV6 19 | } = require './constants' 20 | 21 | class Server extends EventEmitter 22 | @createServer: (opt) -> 23 | new Server(opt) 24 | 25 | constructor: (config) -> 26 | super() 27 | 28 | sock = dgram.createSocket 29 | type: 'udp4' 30 | reuseAddr: true 31 | 32 | sock.on 'message', @ipv4Message.bind @ 33 | 34 | sock.on 'listening', => 35 | @emit 'listening', sock, '' 36 | 37 | sock.on 'close', => 38 | @emit 'close' 39 | 40 | sock6 = dgram.createSocket 41 | type: 'udp6' 42 | reuseAddr: true 43 | 44 | sock6.on 'message', @ipv6Message.bind @ 45 | 46 | sock6.on 'listening', => 47 | @emit 'listening', sock6, 'v6' 48 | 49 | sock6.on 'close', => 50 | @emit 'close' 51 | 52 | @_sock6 = sock6 53 | @_sock = sock 54 | 55 | @_conf = config 56 | @_state = {} 57 | 58 | ipv4Message: (buf) -> 59 | try 60 | req = Protocol.parse(buf) 61 | catch e 62 | @emit 'error', e 63 | return 64 | 65 | @emit 'message', req 66 | 67 | if req.op != BOOTREQUEST 68 | @emit 'error', new Error('Malformed packet'), req 69 | return 70 | 71 | if not req.options[53] 72 | return @handleRequest req 73 | 74 | switch req.options[53] 75 | when DHCPDISCOVER 76 | @handleDiscover req 77 | when DHCPREQUEST 78 | @handleRequest req 79 | 80 | ipv6Message: (buf, rinfo) -> 81 | try 82 | req = Protocol.parseIpv6(buf) 83 | catch e 84 | @emit 'error', e 85 | return 86 | 87 | @emit 'message', req 88 | 89 | @emit DHCPV6.MESSAGETYPE[req.op], req, rinfo 90 | 91 | config: (key) -> 92 | optId = Options.conf[key] 93 | 94 | if undefined != @_conf[key] 95 | val = @_conf[key] 96 | else if undefined != Options.opts[optId] 97 | val = Options.opts[optId].default 98 | 99 | if val == undefined 100 | return 0 101 | else 102 | throw new Error 'Invalid option ' + key 103 | 104 | if val instanceof Function 105 | val = val.call @ 106 | 107 | values = Options.opts[optId]?.enum 108 | 109 | if key not in [ 'range', 'static', 'randomIP' ] and values 110 | for i, v of values when v is val 111 | return parseInt i, 10 112 | 113 | if values[val] is undefined 114 | throw new Error 'Provided enum value for ' + key + ' is not valid' 115 | else 116 | val = parseInt val, 10 117 | 118 | val 119 | 120 | _getOptions: (pre, required, requested) -> 121 | for req in required 122 | if Options.opts[req] != undefined 123 | pre[req] ?= @config(Options.opts[req].config) 124 | 125 | if not pre[req] 126 | throw new Error "Required option #{ Options.opts[req].config } does not have a value set" 127 | else 128 | @emit 'error', 'Unknown option ' + req 129 | 130 | if requested 131 | for req in requested 132 | if Options.opts[req] isnt undefined and pre[req] is undefined 133 | val = @config(Options.opts[req].config) 134 | 135 | if val 136 | pre[req] = val 137 | else 138 | @emit 'error', 'Unknown option ' + req 139 | 140 | forceOptions = @_conf.forceOptions 141 | 142 | if Array.isArray forceOptions 143 | 144 | for option in forceOptions 145 | if isNaN option 146 | id = Options.conf[option] 147 | else 148 | id = option 149 | 150 | if id? and pre[id] is undefined 151 | pre[id] = @config(option) 152 | 153 | pre 154 | 155 | _selectAddress: (clientMAC, req) -> 156 | if @_state[clientMAC]?.address 157 | return @_state[clientMAC].address 158 | 159 | _static = @config 'static' 160 | 161 | if typeof _static is 'function' 162 | staticResult = _static clientMAC, req 163 | 164 | if staticResult 165 | return staticResult 166 | else if _static[clientMAC] 167 | return _static[clientMAC] 168 | 169 | randIP = @config 'randomIP' 170 | _tmp = @config 'range' 171 | 172 | firstIP = Tools.parseIp _tmp[0] 173 | lastIP = Tools.parseIp _tmp[1] 174 | 175 | ips = [ @config('server') ] 176 | 177 | oldestMac = null 178 | oldestTime = Infinity 179 | 180 | leases = 0 181 | 182 | for mac, v of @_state 183 | if v.address 184 | ips.push v.address 185 | 186 | if v.leaseTime < oldestTime 187 | oldestTime = v.leaseTime 188 | oldestMac = mac 189 | 190 | leases++ 191 | 192 | if oldestMac and lastIP - firstIP == leases 193 | ip = @_state[oldestMac].address 194 | 195 | delete @_state[oldestMac] 196 | 197 | return ip 198 | 199 | if randIP 200 | loop 201 | ip = Tools.formatIp(firstIP + Math.random() * (lastIP - firstIP) | 0) 202 | 203 | if ips.indexOf(ip) == -1 204 | return ip 205 | 206 | i = firstIP 207 | 208 | while i <= lastIP 209 | ip = Tools.formatIp i 210 | 211 | if ip not in ips 212 | return ip 213 | 214 | i++ 215 | 216 | handleDiscover: (req) -> 217 | lease = @_state[req.chaddr] = @_state[req.chaddr] or new Lease 218 | lease.address = @_selectAddress(req.chaddr, req) 219 | lease.leasePeriod = @config 'leaseTime' 220 | lease.server = @config 'server' 221 | lease.state = 'OFFERED' 222 | 223 | console.log '>>> DISCOVER', JSON.stringify lease 224 | 225 | @sendOffer req 226 | 227 | sendOffer: (req) -> 228 | if req.options[97] and req.options[55].indexOf(97) == -1 229 | req.options[55].push 97 230 | 231 | if req.options[60] and req.options[60].indexOf('PXEClient') == 0 232 | [ 66, 67 ].forEach (opt) -> 233 | if req.options[55].indexOf(opt) is -1 234 | req.options[55].push opt 235 | 236 | ans = 237 | op: BOOTREPLY 238 | htype: 1 239 | hlen: 6 240 | hops: 0 241 | xid: req.xid 242 | secs: 0 243 | flags: req.flags 244 | ciaddr: INADDR_ANY 245 | yiaddr: @_selectAddress req.chaddr 246 | siaddr: @config 'server' 247 | giaddr: req.giaddr 248 | chaddr: req.chaddr 249 | sname: '' 250 | file: '' 251 | options: @_getOptions { 53: DHCPOFFER }, [ 252 | 1 253 | 3 254 | 51 255 | 54 256 | 6 257 | ], req.options[55] 258 | 259 | console.log '<<< OFFER', JSON.stringify ans 260 | 261 | @_send @config('broadcast'), ans 262 | 263 | handleRequest: (req) -> 264 | lease = @_state[req.chaddr] = @_state[req.chaddr] or new Lease 265 | lease.address = @_selectAddress req.chaddr 266 | lease.leasePeriod = @config 'leaseTime' 267 | lease.server = @config 'server' 268 | lease.state = 'BOUND' 269 | lease.bindTime = new Date 270 | lease.file = req.file 271 | 272 | console.log '>>> REQUEST', JSON.stringify lease 273 | 274 | @sendAck req 275 | 276 | sendAck: (req) -> 277 | if req.options[97] and req.options[55].indexOf(97) == -1 278 | req.options[55].push 97 279 | 280 | if req.options[60] and req.options[60].indexOf('PXEClient') == 0 281 | [ 66, 67 ].forEach (opt) -> 282 | if req.options[55].indexOf(opt) is -1 283 | req.options[55].push opt 284 | 285 | options = @_getOptions { 53: DHCPACK }, [ 286 | 1 287 | 3 288 | 51 289 | 54 290 | 6 291 | ], req.options[55] 292 | 293 | ans = 294 | op: BOOTREPLY 295 | htype: 1 296 | hlen: 6 297 | hops: 0 298 | xid: req.xid 299 | secs: 0 300 | flags: req.flags 301 | ciaddr: req.ciaddr 302 | yiaddr: @_selectAddress req.chaddr 303 | siaddr: @config 'server' 304 | giaddr: req.giaddr 305 | chaddr: req.chaddr 306 | sname: '' 307 | file: req.file 308 | options: options 309 | 310 | @_send @config('broadcast'), ans, => 311 | @emit 'bound', @_state, ans 312 | 313 | sendNak: (req) -> 314 | 315 | ans = 316 | op: BOOTREPLY 317 | htype: 1 318 | hlen: 6 319 | hops: 0 320 | xid: req.xid 321 | secs: 0 322 | flags: req.flags 323 | ciaddr: INADDR_ANY 324 | yiaddr: INADDR_ANY 325 | siaddr: INADDR_ANY 326 | giaddr: req.giaddr 327 | chaddr: req.chaddr 328 | sname: '' 329 | file: '' 330 | options: @_getOptions { 53: DHCPNAK }, [ 54 ] 331 | 332 | console.log '<<< NAK', JSON.stringify ans 333 | 334 | @_send @config('broadcast'), ans 335 | 336 | handleRelease: -> 337 | 338 | handleRenew: -> 339 | return 340 | 341 | listen: (port, host, fn = ->) -> 342 | ip = Ips().find ({ family }) -> family is 'IPv6' 343 | 344 | connacks = Number not not ip 345 | 346 | onConnect = -> 347 | connacks++ 348 | 349 | if connacks is 2 350 | process.nextTick fn 351 | 352 | @_sock.bind port or SERVER_PORT, host or INADDR_ANY, => 353 | @_sock.setBroadcast true 354 | 355 | onConnect() 356 | 357 | if ip 358 | @_sock6.bind 547, '::', => 359 | @_sock6.setBroadcast true 360 | 361 | try 362 | @_sock6.addMembership 'ff02::1', ip.address 363 | catch e 364 | 365 | onConnect() 366 | 367 | @ 368 | 369 | close: (callback) -> 370 | connacks = 0 371 | 372 | onClose = -> 373 | connacks++ 374 | 375 | if connacks is 2 376 | callback() 377 | 378 | @_sock.close onClose 379 | @_sock6.close onClose 380 | 381 | _send: (host, data, cb = ->) -> 382 | sb = Protocol.format data 383 | 384 | @_sock.send sb._data, 0, sb._w, CLIENT_PORT, host, (err, bytes) -> 385 | if err 386 | console.log err 387 | 388 | cb err, bytes 389 | 390 | module.exports = Server 391 | -------------------------------------------------------------------------------- /src/dhcp/tools.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | parseIp: (str) -> 4 | octs = str.split('.') 5 | 6 | if octs.length != 4 7 | throw new Error('Invalid IP address ' + str) 8 | 9 | octs.reduce (prev, val) -> 10 | val = parseInt(val, 10) 11 | 12 | if 0 <= val and val < 256 13 | return prev << 8 | val 14 | else 15 | throw new Error('Invalid IP address ' + str) 16 | 17 | return 18 | , 0 19 | 20 | formatIp: (num) -> 21 | ip = '' 22 | i = 24 23 | 24 | while i >= 0 25 | if ip 26 | ip += '.' 27 | 28 | ip += (num >>> i & 0xFF).toString(10) 29 | i -= 8 30 | 31 | ip 32 | 33 | netmaskFromCIDR: (cidr) -> 34 | -1 << 32 - cidr 35 | 36 | netmaskFromIP: (ip) -> 37 | if typeof ip == 'string' 38 | ip = @parseIp(ip) 39 | 40 | first = ip >>> 24 41 | 42 | if first <= 127 43 | 0xff000000 44 | else if first >= 192 45 | 0xffffff00 46 | else 47 | 0xffff0000 48 | 49 | wildcardFromCIDR: (cidr) -> 50 | ~@netmaskFromCIDR(cidr) 51 | 52 | networkFromIpCIDR: (ip, cidr) -> 53 | if typeof ip == 'string' 54 | ip = @parseIp(ip) 55 | 56 | @netmaskFromCIDR(cidr) & ip 57 | 58 | broadcastFromIpCIDR: (ip, cidr) -> 59 | if typeof ip == 'string' 60 | ip = @parseIp(ip) 61 | 62 | @networkFromIpCIDR(ip, cidr) | @wildcardFromCIDR(cidr) 63 | 64 | CIDRFromNetmask: (net) -> 65 | if typeof net == 'string' 66 | net = @parseIp(net) 67 | 68 | s = 0 69 | d = 0 70 | 71 | t = net & 1 72 | wild = t 73 | i = 0 74 | 75 | while i < 32 76 | d += t ^ net & 1 77 | t = net & 1 78 | 79 | net >>>= 1 80 | 81 | s += t 82 | i++ 83 | 84 | if d != 1 85 | throw new Error('Invalid Netmask ' + net) 86 | 87 | if wild 88 | s = 32 - s 89 | 90 | s 91 | 92 | gatewayFromIpCIDR: (ip, cidr) -> 93 | if typeof ip == 'string' 94 | ip = @parseIp(ip) 95 | 96 | if cidr == 32 97 | return ip 98 | 99 | @networkFromIpCIDR(ip, cidr) + 1 100 | 101 | netmaskFromRange: (ip1, ip2) -> 102 | if typeof ip1 == 'string' 103 | ip1 = @parseIp(ip1) 104 | 105 | if typeof ip2 == 'string' 106 | ip2 = @parseIp(ip2) 107 | 108 | cidr = 32 - Math.floor(Math.log2((ip1 ^ ip2 - 1) + 2)) - 1 109 | 110 | @netmaskFromCIDR cidr 111 | -------------------------------------------------------------------------------- /src/dns.coffee: -------------------------------------------------------------------------------- 1 | { EventEmitter } = require'events' 2 | { createSocket } = require('dgram') 3 | { isIPv6 } = require('net') 4 | 5 | dns = require('dns') 6 | 7 | bitSlice = (b, offset, length) -> 8 | b >>> 7 - (offset + length - 1) & ~(0xff << length) 9 | 10 | bufferify = (ip) -> 11 | if isIPv6 ip 12 | bufferifyV6 ip 13 | else bufferifyV4 ip 14 | 15 | bufferifyV4 = (ip) -> 16 | ip = ip.split('.').map (n) -> parseInt n, 10 17 | 18 | result = 0 19 | base = 1 20 | 21 | i = ip.length - 1 22 | 23 | while i >= 0 24 | result += ip[i] * base 25 | base *= 256 26 | i-- 27 | 28 | buf = Buffer.alloc(4) 29 | buf.writeUInt32BE result 30 | buf 31 | 32 | bufferifyV6 = (rawIp) -> 33 | 34 | countColons = (x) -> 35 | n = 0 36 | x.replace /:/g, (c) -> n++ 37 | n 38 | 39 | ip = rawIp.replace(/\/\d{1,3}(?=%|$)/, '').replace(/%.*$/, '') 40 | 41 | hexIp = ip 42 | .replace /::/, (two) -> ':' + Array(7 - countColons(ip) + 1).join(':') + ':' 43 | .split ':' 44 | .map (x) -> Array(4 - (x.length)).fill('0').join('') + x 45 | .join('') 46 | 47 | Buffer.from hexIp, 'hex' 48 | 49 | domainify = (qname) -> 50 | parts = [] 51 | 52 | i = 0 53 | 54 | while i < qname.length and qname[i] 55 | length = qname[i] 56 | offset = i + 1 57 | parts.push qname.slice(offset, offset + length).toString() 58 | 59 | i = offset + length 60 | 61 | parts.join '.' 62 | 63 | qnameify = (domain) -> 64 | qname = Buffer.alloc(domain.length + 2) 65 | offset = 0 66 | 67 | domain = domain.split('.') 68 | 69 | i = 0 70 | 71 | while i < domain.length 72 | qname[offset] = domain[i].length 73 | qname.write domain[i], offset + 1, domain[i].length, 'ascii' 74 | 75 | offset += qname[offset] + 1 76 | i++ 77 | 78 | qname[qname.length - 1] = 0 79 | qname 80 | 81 | functionify = (val) -> 82 | (addr, callback) -> 83 | callback null, val 84 | 85 | parse = (buf) -> 86 | header = {} 87 | question = {} 88 | 89 | b = buf.slice(2, 3).toString('binary', 0, 1).charCodeAt(0) 90 | 91 | header.id = buf.slice(0, 2) 92 | header.qr = bitSlice(b, 0, 1) 93 | header.opcode = bitSlice(b, 1, 4) 94 | header.aa = bitSlice(b, 5, 1) 95 | header.tc = bitSlice(b, 6, 1) 96 | header.rd = bitSlice(b, 7, 1) 97 | 98 | b = buf.slice(3, 4).toString('binary', 0, 1).charCodeAt(0) 99 | 100 | header.ra = bitSlice(b, 0, 1) 101 | header.z = bitSlice(b, 1, 3) 102 | header.rcode = bitSlice(b, 4, 4) 103 | header.qdcount = buf.slice(4, 6) 104 | header.ancount = buf.slice(6, 8) 105 | header.nscount = buf.slice(8, 10) 106 | header.arcount = buf.slice(10, 12) 107 | 108 | question.qname = buf.slice(12, buf.length - 4) 109 | question.qtype = buf.slice(buf.length - 4, buf.length - 2) 110 | question.qclass = buf.slice(buf.length - 2, buf.length) 111 | 112 | { header, question } 113 | 114 | responseBuffer = (query) -> 115 | question = query.question 116 | header = query.header 117 | qname = question.qname 118 | 119 | offset = 16 + qname.length 120 | 121 | length = offset 122 | i = 0 123 | 124 | while i < query.rr.length 125 | length += query.rr[i].qname.length + 10 126 | i++ 127 | 128 | buf = Buffer.alloc(length) 129 | 130 | header.id.copy buf, 0, 0, 2 131 | 132 | buf[2] = 0x00 | header.qr << 7 | header.opcode << 3 | header.aa << 2 | header.tc << 1 | header.rd 133 | buf[3] = 0x00 | header.ra << 7 | header.z << 4 | header.rcode 134 | 135 | buf.writeUInt16BE header.qdcount, 4 136 | buf.writeUInt16BE header.ancount, 6 137 | buf.writeUInt16BE header.nscount, 8 138 | buf.writeUInt16BE header.arcount, 10 139 | 140 | qname.copy buf, 12 141 | 142 | question.qtype.copy buf, 12 + qname.length, question.qtype, 2 143 | question.qclass.copy buf, 12 + qname.length + 2, question.qclass, 2 144 | 145 | i = 0 146 | 147 | while i < query.rr.length 148 | rr = query.rr[i] 149 | rr.qname.copy buf, offset 150 | offset += rr.qname.length 151 | buf.writeUInt16BE rr.qtype, offset 152 | buf.writeUInt16BE rr.qclass, offset + 2 153 | buf.writeUInt32BE rr.ttl, offset + 4 154 | buf.writeUInt16BE rr.rdlength, offset + 8 155 | buf = Buffer.concat([ 156 | buf 157 | rr.rdata 158 | ]) 159 | offset += 14 160 | i++ 161 | buf 162 | 163 | response = (query, ttl, to) -> 164 | response = {} 165 | 166 | header = response.header = {} 167 | question = response.question = {} 168 | rrs = resolve(query.question.qname, ttl, to) 169 | 170 | header.id = query.header.id 171 | header.ancount = rrs.length 172 | header.qr = 1 173 | header.opcode = 0 174 | header.aa = 0 175 | header.tc = 0 176 | header.rd = 1 177 | header.ra = 0 178 | header.z = 0 179 | header.rcode = 0 180 | header.qdcount = 1 181 | header.nscount = 0 182 | header.arcount = 0 183 | 184 | question.qname = query.question.qname 185 | question.qtype = query.question.qtype 186 | question.qclass = query.question.qclass 187 | 188 | response.rr = rrs 189 | 190 | responseBuffer response 191 | 192 | resolve = (qname, ttl, to) -> 193 | r = {} 194 | r.qname = qname 195 | r.qtype = if to.length == 4 then 1 else 28 196 | 197 | r.qclass = 1 198 | r.ttl = ttl 199 | r.rdlength = to.length 200 | r.rdata = to 201 | 202 | [ r ] 203 | 204 | lookup = (addr, callback) -> 205 | if net.isIP(addr) 206 | return callback(null, addr) 207 | 208 | dns.lookup addr, callback 209 | 210 | class Server extends EventEmitter 211 | constructor: (proxy = '8.8.8.8') -> 212 | super 213 | 214 | @_socket = createSocket if isIPv6(proxy) then 'udp6' else 'udp4' 215 | 216 | routes = [] 217 | 218 | @_socket.on 'message', (message, rinfo) => 219 | query = parse message 220 | domain = domainify query.question.qname 221 | 222 | routeData = { domain, rinfo } 223 | 224 | @emit 'resolve', routeData 225 | 226 | respond = (buf) => 227 | @_socket.send buf, 0, buf.length, rinfo.port, rinfo.address 228 | 229 | onerror = (err) => 230 | @emit 'error', err 231 | 232 | onproxy = -> 233 | sock = createSocket if isIPv6(proxy) then 'udp6' else 'udp4' 234 | sock.send message, 0, message.length, 53, proxy 235 | 236 | sock.on 'error', onerror 237 | 238 | sock.on 'message', (response) -> 239 | respond response 240 | sock.close() 241 | 242 | i = 0 243 | 244 | while i < routes.length 245 | if routes[i].pattern.test(domain) 246 | route = routes[i].route 247 | break 248 | i++ 249 | 250 | if not route 251 | return onproxy() 252 | 253 | route routeData, (err, to) => 254 | if typeof to == 'string' 255 | toIp = to 256 | ttl = 1 257 | else 258 | toIp = to.ip 259 | ttl = to.ttl 260 | 261 | if err 262 | return onerror(err) 263 | 264 | if !toIp 265 | return onproxy() 266 | 267 | lookup toIp, (err, addr) => 268 | if err 269 | return onerror(err) 270 | 271 | @emit 'route', domain, addr 272 | 273 | respond response(query, ttl, bufferify(addr)) 274 | 275 | route: (pattern, route) -> 276 | if Array.isArray pattern 277 | pattern.forEach (item) => 278 | @route item, route 279 | 280 | return @ 281 | 282 | if typeof pattern == 'function' 283 | return @route('*', pattern) 284 | 285 | if typeof route == 'string' 286 | return @route(pattern, functionify(route)) 287 | 288 | pattern = if pattern is '*' 289 | /.?/ 290 | else 291 | new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*\\\./g, '(.+)\\.') + '$', 'i') 292 | 293 | routes.push { pattern, route } 294 | 295 | @ 296 | 297 | listen: (port) -> 298 | @_socket.bind port or 53 299 | 300 | @ 301 | 302 | close: (callback) -> 303 | @_socket.close callback 304 | 305 | @ 306 | -------------------------------------------------------------------------------- /src/get-port.coffee: -------------------------------------------------------------------------------- 1 | { createServer } = require 'net' 2 | 3 | args = require './args' 4 | 5 | module.exports = -> 6 | 7 | new Promise (resolve, reject) -> 8 | if args.port 9 | return resolve args.port 10 | 11 | server = createServer() 12 | server.unref() 13 | 14 | server.on 'error', reject 15 | 16 | server.listen 0, -> 17 | port = server.address().port 18 | 19 | server.close -> 20 | resolve port 21 | -------------------------------------------------------------------------------- /src/http/cwmp/index.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | { parse, methods, createSoapEnv, fileTypes } = require './xml' 4 | 5 | file = require '../file' 6 | args = require '../../args' 7 | 8 | stage = null 9 | device = {} 10 | env = [] 11 | finished = false 12 | 13 | set = (obj, key, value) -> 14 | attrs = key.split '.' 15 | for attr, i in attrs 16 | if i is attrs.length - 1 17 | obj[attr] = value 18 | else 19 | obj = obj[attr] ?= {} 20 | obj 21 | 22 | request = (url, req, res) -> 23 | if req.body.length > 0 24 | console.log '>>> REQUEST' 25 | console.dir [ req.headers, req.body ] 26 | 27 | xml = parse req.body 28 | 29 | element = xml['soapenv:Envelope'] 30 | body = element['soapenv:Body'] 31 | header = element['soapenv:Header'] 32 | 33 | for k, v of element.attributes 34 | return unless k? and v? 35 | 36 | str = k.replace('soapenv', 'soap-env') + '=\'' + v + '\'' 37 | 38 | if env.indexOf(str) is -1 39 | env.push str 40 | 41 | res.name = stage = Object.keys(body)[0] 42 | 43 | cwmp = element.attributes?['xmlns:cwmp'] 44 | 45 | [ input, cwmpVersion ] = /urn:dslforum-org:cwmp-(\d+-\d+)/.exec(cwmp) or [ cwmp, '1-2' ] 46 | 47 | res.cwmpVersion = cwmpVersion.replace /-/g, '.' 48 | 49 | idElement = header['cwmp:ID'] 50 | 51 | if idElement 52 | res.id = req.id = idElement 53 | 54 | for key, value of body[stage] 55 | res[key] = value 56 | 57 | if res.ParameterList?.ParameterValueStruct? 58 | params = res.ParameterList.ParameterValueStruct 59 | 60 | res.params = Object.keys(params).reduce (obj, k) -> 61 | set(obj, k, params[k]) if typeof params[k] is 'string' 62 | obj 63 | , {} 64 | 65 | device = res.params.Device or res.params.InternetGatewayDevice or {} 66 | 67 | res.name += 'Response' 68 | else if stage is 'cwmp:Inform' 69 | console.log '>>> EMPTY REQUEST' 70 | console.dir [ req.headers, req.body ] 71 | 72 | res.name = 'cwmp:Download' 73 | 74 | if args.fileType and fileTypes[args.fileType]? 75 | res.fileType = fileTypes[args.fileType] 76 | else 77 | res.fileType = switch file.ext 78 | when '.rbi' then '1 Firmware Upgrade Image' 79 | when '.sts' then '3 Vendor Configuration File' 80 | 81 | res.fileSize = file.data.length 82 | res.url = "#{ url }#{file.name}" 83 | 84 | res.env = env.join ' ' 85 | 86 | response res 87 | 88 | response = (res) -> 89 | headers = 90 | 'Content-Type' : 'text/xml; charset="utf-8"' 91 | 'Server' : 'ACSServer' 92 | 'SOAPServer' : 'ACSServer' 93 | 94 | code = 404 95 | data = null 96 | 97 | if res.name and methods[res.name]? 98 | res.id ?= '1690d26c77f0000' 99 | 100 | data = createSoapEnv res, headers 101 | code = 200 102 | 103 | headers['Content-Length'] = data.length 104 | 105 | if res.name is 'cwmp:TransferCompleteResponse' 106 | finished = true 107 | 108 | else if finished 109 | code = 204 110 | 111 | headers['Connection'] = "close" 112 | headers['Content-Length'] = 0 113 | 114 | console.log '<<< EMPTY RESPONSE' 115 | 116 | console.dir [ code, headers, data ] 117 | 118 | res.writeHead code, headers 119 | res.end data 120 | 121 | return 122 | 123 | module.exports = (url) -> 124 | (req, res) -> 125 | COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g 126 | 127 | while match = COOKIE_REGEX.exec(req.headers.cookie) 128 | res.id = match[2] if match[1] is 'session' 129 | 130 | req.body = '' 131 | 132 | req.on 'data', (chunk) -> 133 | req.body += chunk 134 | 135 | req.on 'end', -> 136 | request url, req, res 137 | 138 | return 139 | -------------------------------------------------------------------------------- /src/http/cwmp/xml.coffee: -------------------------------------------------------------------------------- 1 | 2 | exports.parse = (xml) -> 3 | 4 | declaration = -> 5 | m = match /^<\?xml\s*/ 6 | 7 | return unless m 8 | 9 | node = {} 10 | 11 | while !(eos() or has('?>')) 12 | attr = attribute() 13 | 14 | if not attr 15 | return node 16 | 17 | node.attributes ?= {} 18 | node.attributes[attr.name] = attr.value 19 | 20 | match /\?>\s*/ 21 | 22 | node 23 | 24 | tag = -> 25 | m = match /^<([\w-:.]+)\s*/ 26 | 27 | return unless m 28 | 29 | node = {} 30 | 31 | while !(eos() or has('>') or has('?>') or has('/>')) 32 | attr = attribute() 33 | 34 | if !attr 35 | return [ m[1], node ] 36 | 37 | node.attributes ?= {} 38 | node.attributes[attr.name] = attr.value 39 | 40 | if match(/^\s*\/>\s*/) 41 | return [ m[1], node ] 42 | 43 | match /\??>\s*/ 44 | 45 | c = content() 46 | 47 | if c 48 | node = c 49 | 50 | while child = tag() 51 | if child[1].Name and child[1].Value 52 | node[child[0]] ?= {} 53 | node[child[0]][child[1].Name] = child[1].Value 54 | else 55 | node[child[0]] = child[1] 56 | 57 | match /^<\/[\w-:.]+>\s*/ 58 | 59 | [ m[1], node ] 60 | 61 | content = -> 62 | m = match(/^([^<]*)/) 63 | m?[1] 64 | 65 | attribute = -> 66 | m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/) 67 | 68 | return unless m 69 | 70 | name: m[1] 71 | value: m[2].replace /^['"]|['"]$/g, '' 72 | 73 | match = (re) -> 74 | m = xml.match(re) 75 | 76 | return unless m 77 | 78 | xml = xml.slice m[0].length 79 | 80 | m 81 | 82 | eos = -> 83 | not xml.length 84 | 85 | has = (prefix) -> 86 | 0 == xml.indexOf(prefix) 87 | 88 | xml = xml.trim() 89 | xml = xml.replace(//g, '') 90 | 91 | [ name, obj ] = tag() 92 | 93 | "#{name}": obj 94 | 95 | exports.methods = methods = 96 | 97 | 'cwmp:TransferCompleteResponse': (res) -> 98 | i = 0 99 | 100 | [ h, w ] = process.stdout.getWindowSize() 101 | 102 | while i++ < w 103 | console.log '\u000d\n' 104 | 105 | console.log "\n*** PRESS AND HOLD THE WPS BUTTON ON YOUR GATEWAY ***\n" 106 | console.log "### WAITING FOR WPS CALLBACK" 107 | 108 | """""" 109 | 110 | 'cwmp:Download': (res) -> 111 | """ 112 | #{ res.commandKey or res.id } 113 | #{ res.fileType } 114 | #{ res.url } 115 | #{ res.fileSize or 0 } 116 | 0 117 | """ 118 | 119 | 'cwmp:InformResponse': (res, headers) -> 120 | headers['Set-Cookie'] = "session=7b0fa33078153e5c" 121 | 122 | """ 123 | 1 124 | """ 125 | 126 | exports.createSoapEnv = (res, headers) -> 127 | """ 128 | 129 | 130 | #{ res.id } 131 | 132 | 133 | #{ methods[res.name] res, headers } 134 | 135 | """ 136 | 137 | exports.fileTypes = 138 | 1: '1 Firmware Upgrade Image' 139 | 2: '2 Web Content' 140 | 3: '3 Vendor Configuration File' 141 | 4: '4 Tone File' 142 | 5: '5 Ringer File' 143 | -------------------------------------------------------------------------------- /src/http/file.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | name: 'file.sts' 4 | data: Buffer.from '7265626f6f74206f66660a73657420627574746f6e2e7770732e68616e646c65723d22736564202d69202773232f726f6f743a2e2a24232f726f6f743a2f62696e2f6173682327202f6574632f706173737764202626206563686f20726f6f743a726f6f74207c20636870617373776420262620736564202d69202d652027732f232f2f27202d652027732361736b636f6e736f6c653a2e2a5c242361736b636f6e736f6c653a2f62696e2f6173682327202f6574632f696e69747461622026262028756369202d712064656c6574652064726f70626561722e616667207c7c20747275652920262620756369206164642064726f70626561722064726f7062656172202626207563692072656e616d652064726f70626561722e4064726f70626561725b2d315d3d61666720262620756369207365742064726f70626561722e6166672e656e61626c653d27312720262620756369207365742064726f70626561722e6166672e496e746572666163653d276c616e2720262620756369207365742064726f70626561722e6166672e506f72743d2732322720262620756369207365742064726f70626561722e6166672e49646c6554696d656f75743d273630302720262620756369207365742064726f70626561722e6166672e50617373776f7264417574683d276f6e2720262620756369207365742064726f70626561722e6166672e526f6f7450617373776f7264417574683d276f6e2720262620756369207365742064726f70626561722e6166672e526f6f744c6f67696e3d2731272026262028756369207365742064726f70626561722e6c616e2e656e61626c653d273027207c7c2074727565292026262075636920636f6d6d69742064726f7062656172202626202f6574632f696e69742e642f64726f706265617220656e61626c65202626202f6574632f696e69742e642f64726f706265617220726573746172742026262028756369202d71207365742024287563692073686f77206669726577616c6c207c2067726570202d6d2031202428667733202d71207072696e74207c206567726570202769707461626c6573202d742066696c746572202d41207a6f6e655f6c616e5f696e707574202d7020746370202d6d20746370202d2d64706f7274203232202d6d20636f6d6d656e74202d2d636f6d6d656e74205c22216677333a202e2b5c22202d6a2044524f5027207c20736564202d6e202d652027732f5e69707461626c65732e5c2b6677333a205c282e5c2b5c295c222e5c2b2f5c312f702729207c20736564202d6e202d65205c22732f5c282e5c2b5c292e6e616d653d272e5c2b27242f5c312f705c22292e7461726765743d2741434345505427207c7c2074727565292026262075636920636f6d6d6974206669726577616c6c202626202f6574632f696e69742e642f6669726577616c6c2072656c6f6164202626207563692073657420627574746f6e2e7770732e68616e646c65723d277770735f627574746f6e5f707265737365642e7368272026262075636920636f6d6d69742026262077676574207b7b75726c7d7d646f6e65207c7c207472756522', 'hex' 5 | -------------------------------------------------------------------------------- /src/http/index.coffee: -------------------------------------------------------------------------------- 1 | { Duplex } = require 'stream' 2 | { createServer } = require 'http' 3 | { readFileSync, existsSync, statSync } = require 'fs' 4 | 5 | path = require 'path' 6 | 7 | file = require './file' 8 | route = require './router' 9 | args = require '../args' 10 | cwmp = require './cwmp' 11 | 12 | module.exports = (ip, port, url) -> 13 | 14 | if args.file 15 | file.name = path.basename args.file 16 | 17 | try 18 | file.data = readFileSync args.file 19 | catch e 20 | throw e 21 | 22 | file.data = Buffer.from file.data 23 | .toString 'utf8' 24 | .replace '{{url}}', url 25 | , 'utf8' 26 | 27 | file.ext = path.extname file.name 28 | 29 | route 30 | .get "/#{file.name}", (req, res) -> 31 | ext = file.ext.toUpperCase() 32 | 33 | console.log ">>> #{ ext } REQUEST" 34 | 35 | headers = 36 | 'Content-Type': 'text/plain' 37 | 'Content-Length': file.data.length 38 | 39 | console.log '>>> #{ ext } RESPONSE' 40 | console.dir [headers, file.data.toString('utf8')] 41 | 42 | res.writeHead 200, headers 43 | 44 | stream = new Duplex() 45 | stream.push file.data 46 | stream.push null 47 | stream.pipe res 48 | .get '/done', (req, res) -> 49 | console.log '>>> WPS CALLBACK' 50 | console.log """\n 51 | All done, 52 | 53 | - change network card settings back to dhcp and move the cable back to a lan port 54 | - try ssh connection to the gateways ip (usually 192.168.0.1) with username root and password root (change password immediately with passwd!) 55 | 56 | ssh root@192.168.0.1""" 57 | 58 | setTimeout -> 59 | process.exit 1 60 | , 20000 61 | 62 | res.writeHead 200 63 | res.end() 64 | 65 | .post '/', cwmp(url) 66 | 67 | srv = createServer route 68 | srv.keepAliveTimeout = 30000 69 | 70 | srv.on 'error', (e) -> 71 | if e.code in [ 'EADDRINUSE', 'EADDRNOTAVAIL' ] 72 | console.log e.code + ', retrying...' 73 | 74 | setTimeout -> 75 | srv.close() 76 | srv.listen port 77 | , 1000 78 | else console.error e 79 | 80 | srv.listen port 81 | 82 | srv 83 | -------------------------------------------------------------------------------- /src/http/router.coffee: -------------------------------------------------------------------------------- 1 | param = (val) -> 2 | (map) -> map[val] 3 | 4 | str = (val) -> 5 | -> val 6 | 7 | formatter = (format) -> 8 | if !format 9 | return null 10 | 11 | format = format.replace(/\{\*\}/g, '*').replace(/\*/g, '{*}').replace(/:(\w+)/g, '{$1}') 12 | 13 | format = format.match(/(?:[^\{]+)|(?:{[^\}]+\})/g).map (item) -> 14 | if item[0] != '{' then str(item) else param(item.substring(1, item.length - 1)) 15 | 16 | (params) -> 17 | format.reduce (result, item) -> 18 | result + item(params) 19 | , '' 20 | 21 | decode = (str) -> 22 | try 23 | decodeURIComponent(str) 24 | catch err 25 | str 26 | 27 | matcher = (pattern) -> 28 | if typeof pattern != 'string' 29 | return (url) -> 30 | url.match pattern 31 | 32 | keys = [] 33 | 34 | pattern = pattern.replace(/:(\w+)/g, '{$1}').replace('{*}', '*') 35 | 36 | pattern = pattern.replace /(\/)?(\.)?\{([^}]+)\}(?:\(([^)]*)\))?(\?)?/g, (match, slash, dot, key, capture, opt, offset) -> 37 | incl = (pattern[match.length + offset] or '/') == '/' 38 | keys.push key 39 | (if incl then '(?:' else '') + (slash or '') + (if incl then '' else '(?:') + (dot or '') + '(' + (capture or '[^/]+') + '))' + (opt or '') 40 | 41 | pattern = pattern.replace(/([\/.])/g, '\\$1').replace(/\*/g, '(.+)') 42 | pattern = new RegExp('^' + pattern + '[\\/]?$', 'i') 43 | 44 | (str) -> 45 | match = str.match(pattern) 46 | 47 | if !match 48 | return match 49 | 50 | map = {} 51 | 52 | match.slice(1).forEach (param, i) -> 53 | k = keys[i] = keys[i] or 'wildcard' 54 | param = param and decode(param) 55 | map[k] = if map[k] then [].concat(map[k]).concat(param) else param 56 | 57 | if map.wildcard 58 | map['*'] = map.wildcard 59 | 60 | map 61 | 62 | METHODS = [ 63 | 'get' 64 | 'post' 65 | 'put' 66 | 'del' 67 | 'delete' 68 | 'head' 69 | 'options' 70 | ] 71 | 72 | HTTP_METHODS = [ 73 | 'GET' 74 | 'POST' 75 | 'PUT' 76 | 'DELETE' 77 | 'DELETE' 78 | 'HEAD' 79 | 'OPTIONS' 80 | ] 81 | 82 | noop = -> 83 | 84 | error = (res) -> 85 | -> 86 | res.statusCode = 404 87 | res.end() 88 | return 89 | 90 | router = -> 91 | methods = {} 92 | traps = {} 93 | 94 | HTTP_METHODS.forEach (method) -> 95 | methods[method] = [] 96 | 97 | route = (req, res, next) -> 98 | method = methods[req.method] 99 | 100 | trap = traps[req.method] 101 | index = req.url.indexOf('?') 102 | 103 | url = if index == -1 then req.url else req.url.substr(0, index) 104 | 105 | i = 0 106 | 107 | next = next or error(res) 108 | 109 | if !method 110 | return next() 111 | 112 | lp = (err) -> 113 | if err 114 | return next(err) 115 | 116 | while i < method.length 117 | route = method[i] 118 | i++ 119 | 120 | req.params = route.pattern(url) 121 | 122 | if !req.params 123 | continue 124 | 125 | if route.rewrite 126 | req.url = url = route.rewrite(req.params) + (if index == -1 then '' else req.url.substr(index)) 127 | 128 | route.fn req, res, lp 129 | return 130 | 131 | if !trap 132 | return next() 133 | 134 | trap req, res, next 135 | 136 | return 137 | 138 | lp() 139 | 140 | return 141 | 142 | METHODS.forEach (method, i) -> 143 | 144 | route[method] = (pattern, rewrite, fn) -> 145 | if Array.isArray(pattern) 146 | pattern.forEach (item) -> 147 | route[method] item, rewrite, fn 148 | 149 | if !fn and !rewrite 150 | return route[method](null, null, pattern) 151 | 152 | if !fn and typeof rewrite == 'string' 153 | return route[method](pattern, rewrite, route) 154 | 155 | if !fn and typeof rewrite == 'function' 156 | return route[method](pattern, null, rewrite) 157 | 158 | if !fn 159 | return route 160 | 161 | (route.onmount or noop) pattern, rewrite, fn 162 | 163 | if !pattern 164 | traps[HTTP_METHODS[i]] = fn 165 | return route 166 | 167 | methods[HTTP_METHODS[i]].push 168 | pattern: matcher(pattern) 169 | rewrite: formatter(rewrite) 170 | fn: fn 171 | 172 | route 173 | 174 | return 175 | 176 | route.all = (pattern, rewrite, fn) -> 177 | METHODS.forEach (method) -> 178 | route[method] pattern, rewrite, fn 179 | 180 | route 181 | 182 | route 183 | 184 | module.exports = router() 185 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | pkg = require '../package.json' 2 | args = require './args' 3 | 4 | 5 | if args.tftp 6 | cable = "a LAN (yellow)" 7 | ip = args.ip or '192.168.0.254' 8 | else 9 | cable = "the WAN (red)" 10 | ip = args.ip or '58.162.0.1' 11 | 12 | console.log """ 13 | Technicolor OpenWRT Shell Unlocker v#{ pkg.version } By BoLaMN 14 | 15 | * Connect network cable from your computer to #{ cable } port of the modem 16 | * Change your computers network card to be a static ip address 17 | 18 | IPv4 Address: #{ ip } 19 | Subnet Mask: 255.255.255.0 20 | Default Gateway\\Router: #{ ip } 21 | 22 | """ 23 | 24 | ask = require './ask' 25 | dhcpd = require './dhcp' 26 | httpd = require './http' 27 | port = require './get-port' 28 | tftp = require './tftp' 29 | 30 | servers = [] 31 | 32 | if args.tftp 33 | servers.push tftp(args)... 34 | else if args.dhcponly 35 | servers.push dhcpd ip, args.acsurl, args.acspass 36 | else 37 | ask ip 38 | .then port 39 | .then (p) -> 40 | u = new URL args.acsurl or "http://#{ ip }" 41 | u.port = p 42 | 43 | url = u.toString() 44 | 45 | console.log "listening for cwmp requests at #{ url }" 46 | 47 | servers.push dhcpd ip, url, args.acspass 48 | servers.push httpd ip, p, url 49 | 50 | if process.platform is 'win32' 51 | rl = require('readline').createInterface 52 | input: process.stdin 53 | output: process.stdout 54 | 55 | rl.on 'SIGINT', -> 56 | process.emit 'SIGINT' 57 | 58 | process.on 'SIGINT', -> 59 | console.log "\nshutting down servers from SIGINT (Ctrl+C)" 60 | 61 | servers.forEach (server) -> 62 | server.close() 63 | 64 | setTimeout -> 65 | process.exit() 66 | , 2000 67 | -------------------------------------------------------------------------------- /src/ips.coffee: -------------------------------------------------------------------------------- 1 | { networkInterfaces } = require 'os' 2 | 3 | module.exports = -> 4 | addr = [] 5 | obj = {} 6 | 7 | for name, details of networkInterfaces() 8 | obj[name] ?= {} 9 | 10 | for { family, internal, address } in details when not internal 11 | if not address.startsWith '2001' 12 | obj[name][family] ?= [] 13 | obj[name][family].push { name, address, family } 14 | 15 | for k, v of obj when v.IPv4? 16 | for s, t of v 17 | addr.push t... 18 | 19 | addr 20 | -------------------------------------------------------------------------------- /src/ntp/client.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Packet = require './packet' 4 | 5 | { createSocket } = require 'dgram' 6 | { EventEmitter } = require 'events' 7 | 8 | class NTP extends EventEmitter 9 | 10 | constructor: (options, callback) -> 11 | if typeof options is 'function' 12 | callback = options 13 | options = {} 14 | 15 | Object.assign @, { 16 | server: '127.0.0.1' 17 | port: 123 18 | }, options 19 | 20 | @socket = new createSocket 'udp4' 21 | 22 | if typeof callback is 'function' 23 | @time callback 24 | 25 | time: (callback) -> 26 | { server, port, timeout } = @ 27 | 28 | packet = NTP.createPacket() 29 | 30 | @socket.send packet, 0, packet.length, port, server, (err) => 31 | if err 32 | return callback err 33 | 34 | @socket.once 'message', (data) => 35 | message = NTP.parse(data) 36 | 37 | callback err, message 38 | 39 | @ 40 | 41 | @time: (options, callback) -> 42 | new NTP(options, callback) 43 | 44 | @createPacket: -> 45 | packet = new Packet 46 | packet.mode = Packet.MODES.CLIENT 47 | 48 | packet.toBuffer() 49 | 50 | @parse: (buffer) -> 51 | message = Packet.parse(buffer) 52 | 53 | T1 = message.originateTimestamp 54 | T2 = message.receiveTimestamp 55 | T3 = message.transmitTimestamp 56 | T4 = message.destinationTimestamp 57 | 58 | message.d = T4 - T1 - (T3 - T2) 59 | message.t = (T2 - T1 + T3 - T4) / 2 60 | message 61 | 62 | module.exports = NTP 63 | -------------------------------------------------------------------------------- /src/ntp/index.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | { createServer } = require './server' 4 | 5 | ntp = createServer (message, response) -> 6 | console.log 'server message:', message 7 | 8 | response message 9 | 10 | ntp.listen 123, (err) -> 11 | console.log 'ntp server is running at %s', ntp.address().port 12 | 13 | module.exports = ntp 14 | -------------------------------------------------------------------------------- /src/ntp/packet.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | 3 | SEVENTY_YEARS = 2208988800 4 | 5 | toMsecs = (buffer, offset) -> 6 | seconds = 0 7 | fraction = 0 8 | 9 | i = 0 10 | 11 | while i < 4 12 | seconds = seconds * 256 + buffer[offset + i] 13 | ++i 14 | 15 | i = 4 16 | 17 | while i < 8 18 | fraction = fraction * 256 + buffer[offset + i] 19 | ++i 20 | 21 | (seconds - SEVENTY_YEARS + fraction / 2 ** 32) * 1000 22 | 23 | writeMsecs = (buffer, offset, ts) -> 24 | seconds = Math.floor(ts / 1000) + SEVENTY_YEARS - SEVENTY_YEARS 25 | fraction = Math.round(ts % 1000 / 1000 * 2 ** 32) 26 | 27 | buffer[offset + 0] = (seconds & 0xFF000000) >> 24 28 | buffer[offset + 1] = (seconds & 0x00FF0000) >> 16 29 | buffer[offset + 2] = (seconds & 0x0000FF00) >> 8 30 | buffer[offset + 3] = seconds & 0x000000FF 31 | 32 | buffer[offset + 4] = (fraction & 0xFF000000) >> 24 33 | buffer[offset + 5] = (fraction & 0x00FF0000) >> 16 34 | buffer[offset + 6] = (fraction & 0x0000FF00) >> 8 35 | buffer[offset + 7] = fraction & 0x000000FF 36 | 37 | buffer 38 | 39 | before = (val) -> 40 | value = parseInt(val.toString().split('.')[0], 10) 41 | 42 | if value then value else 0 43 | 44 | after = (val) -> 45 | value = parseInt(val.toString().split('.')[1], 10) 46 | 47 | if value then value else 0 48 | 49 | class Packet 50 | 51 | @MODES: 52 | CLIENT: 3 53 | SERVER: 4 54 | 55 | constructor: -> 56 | Object.assign @, 57 | leapIndicator: 0 58 | version: 4 59 | mode: 3 60 | stratum: 0 61 | pollInterval: 6 62 | precision: 236 63 | referenceIdentifier: 0 64 | referenceTimestamp: 0 65 | originateTimestamp: 0 66 | receiveTimestamp: 0 67 | transmitTimestamp: 0 68 | 69 | @parse: (buffer) -> 70 | assert.equal buffer.length, 48, 'Invalid Package' 71 | 72 | packet = new Packet 73 | 74 | packet.leapIndicator = buffer[0] >> 6 75 | packet.version = (buffer[0] & 0x38) >> 3 76 | packet.mode = buffer[0] & 0x7 77 | packet.stratum = buffer[1] 78 | packet.pollInterval = buffer[2] 79 | packet.precision = buffer[3] 80 | packet.rootDelay = buffer.slice(4, 8) 81 | packet.rootDispersion = buffer.slice(8, 12) 82 | packet.referenceIdentifier = buffer.slice(12, 16) 83 | 84 | packet.referenceTimestamp = toMsecs(buffer, 16) 85 | packet.originateTimestamp = toMsecs(buffer, 24) 86 | packet.receiveTimestamp = toMsecs(buffer, 32) 87 | packet.transmitTimestamp = toMsecs(buffer, 40) 88 | 89 | packet 90 | 91 | toBuffer: -> 92 | buffer = Buffer.alloc(48).fill(0x00) 93 | buffer[0] = 0 94 | 95 | # 0b11100011; // LI, Version, Mode 96 | 97 | buffer[0] += @leapIndicator << 6 98 | buffer[0] += @version << 3 99 | buffer[0] += @mode << 0 100 | buffer[1] = @stratum 101 | buffer[2] = @pollInterval 102 | buffer[3] = @precision 103 | 104 | buffer.writeUInt32BE @rootDelay, 4 105 | buffer.writeUInt32BE @rootDispersion, 8 106 | buffer.writeUInt32BE @referenceIdentifier, 12 107 | 108 | writeMsecs buffer, 16, @referenceTimestamp 109 | writeMsecs buffer, 24, @originateTimestamp 110 | writeMsecs buffer, 32, @receiveTimestamp 111 | writeMsecs buffer, 40, @transmitTimestamp 112 | 113 | buffer 114 | 115 | toJSON: -> 116 | output = Object.assign {}, @ 117 | 118 | output.version = @version 119 | 120 | output.leapIndicator = { 121 | 0: 'no-warning' 122 | 1: 'last-minute-61' 123 | 2: 'last-minute-59' 124 | 3: 'alarm' 125 | }[@leapIndicator] 126 | 127 | switch @mode 128 | when 1 129 | output.mode = 'symmetric-active' 130 | when 2 131 | output.mode = 'symmetric-passive' 132 | when 3 133 | output.mode = 'client' 134 | when 4 135 | output.mode = 'server' 136 | when 5 137 | output.mode = 'broadcast' 138 | when 0, 6, 7 139 | output.mode = 'reserved' 140 | 141 | if @stratum == 0 142 | output.stratum = 'death' 143 | else if @stratum == 1 144 | output.stratum = 'primary' 145 | else if @stratum <= 15 146 | output.stratum = 'secondary' 147 | else 148 | output.stratum = 'reserved' 149 | 150 | output.referenceTimestamp = new Date(@referenceTimestamp) 151 | output.originateTimestamp = new Date(@originateTimestamp) 152 | output.receiveTimestamp = new Date(@receiveTimestamp) 153 | output.transmitTimestamp = new Date(@transmitTimestamp) 154 | output.destinationTimestamp = new Date(@destinationTimestamp) 155 | 156 | output 157 | 158 | module.exports = Packet 159 | -------------------------------------------------------------------------------- /src/ntp/server.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | udp = require('dgram') 4 | EventEmitter = require('events') 5 | Packet = require('./packet') 6 | 7 | class NTPServer extends EventEmitter 8 | 9 | @createServer: (options) -> 10 | new NTPServer options 11 | 12 | constructor: (options, onRequest) -> 13 | super() 14 | 15 | if typeof options == 'function' 16 | onRequest = options 17 | options = {} 18 | 19 | Object.assign @, { port: 123 }, options 20 | 21 | @socket = udp.createSocket('udp4') 22 | @socket.on 'message', @parse.bind(@) 23 | 24 | if onRequest 25 | @on 'request', onRequest 26 | 27 | @ 28 | 29 | listen: (port, address) -> 30 | @socket.bind port or @port, address 31 | @ 32 | 33 | address: -> 34 | @socket.address() 35 | 36 | send: (rinfo, message, callback = ->) -> 37 | if message instanceof Packet 38 | message.mode = Packet.MODES.SERVER 39 | message = message.toBuffer() 40 | 41 | console.log 'response', message, 0, message.length, rinfo.port, rinfo.address 42 | @socket.send message, 0, message.length, rinfo.port, rinfo.address 43 | 44 | @socket.send message, rinfo.port, rinfo.server, callback 45 | @ 46 | 47 | parse: (message, rinfo) -> 48 | packet = Packet.parse(message) 49 | 50 | @send rinfo, packet, (err) -> 51 | if err then console.error err 52 | 53 | @ 54 | 55 | module.exports = NTPServer 56 | -------------------------------------------------------------------------------- /src/ntp/server2.coffee: -------------------------------------------------------------------------------- 1 | { EventEmitter } = require 'events' 2 | 3 | util = require 'util' 4 | dgram = require 'dgram' 5 | 6 | class TimeServer extends EventEmitter 7 | constructor: (time, error, version, mode, stratum, delay, dispersion) -> 8 | super 9 | 10 | @_socket = dgram.createSocket('udp4') 11 | 12 | #0=no error, 1=last minute of the day has 61 seconds, 2=last minute of the day has 59 seconds, 3=unknown 13 | ntp_server_error = ('0' + parseInt(error, 10).toString(2)).slice(-2) 14 | 15 | #integer showing NTP server version 16 | ntp_server_version = ('00' + parseInt(version, 10).toString(2)).slice(-3) 17 | 18 | #0=reserved, 1=symmetric active, 2=symmetric passice, 3=client, 4=server, 5=broadcast, 6=NTP control message, 7=reserved for private use 19 | ntp_server_mode = ('00' + parseInt(mode, 10).toString(2)).slice(-3) 20 | 21 | #0=unspecified, 1=primary, 2-15=secondary, 16=unsychronized 22 | ntp_peer_clock_stratum = '1' 23 | ntp_peer_clock_precision = '128' 24 | 25 | #0.000 - 9.999 26 | ntp_root_delay = '0.9900' 27 | 28 | #0.000 - 9.999 29 | ntp_root_dispersion = '0.9900' 30 | 31 | #seconds since 1.1.1900, needs to be added for NTP to date generated by new Date() 32 | ntp_seconds_since_epoch = '2208988800' 33 | 34 | #provided reference ID 35 | ntp_reference_id = [ 78, 85, 76, 76 ] 36 | 37 | if time is '' 38 | createTime = 'recent' 39 | else 40 | createTime = (parseInt(new Date / 1000) - parseInt(time)).toString() 41 | 42 | @_socket.on 'message', (msg, rinfo) => 43 | @emit 'data', 'received message from ' + rinfo.address + ':' + rinfo.port 44 | 45 | if createTime == 'recent' 46 | timestamp = (new Date / 1000).toString() 47 | else 48 | timestamp = (parseInt(new Date / 1000) - parseInt(createTime)).toString() 49 | 50 | # set flags 51 | msg.writeUIntBE parseInt(ntp_server_error + ntp_server_version + ntp_server_mode, 2), 0, 1 52 | 53 | # set peer clock stratum 54 | msg.writeUIntBE parseInt(ntp_peer_clock_stratum, 10), 1, 1 55 | 56 | # set peer clock precision 57 | msg.writeUIntBE parseInt(ntp_peer_clock_precision, 10), 3, 1 58 | 59 | # set root delay seconds 60 | msg.writeUIntBE ntp_root_delay.before(), 4, 2 61 | 62 | # set root delay fraction 63 | msg.writeUIntBE 65535 / 10000 * ntp_root_delay.after(), 6, 2 64 | 65 | # set root dispersion seconds 66 | msg.writeUIntBE parseInt(ntp_root_dispersion.before(), 10), 8, 2 67 | 68 | # set root dispersion fraction 69 | msg.writeUIntBE 65535 / 10000 * ntp_root_dispersion.after(), 10, 2 70 | 71 | #set reference ID 72 | msg.writeUIntBE parseInt(ntp_reference_id[0], 10), 12, 1 73 | msg.writeUIntBE parseInt(ntp_reference_id[1], 10), 13, 1 74 | msg.writeUIntBE parseInt(ntp_reference_id[2], 10), 14, 1 75 | msg.writeUIntBE parseInt(ntp_reference_id[3], 10), 15, 1 76 | 77 | # set reference timestamp 78 | msg.writeUIntBE parseInt(ntp_seconds_since_epoch, 10) + timestamp.before(), 16, 4 79 | 80 | # set origin timestamp 81 | msg.writeUIntBE parseInt(ntp_seconds_since_epoch, 10) + timestamp.before(), 24, 4 82 | 83 | # set receive timestamp 84 | msg.writeUIntBE parseInt(ntp_seconds_since_epoch, 10) + timestamp.before(), 32, 4 85 | 86 | # set transmit timestamp 87 | msg.writeUIntBE parseInt(ntp_seconds_since_epoch, 10) + timestamp.before(), 40, 4 88 | 89 | @_socket.send msg, 0, msg.length, rinfo.port, rinfo.address, (err, bytes) => 90 | if err 91 | throw err 92 | 93 | @emit 'data', 'send response to ' + rinfo.address + ':' + rinfo.port 94 | 95 | @_socket.on 'listening', => 96 | address = @_socket.address() 97 | 98 | @emit 'data', 'server listening ' + address.address + ':' + address.port 99 | 100 | @_socket.on 'error', (err) => 101 | @emit 'data', err 102 | 103 | @_socket.bind 123 104 | 105 | String::before = -> 106 | value = parseInt(@toString().split('.')[0], 10) 107 | #before 108 | if value then value else 0 109 | 110 | String::after = -> 111 | value = parseInt(@toString().split('.')[1], 10) 112 | #after 113 | if value then value else 0 114 | 115 | server = new TimeServer('1220580245', '0', '4', '4', '1', '0.9900', '0.9900') 116 | 117 | server.on 'data', (output) -> 118 | console.log output 119 | 120 | module.exports = TimeServer 121 | -------------------------------------------------------------------------------- /src/tftp.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | { createServer } = require 'tftp' 5 | { Transform } = require 'stream' 6 | 7 | dhcp = require './dhcp/server' 8 | ips = require('./ips')() 9 | 10 | 11 | class ProgressIndicator extends Transform 12 | constructor: (@size, options) -> 13 | super options 14 | 15 | @last = 0 16 | @bytes = 0 17 | 18 | _transform: (chunk, encoding, cb) -> 19 | @bytes += chunk.length 20 | 21 | percent = @bytes / @size * 100 | 0 22 | 23 | if (percent % 5) is 0 and percent isnt @last 24 | @last = percent 25 | 26 | @emit 'progress', 27 | percent: percent 28 | loaded: @bytes 29 | total: @size 30 | 31 | cb null, chunk 32 | 33 | return 34 | 35 | module.exports = ({ eth, ip, tftp }) -> 36 | if ! fs.existsSync tftp 37 | console.error 'ERROR:', tftp, 'not found?' 38 | process.exit 1 39 | 40 | if eth == null || eth == undefined || eth == true 41 | network = ips.find ({ name }) -> name is eth 42 | 43 | if ip == null || ip == undefined || ip == true 44 | ip = if network == null || network == undefined then "192.168.0.254" else network.address 45 | 46 | addr = ip.split '.' 47 | addr.pop() 48 | addr = addr.join '.' 49 | 50 | dhcpd = dhcp 51 | .createServer 52 | range: [ 53 | addr + '.10' 54 | addr + '.15' 55 | ] 56 | forceOptions: [ 'router', 'hostname', 'bootFile' ] 57 | randomIP: true 58 | netmask: '255.255.255.0' 59 | router: [ ip ] 60 | hostname: 'second.gateway' 61 | broadcast: addr + '.255' 62 | bootFile: (req, res) -> 63 | path.basename tftp 64 | server: ip 65 | .on 'listening', (sock, type) -> 66 | { address, port } = sock.address() 67 | 68 | console.log "Waiting for DHCP#{type} request... #{ address }:#{ port }" 69 | .on 'message', (data) -> 70 | console.log '### MESSAGE', JSON.stringify data 71 | .on 'bound', (state, ans) -> 72 | console.log '### BOUND', JSON.stringify state 73 | .on 'error', (err, data) -> 74 | return unless data 75 | 76 | console.log '!!! ERROR', err, data 77 | .listen 67 78 | 79 | server = createServer { 80 | host: '0.0.0.0' 81 | port: 69 82 | denyPUT: true 83 | }, (req, res) -> 84 | console.log 'Received tftp request from', req.stats.remoteAddress, 'for file', req.file 85 | stats = fs.statSync tftp 86 | res.setSize stats.size 87 | firmwareStream = fs.createReadStream tftp 88 | console.log 'Sending firmware to router...' 89 | prog = new ProgressIndicator stats.size 90 | 91 | done = false 92 | prog.on 'progress', ({ percent }) -> 93 | p = Math.round(percent * 100) / 100 94 | if p % 10 is 0 95 | console.log 'Sent: ' + p + '%' 96 | if percent >= 100 97 | if done 98 | return 99 | console.log 'Firmware sent! Now just wait for the router to reboot' 100 | firmwareStream.close() 101 | setTimeout -> 102 | process.exit 1 103 | , 20000 104 | 105 | done = true 106 | return 107 | 108 | firmwareStream 109 | .pipe prog 110 | .pipe res 111 | 112 | req.on 'error', (err) -> 113 | console.error 'ERROR:', err 114 | 115 | server.on 'error', (err) -> 116 | console.error 'ERROR:', err 117 | 118 | console.log 'Starting tftp server, listening on ' + ip + ':69' 119 | 120 | server.listen() 121 | 122 | [ dhcpd, server ] 123 | --------------------------------------------------------------------------------