Repository: xvzf/zyxel-gpon-sfp Branch: main Commit: 187435b0cb1c Files: 14 Total size: 19.7 KB Directory structure: gitextract_nqxjjm6j/ ├── .gitignore ├── Readme.md ├── flashdumps/ │ ├── binwalk.out │ ├── mtd0 │ ├── mtd1 │ ├── mtd2 │ ├── mtd3 │ ├── mtd4 │ ├── mtd5 │ ├── mtd6 │ ├── mtd7 │ └── sha256.sum ├── requirements.txt └── zyxel_gpon_sfp.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ venv/ ================================================ FILE: Readme.md ================================================ # "Hacking" the (Telekom) Zyxel GPON SFP module (PMG3000-D20B) > The SFP can be sourced very easily and is widely available in Germany. ## I just want fiber internet on my non-Telekom router [jaseg](https://github.com/jaseg/) has written a guide on this [on his blog](https://jaseg.de/blog/telekom-gpon-sfp/). ## TLDR Checkout the three options for configuring your SFP. When requiring a serial number change, this can be performed by the CLI only. Note, that some SFP NICs don't support hardwireing the speed settings. In this case, you need to connect the GPON fibre link to the module to be able to access it (see https://github.com/xvzf/zyxel-gpon-sfp/issues/8). ### 1. WEB UI 1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`. 2. Port-forward the SFPs web interface to your local machine via SSH: `ssh -L 127.0.0.1:8080:10.10.1.1:80 ` (maybe you need to add option '-oHostKeyAlgorithms=+ssh-dss' before '-L'to connect). 3. Access the web-interface on `http://localhost:8080`, username `admin`, password `1234`. ### 2. CLI (on the SFP) > Note: The PLOAM ID has to be HEX encoded, in case yours is a 10-character string, you can transform it using `python3 -c 'print(hex(""))'`. Omit the `0x` prefix. 1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`. 2. SSH into the module using `admin@10.10.1.1`, password `admin`. 3. Login into the CLI with user `admin`, password `1234`. 4. Change the _PLOAM/SLID/Installationskennung_ by entering following commands followed by a newline: - `hal` - `password ` 5. _Optional_: Change the serial number using `set sn ...`; the first four characters are ASCII encoded, e.g. `SCOM`, the rest is followed in hex. Within sw version 'V1.00(ABVJ.0)b3v' you need to use the whole SN as ASCII encoded string (cmd like 'set sn 534312345678'). ### 3. CLI (remote) > Note: requires Python >= 3.8 ``` NAME zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1 SYNOPSIS zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1 - COMMAND COMMANDS COMMAND is one of the following: info set_slid set_sn ``` ## Motivation My ISPs ([Deutsche Telekom](https://www.telekom.de/)) FTTH offering uses on a GPON network and distributes ONUs with a 1G (or 2.5G Ethernet) for non-business customers. I intended to run the fiber directly into my Linux router (using one of the SFP+ ports). Looking at the business offerings building upon the same technology revealed SFPs distributed only business customers using the [_Digitalisierungsbox Premium 2_](https://www.telekom.de/hilfe/geraete-zubehoer/router/digitalisierungsbox/premium-2#e_745060). The mentioned SFP is made by Zyxel with the identifier `PMG3000-D20B` and sold as [_Digitalisierungsbox Glasfaser Modem_](https://geschaeftskunden.telekom.de/internet-dsl/produkt/digitalisierungsbox-glasfasermodem-kaufen) (Telekom only sells it to business customers but it is available online for ~40 Euros). The module is based on a Lantiq 98035 SoC, [datasheet](https://www.electronicsdatasheets.com/download/51c42036e34e246e4900009c.pdf?format=pdf), [link to OpenWRT forums discussion on Huawei SFP module based on the same SoC](https://forum.openwrt.org/t/support-ma5671a-sfp-gpon/48042). ## Accessing the module After _reverse engineering_ (this time it has been a `fzf` through all files, not analysing the binaries) the firmware of _Telekom Digitalisierungsbox 2_, I've identified the IP address of the module being `10.10.1.1/24` based on a SQL statement with a comment: ```sql -- BS-6456: remove marker 'RESERVED' from static IP used to access the SFP module UPDATE Ip SET Name="" WHERE IpAddress="10.10.1.2" AND Interface="eth1" AND LogicalInterface="eth1"; ``` Digging a bit further in plaintext SQL statements reveals the credentials. ```sql -- ... INSERT INTO SshConfiguration VALUES ( 1, 0, 5, 22, 'Access only for authorized persons!', 0, '' ); INSERT INTO SshUser VALUES ( 1, 0, 'admin', 'admin', 0 ); -- ... INSERT INTO GPONConfig VALUES ( 1, 1, '10.10.1.1', 'admin', '1234', '', '' ); ``` Well, let's give it a try. SSH access sounds like a charm and is confirmed by nmap: ```bash xvzf@e300 ~ % nmap 10.10.1.1 Starting Nmap 7.80 ( https://nmap.org ) at 2022-02-02 06:31 UTC Nmap scan report for 10.10.1.1 Host is up (0.00079s latency). Not shown: 998 closed ports PORT STATE SERVICE 22/tcp open ssh 80/tcp open http MAC Address: (Zyxel Communications) Nmap done: 1 IP address (1 host up) scanned in 4.15 seconds ``` Let's give it a try with `ssh admin@10.10.1.1`: ``` ####################################################### # # # Please login to CLI mode. Then You can do commands. # # # ####################################################### Entering character mode Escape character is '^]'. Login: admin Password: ZYXEL# ZYXEL# linuxshell Enter linux shell show show system manufactory config mib sf log timer bsp hal igmp omci ssp ZYXEL# show version Project Name: TW2362H-CDEL Client Product Name: GTO100I_SFP_ZYXEL Internal Product Name: GTO100I_SFP_ZYXEL Hardware Version: PMG3000-D20B Boot Version: V1.0.0 Client Software Version: V1.0.0 Internal Software Version: V1.0.0 Build User: jiangyuanqi Build Time: 2021-05-08 11:28:36 Build Method: export ONU=gto100i_sfp_zyxel && cd ../drv && make install && cd .. && make rootfs && make install GIT Info: TW2362H-CDEL_lantiq98035/customize/TW2362H-CDEL_lantiq98035_general_20150131:e057bd83 ZYXEL# ``` So, we can get a linux shell, nice. My SFP is running a (very old) release of [OpenWrt](https://openwrt.org): ```bash ZYXEL# linuxshell BusyBox v1.19.4 (2014-06-30 12:00:02 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. _______ ________ __ | |.-----.-----.-----.| | | |.----.| |_ | - || _ | -__| || | | || _|| _| |_______|| __|_____|__|__||________||__| |____| |__| W I R E L E S S F R E E D O M ----------------------------------------------------- ATTITUDE ADJUSTMENT (Attitude Adjustment, 12.09_ltq) ----------------------------------------------------- * 1/4 oz Vodka Pour all ingredients into mixing * 1/4 oz Gin tin with ice, strain into glass. * 1/4 oz Amaretto * 1/4 oz Triple sec * 1/4 oz Peach schnapps * 1/4 oz Sour mix * 1 splash Cranberry juice ----------------------------------------------------- admin@SFP:~# uname -a Linux SFP 3.10.12 #2 Wed Jul 12 12:01:33 CST 2017 mips GNU/Linux admin@SFP:~# ``` ## Changing GPON Serial Number / PLOAM Password ``` ZYXEL# hal Hal# linuxshell Enter linux shell show show HAL configuration sn change ont parameters password change ont password set set ont parameters to1 change ont to1 interval to2 change ont to2 interval berinterval change BER interval sfthreshold change SF threshold sdthreshold change SD threshold tcont add tcont no delete HAL item gemport add HAL item reset Reset all pon configurations get get omci omci stream stream mvlanaction mvlanaction uni PPTP UNI configuration mtu MTU R/W multicast multicast configartion iphost iphost init init deny deny permit permit monitor monitor mac mac storm storm print print igmp igmp mcastfilt McastFilt Hal# sn change ont serial number Hal# password Formate:XXXXXXXXXXXXXXXXXXXX ``` The password seems to consist of 10 bytes, entered hex encoded. This is likely the PLOAM password / SLID / _Installationskennung_ / whatever you'd like to call it. The `sn` seems to change the serial number of the ONU (ONT) itself. This works, though it expects the first 4 characters to be ASCII encoded (e.g. for the Telekom Glasfasermodem 2, it likely starts with SCOM (hex:`5343 4f4d`) I assumed the CLI is using the configuration interface of OpenWRT under the hood; turns out I was right: ``` uci show gpon gpon.ploam=gpon gpon.ploam.nPassword=0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 gpon.ploam.nT01=16000 gpon.ploam.nT02=100 gpon.ploam.nEmergencyStopState=0 gpon.ploam.nRogueMsgIdUpstreamReset=255 gpon.ploam.nRogueMsgRepeatUpstreamReset=3 gpon.ploam.nRogueMsgIdDeviceReset=255 gpon.ploam.nRogueMsgRepeatDeviceReset=3 gpon.ploam.nRogueEnable=0 gpon.gtc=gpon gpon.gtc.bDlosEnable=0 gpon.gtc.bDlosInversion=0 gpon.gtc.nDlosWindowSize=0 gpon.gtc.nDlosTriggerThreshold=0 gpon.gtc.ePower=0 gpon.gtc.nLaserGap=0 gpon.gtc.nLaserOffset=0 gpon.gtc.nLaserEnEndExt=0 gpon.gtc.nLaserEnStartExt=0 gpon.gtc.nDyingGaspHyst=0 gpon.gtc.nDyingGaspMsg=0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 gpon.gtc.nDyingGaspEnable=0 gpon.ethernet=gpon gpon.ethernet.bUNI_PortEnable0=1 gpon.ethernet.bUNI_PortEnable1=1 gpon.ethernet.bUNI_PortEnable2=1 gpon.ethernet.bUNI_PortEnable3=1 gpon.gpe=gpon gpon.gpe.nPeNumber=6 ``` ## Observing the GPON SN and Password in real time ### Serial and Password The `onu` command helps debugging the system: - `onu gtcpg`: Retrieve password - `onu gtcsng`: Retrieve serial number - `onu gtcsns`: Set serial number ### Connection state **Connected** (`curr_state=5`) ```bash admin@SFP:~# onu ploamsg errorcode=0 curr_state=5 ``` **Disconnected** (`curr_state=1`): ```bash admin@SFP:~# onu ploamsg errorcode=0 curr_state=1 previous_state=0 elapsed_msec=16907701 ``` ## Enable 2.5G 2.5G may not be enabled by default on the SFP. Use the following command to enable 2.5 manually: ``` ZYXEL# hal Hal# set speed 2.5g mode full ``` You may have to disable auto-negotation and set a fixed port speed of 2.5G on your network adapter to make it work. ## How to handle with unknown passwords for Zyxel PMG3000-D20B **Disclaimer: Use this on your own risk. I am not responsible for any damages!** Sometimes the default login `admin/1234` for Zyxel PMG3000-D20B does not work - this describes how to get the password and how to change it. ### Get the system default password: 1. Open the webinterface (`http://10.10.1.1` if the modules default ip has not been changed) 2. Login with `guest/guest` 3. Set the admin password to `admin/1234`: `http://10.10.1.1/cgi/set_admin?rand=0.7041383755617387&type=2&username=admin&password=31rmzl323334m&level=0` - the device responds "1" 4. Open SSH, Login with `admin/admin` and for the Zyxel CLI Mode with `admin/1234` 5. Call `linuxshell` 6. Display device default password: `cat /var/config/.user_cfg` If this is not successful: 1. Write the actual config with `http://10.10.1.1/cgi/set_save?rand=0.4798344808717123` - the device responds "1" 2. Display device default password: `grep -ie "admin Password" /var/config/mib.conf` ### If you would like to change this permanently (survives a factory reset): The device default password is stored in the uboot-env partition (mtd1). The firmware contains the needed binaries but unfortunately not the needed layout-config (/etc/fw_env.config). Furthermore the /etc directory is not writeable because it is a squashfs filesystem. 1. Login into the device, start `linuxshell` 2. `cd /tmp` 3. `mkdir /tmp/mount_bind` 4. copy the hole /etc into /tmp/mount_bind: `cp -r /etc/ /tmp/mount_bind/` 5. mount the copy for /etc: `mount -o bind /tmp/mount_bind/etc/ /etc/` Now the /etc is (up to the next reboot) writeable, because its redirected to /tmp/mount_bind/etc. For creating the needed fw_env.config there is already a script which is called at boot time (and normally fails because of the read-only access). 1. `/etc/init.d/fw_env.sh boot` 2. Show the uboot-env variables: `fw_printenv` look for `remote_account_pwd` It is important that fw_printenv does not complain about checksum errors. **If it complains, do not continue!** Be careful changing values in the uboot-env! Laser calibration data is also stored here, they are individual for every module! Its a good idea to store the output of `fw_printenv` - alternatively make a backup of your mtd1 partition! 1. Set the new password: `fw_setenv remote_account_pwd 1234` 2. Restore factory-defaults: `http://10.10.1.1/cgi/set_default?rand=0.8890542929500389` - the device responds "1" 3. Reboot: `http://10.10.1.1/cgi/reset_onu?rand=0.14418772918225453` - the device responds "1" Be happy :-) ## HTTP API Only after getting SSH access I discovered the SFP comes with a WebUI and a _sort of_ API. The CLI `zyxel_gpon_sfp.py` makes use of this API to remotely configure the PLOAM password and possibly SN (again, didn't check it). ## TODO - [ ] Prometheus exporter - [ ] Integrate into OpenWRT ================================================ FILE: flashdumps/binwalk.out ================================================ Scan Time: 2022-02-14 09:07:48 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd0 MD5 Checksum: 073455446a6d5f611cb083897f596c2a Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 173708 0x2A68C CRC32 polynomial table, little endian Scan Time: 2022-02-14 09:07:48 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd1 MD5 Checksum: 9e8a8ec86a89b832696fc75b5202f317 Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- Scan Time: 2022-02-14 09:07:48 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd2 MD5 Checksum: 1ccc7807624108778f53ea5599acbafa Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 512 0x200 uImage header, header size: 64 bytes, header CRC: 0x4839EC66, created: 2017-07-12 04:01:47, image size: 1184511 bytes, Data Address: 0x80002000, Entry Point: 0x80002000, data CRC: 0xD4AF7C71, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "OpenWrtLinux-3.10.12-svn" 576 0x240 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3481980 bytes 1310720 0x140000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38 Scan Time: 2022-02-14 09:07:48 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd3 MD5 Checksum: 1ccc7807624108778f53ea5599acbafa Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 512 0x200 uImage header, header size: 64 bytes, header CRC: 0x4839EC66, created: 2017-07-12 04:01:47, image size: 1184511 bytes, Data Address: 0x80002000, Entry Point: 0x80002000, data CRC: 0xD4AF7C71, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "OpenWrtLinux-3.10.12-svn" 576 0x240 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3481980 bytes 1310720 0x140000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38 Scan Time: 2022-02-14 09:07:49 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd4 MD5 Checksum: 7a870b94b05dc6d2a37212fc0c8fa6e8 Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JFFS2 filesystem, big endian Scan Time: 2022-02-14 09:07:49 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd5 MD5 Checksum: 40f25c5f48fbf1df0286e3a80bc05911 Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- Scan Time: 2022-02-14 09:07:49 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd6 MD5 Checksum: 08c5b23a7cf0a1d97dcdca96ceb62c5b Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38 Scan Time: 2022-02-14 09:07:49 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd7 MD5 Checksum: 41d2e2c0c0edfccf76fa1c3e38bc1cf2 Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- Scan Time: 2022-02-14 09:07:49 Target File: /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/sha256.sum MD5 Checksum: 934ac986d9125173b5478f23e37f84dc Signatures: 411 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- ================================================ FILE: flashdumps/mtd5 ================================================ contains serial number, PLOAM etc ================================================ FILE: flashdumps/mtd7 ================================================ ================================================ FILE: flashdumps/sha256.sum ================================================ e7abc5af0c769c7a5f94fc54467b8c44c9a704b1b6c7390e113a5eee9cc89582 mtd0 2e5f53b7435d2cc6be28a4c4a1ce10797a499a903ef79ff0bd7d44cb25d626b3 mtd1 513ecc010db160f3ead5938dc2835eaa298d4aa390c5e4fdb7b8855b45067646 mtd2 513ecc010db160f3ead5938dc2835eaa298d4aa390c5e4fdb7b8855b45067646 mtd3 4a8f38484ae405693807c198d6dcf51037bddce5991d11d9d4a98b796ea269d2 mtd4 720eb0caef7a4a6b5796f235b40ed0085b1af439f28592205f50abe37d7bf9c5 mtd5 81e085ea6601838740e728b9310e6b62da54cbcbf42b02a2805625b6db3c5878 mtd6 b5a41c3758763bbec72769fab4a2533bf2db0b6312d93d25a695f9e4b9e02260 mtd7 ================================================ FILE: requirements.txt ================================================ demjson==1.6 fire==0.4.0 requests==2.27.1 ================================================ FILE: zyxel_gpon_sfp.py ================================================ import fire import requests import random import demjson from binascii import hexlify def is_hex(a): """ checks if a is a properly formated hex string """ try: int(a, 16) # Just check if we can properly parse the hex return len(a) % 2 == 0 # We're dealing with strings hex encoded except ValueError: return False class SFP: def __init__(self, sfp_addr, username="admin", password="1234"): self._sfp_addr = sfp_addr self._user = username self._pass = password def _req(self, path, method="GET", headers={}): """ Helper to perform request on the SFPs HTTP API """ full_path = f"{self._sfp_addr}{path}" auth = (self._user, self._pass) if method == "GET": return requests.get(full_path, auth=auth, headers=headers) elif method == "POST": return requests.post(full_path, auth=auth, headers=headers) def info(self): resp_sn = self._req(path="/cgi/get_sn") resp_gpon = self._req(path="/cgi/get_gpon_info") # WARNING: This is a javascript object, not JSON... test = demjson.decode(resp_sn.text) | demjson.decode(resp_gpon.text) return test def set_slid(self, slid, string=False): # Transform to a valid parameter _slid = hexlify(slid).decode( "ascii").lower() if string else slid.lower() if not is_hex(_slid): return f"[!] Invalid SLID `{_slid}` (HEX)" print(f"[ ] Applying SLID `{_slid}` (HEX)") resp = self._req( path=f"/cgi/set_sn?mode=1&pass={_slid}", method="POST") if resp.status_code == 200 and resp.text == "1": return f"[+] Applied SLID `{_slid}`, a reboot of the SFP is required." def set_sn(self, sn, string): return "Untested" # _sn = hexlify(sn) if string else sn # f not is_hex(_sn): # return f"[!] Invalid SN `{_sn}` (HEX)" # print(f"[ ] Applying SN `{_sn}` (HEX)") # resp = self._req(path=f"/cgi/set_sn?mode=1?sn={sn}", method="POST") # if resp.status_code == 200 and resp.text == "1": # return f"[+] Applied SN `{_sn}`, a reboot of the SFP is required." if __name__ == "__main__": fire.Fire(SFP)