Initial commit
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
# Your own values: ONT serial/MAC/versions, router + admin addresses.
|
||||
# Copy example.env -> .env and fill it in. Never commit .env.
|
||||
.env
|
||||
|
||||
# Flash dumps and their checksums — keep LOCAL, never commit/push. They are
|
||||
# device-unique and contain per-device calibration, MAC, serial, PLOAM.
|
||||
backups/
|
||||
*.bin
|
||||
|
||||
# As-built notes for THIS line — contain public IPs, the real GPON serial/MAC,
|
||||
# LAN ranges and port-forward targets. README.md is the shareable guide.
|
||||
# Delete this line if you have sanitised docs/ and want to publish them.
|
||||
docs/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,522 @@
|
||||
# tet-fiber-sfp
|
||||
|
||||
Replace an ISP-supplied **Nokia GPON ONT** with a **D-Link DPN-100** GPON SFP ONT stick
|
||||
(Realtek RTL9601CI) seated directly in a **MikroTik hAP ac** (`sfp1`), by cloning the
|
||||
ONT's identity onto the stick. No extra box, no extra PSU, no extra Ethernet hop.
|
||||
|
||||
Done against **TET** (Latvia) on a Nokia OLT with a **G-2426G-A** ONT. It registered
|
||||
**O5 / OPERATION on the first fibre connect** — no fake-O5 loop — and pulled a public
|
||||
DHCP lease on the WAN VLAN. The method is generic to Nokia-OLT GPON ISPs; the exact
|
||||
identity values and the WAN VLAN are yours to discover.
|
||||
|
||||
> **Scope of this README:** the full procedure, with every line-specific value replaced
|
||||
> by a placeholder. Put your real values in `.env` (gitignored) — see
|
||||
> [`example.env`](example.env).
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Before you start](#1-before-you-start)
|
||||
2. [Read your ONT's identity](#2-read-your-onts-identity-do-this-first)
|
||||
3. [Bench setup — talk to the stick](#3-bench-setup--talk-to-the-stick)
|
||||
4. [Configure `.env`](#4-configure-env)
|
||||
5. [Write the clone values](#5-write-the-clone-values)
|
||||
6. [Reboot, verify, connect the fibre](#6-reboot-verify-connect-the-fibre)
|
||||
7. [Router side — WAN VLAN, DHCP, firewall](#7-router-side--wan-vlan-dhcp-firewall)
|
||||
8. [Lock down the stick's management](#8-lock-down-the-sticks-management)
|
||||
9. [Revert to factory](#9-revert-to-factory)
|
||||
10. [Troubleshooting](#10-troubleshooting)
|
||||
11. [Tools reference](#11-tools-reference)
|
||||
12. [Known unknowns](#12-known-unknowns)
|
||||
|
||||
---
|
||||
|
||||
## 1. Before you start
|
||||
|
||||
**Hardware**
|
||||
|
||||
| Thing | Notes |
|
||||
|---|---|
|
||||
| D-Link DPN-100 GPON SFP ONT | Rev C1 here, stock firmware `R4.2.104.033e` ("Wind"), 16 MB SPI-NOR, Realtek RTL9601CI |
|
||||
| MikroTik with an SFP cage | hAP ac (RB962UiGS), RouterOS 7.x. Any RouterOS box with `sfp1` works |
|
||||
| Your ISP's ONT | still connected, so you can read its identity **before** you swap |
|
||||
| USB-UART / serial adapter | *strongly recommended* — the only recovery path if you brick the stick |
|
||||
|
||||
**Know what you're doing here**
|
||||
|
||||
- You are presenting a different device to your ISP's OLT under your own ONT's identity.
|
||||
Whether that's acceptable is between you and your contract — check it. Only ever clone
|
||||
**your own** ONT, on **your own** line.
|
||||
- Two ONTs with the same serial must never be on the fibre at once. Unplug the original.
|
||||
- The ISP keeps OMCI control of whatever ONT is on the line. That's inherent to GPON.
|
||||
- The `mtd1` "Config" partition holds this stick's unique serial, PLOAM password and **RF
|
||||
calibration** — there is no download for it. Nothing in this guide writes to it directly,
|
||||
but if you want a recovery copy before you start, `tools/gpon_backup.py` dumps a partition
|
||||
over telnet and md5-verifies it (`python3 tools/gpon_backup.py 1 mtd1_config.bin`).
|
||||
- Class 1 laser. Don't look into the fibre or the SFP's port.
|
||||
|
||||
---
|
||||
|
||||
## 2. Read your ONT's identity (do this first)
|
||||
|
||||
While the ISP's ONT is still up, log in to its **web UI** (its LAN address is on the
|
||||
label) and go to **Maintenance → Log**. Everything you need is in the **first few lines**,
|
||||
in one place — that's the ONT reporting its TR-069 `DeviceInfo` parameters to the ISP's ACS
|
||||
at boot, so the whole identity set appears together. The individual values are also
|
||||
scattered across the status pages, but the log saves you the hunt.
|
||||
|
||||
| `.env` key | Field in **Maintenance → Log** | What it is (OMCI) |
|
||||
|---|---|---|
|
||||
| `ONT_VENDOR` | `Manufacturer` | GPON **vendor ID**. Nokia/Alcatel-Lucent reports `ALCL` — the same 4 chars the serial starts with, so the two cross-check each other |
|
||||
| `ONT_SERIAL` | `SerialNumber` | GPON **serial number** — the OLT's primary ONT identifier. 12 chars = 4 ASCII vendor bytes + 4 bytes rendered as 8 hex chars |
|
||||
| `ONT_EQID` | `ProductClass` | **Equipment ID** (ME257 ONT2-G) = the model string |
|
||||
| `ONT_HWVER` | `HWVer` | **Hardware version** (ME256 ONT-G) |
|
||||
| `ONT_SWVER` | `SWVer` | **Software version** (ME7). If it carries a bracketed build suffix like `XXXXXXXXXXXXXX(1.2345.678)`, the OMCI value is the **14-char prefix** without it |
|
||||
| `ONT_MAC` | *not in the log* | The ONT's own MAC — **printed on the box label** |
|
||||
|
||||
Cross-check what you can against the physical labels: the serial and model are printed on
|
||||
the ONT itself, and the hardware version is usually the string the box label calls
|
||||
**"ONT P/N"**. Two independent sources per value is worth the thirty seconds.
|
||||
|
||||
Also worth screenshotting before you lose access: the ONT's WAN service list (which
|
||||
VLAN ID carries internet vs VoIP vs TR-069) and its optical levels (Rx/Tx dBm), so you
|
||||
have a reference for what "healthy" looks like on your line.
|
||||
|
||||
**Two things you probably do not need:**
|
||||
|
||||
- **PLOAM password** — not exposed in the Nokia UI. On the line this was done against,
|
||||
the OLT authenticates by **serial only** and registration worked without it. Leave the
|
||||
stick's factory value; only revisit if the OLT rejects you with a password error.
|
||||
- **The MAC, in bridge mode.** `mac1` is the *stick's own* identity (its management
|
||||
interface). The stick bridges transparently, so the L2 source your ISP's DHCP/BNG
|
||||
actually sees is your **router's** `sfp1.<vlan>` MAC — which you never change, and it
|
||||
still works. Cloning `mac1` is faithful and harmless, but it is not in the data path.
|
||||
It would only matter if you ran the stick in **router** mode.
|
||||
|
||||
---
|
||||
|
||||
## 3. Bench setup — talk to the stick
|
||||
|
||||
Seat the stick in `sfp1` **with no fibre attached**. The host-side link will not come up
|
||||
until you tell RouterOS to ignore loss-of-signal:
|
||||
|
||||
```
|
||||
/interface ethernet set sfp1 sfp-ignore-rx-los=yes
|
||||
```
|
||||
|
||||
`sfp1` should now show `link-ok` at 1 Gbps. The stick's management lives **untagged** on
|
||||
`sfp1` at its factory address `192.168.100.1` (telnet, `root`/`admin` — the documented
|
||||
D-Link/Realtek defaults for this build). Give the router an address in that subnet:
|
||||
|
||||
```
|
||||
/ip address add address=192.168.100.2/24 interface=sfp1 comment=gpon-bench-test
|
||||
```
|
||||
|
||||
**Reaching it from a workstation.** Do **not** try to `dst-nat` to `192.168.100.1` —
|
||||
RouterOS marks that rule INVALID. Source-NAT out of `sfp1` instead, so the stick replies
|
||||
to an address it can route to:
|
||||
|
||||
```
|
||||
/ip firewall nat add chain=srcnat out-interface=sfp1 action=masquerade \
|
||||
src-address=<ADMIN_HOST> comment=gpon-bench-test
|
||||
```
|
||||
|
||||
Your workstation then reaches `192.168.100.1` via its default gateway. Check it:
|
||||
|
||||
```bash
|
||||
cp example.env .env # then edit .env — see step 4
|
||||
python3 tools/gpon_cli.py 'system' 'ontver'
|
||||
```
|
||||
|
||||
**The CLI is not a shell.** Login drops you into the restricted Realtek OMCI CLI,
|
||||
prompt `#ONT>`. Two contexts matter:
|
||||
|
||||
```
|
||||
#ONT> system -> #ONT/system>
|
||||
#ONT/system> misc -> #ONT/system/misc> EEPROM identity fields (<field> get|set)
|
||||
#ONT/system> shell -> #ONT/system/shell> real busybox root shell (uid=0)
|
||||
```
|
||||
|
||||
Other menus: `#ONT/traffic>` → `{pon,eth,voip,omci,router,hpna}`;
|
||||
`#ONT/system>` → `{mib,log,env,misc,manu,fs,ippath,net,shell,debug,ontver,custom}`.
|
||||
|
||||
The stick has **no default route** — it can only reach its own `/24`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Configure `.env`
|
||||
|
||||
```bash
|
||||
cp example.env .env
|
||||
$EDITOR .env
|
||||
```
|
||||
|
||||
`GPON_HOST` / `GPON_PORT` / `GPON_USER` / `GPON_PASS` are the stick's factory defaults and
|
||||
are read by everything in `tools/`. Fill in the `ONT_*` values from step 2, and the
|
||||
`STICK_*` values from the read-back below, which are your revert path.
|
||||
|
||||
`.env` is gitignored. `example.env` is the shareable template. Resolution order is
|
||||
process environment → `.env` → `example.env`, so you can also do a one-off with
|
||||
`GPON_HOST=… python3 tools/…`.
|
||||
|
||||
Record the stick's own factory identity into `.env` now, before you overwrite it:
|
||||
|
||||
```bash
|
||||
python3 tools/gpon_cli.py 'system' 'misc' \
|
||||
'vendor get' 'eqsn get' 'eqid get' 'eqvid get' 'mac1 get' 'pon_passwd get'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Write the clone values
|
||||
|
||||
Five fields are direct EEPROM writes in `#ONT/system/misc`. The sixth (software version)
|
||||
is not an EEPROM field at all.
|
||||
|
||||
### 5a. EEPROM identity
|
||||
|
||||
```bash
|
||||
set -a; . ./.env; set +a
|
||||
python3 tools/gpon_cli.py 'system' 'misc' \
|
||||
"vendor set \"$ONT_VENDOR\"" \
|
||||
"eqsn set \"$ONT_SERIAL\"" \
|
||||
"eqid set \"$ONT_EQID\"" \
|
||||
"eqvid set \"$ONT_HWVER\"" \
|
||||
"mac1 set $ONT_MAC"
|
||||
```
|
||||
|
||||
Each `set` returns `---ATECMDRESULT--- OK`. Read them back with `<field> get`.
|
||||
|
||||
> A read-back of `eqsn` may show the trailing 8 characters **lowercased**. That is
|
||||
> cosmetic: the serial's second half is stored as 4 binary bytes and re-rendered as
|
||||
> lowercase hex. The 8 bytes the OLT checks are the 4 ASCII vendor bytes followed by
|
||||
> those 4 binary bytes, which are unchanged.
|
||||
|
||||
### 5b. Software version (ME7)
|
||||
|
||||
The SW version is read at boot from a config file, not from EEPROM. The Realtek config
|
||||
loader (`libvos.so`) reads `/etc/sys.cfg`, `/tmp/sys.cfg`, **`/mnt/rwdir/sys.cfg`**,
|
||||
`/mnt/backupdir/etc/sys.cfg` in order and **merges** them per key — so a one-line overlay
|
||||
overrides only `SWVER` and leaves the read-only cramfs base untouched:
|
||||
|
||||
```bash
|
||||
set -a; . ./.env; set +a
|
||||
python3 tools/gpon_cmd.py "echo SWVER=$ONT_SWVER > /mnt/rwdir/sys.cfg" 'sync' 'cat /mnt/rwdir/sys.cfg'
|
||||
```
|
||||
|
||||
### 5c. Leave these alone (with fallbacks if it doesn't register)
|
||||
|
||||
| Field | Leave at | Why |
|
||||
|---|---|---|
|
||||
| `misc olt_vendor` | `0` (auto) | Stock `/etc/sys.cfg` ships `MUL_VENDOR_EN=2` + an `EXPECT_VENDOR_ID` list covering ALU/Nokia auto-adapt, and the DPN-100 C1 guides also leave it. **Fallback on fake-O5:** try `21`, then `1`, then `3` |
|
||||
| `misc pon_passwd` (PLOAM) | factory | Serial-based auth. **Fallback:** set it only if the OLT rejects with a password error |
|
||||
| `misc mac2`, `hwver`, `mpcode`, `mpsn`, `mhwv` | unchanged | Manufacturing/secondary fields, not checked by the OLT |
|
||||
|
||||
---
|
||||
|
||||
## 6. Reboot, verify, connect the fibre
|
||||
|
||||
EEPROM writes are immediate and persistent; `sys.cfg` is only read at boot. So a clean
|
||||
**reboot** applies everything:
|
||||
|
||||
```bash
|
||||
python3 tools/gpon_cmd.py 'reboot'
|
||||
python3 tools/gpon_verify.py 'system' 'ontver' 'misc' \
|
||||
'vendor get' 'eqsn get' 'eqid get' 'eqvid get' 'mac1 get'
|
||||
```
|
||||
|
||||
> ⚠ Do **not** use `#ONT/system/mib` → `reset` as a "commit". It resets the runtime MIB to
|
||||
> defaults; it is not a save.
|
||||
|
||||
`gpon_verify.py` waits for telnet to drop (confirming the reboot actually began), waits
|
||||
for it to come back, then reads the values. The one that proves the overlay worked is
|
||||
`ontver` — it can only have changed from the stock firmware string if `/mnt/rwdir/sys.cfg`
|
||||
was read at boot.
|
||||
|
||||
Now **unplug the fibre from the ISP's ONT and plug it into the stick.** Then:
|
||||
|
||||
```bash
|
||||
python3 tools/gpon_cli.py 'traffic' 'pon' 'show link'
|
||||
```
|
||||
|
||||
Want:
|
||||
|
||||
```
|
||||
Link State: ACTIVE
|
||||
Operation State Machine: OPERATION (O5)
|
||||
```
|
||||
|
||||
`show alarm` in the same menu lists PON MAC alarms. If you're stuck below O5, see
|
||||
[Troubleshooting](#10-troubleshooting).
|
||||
|
||||
---
|
||||
|
||||
## 7. Router side — WAN VLAN, DHCP, firewall
|
||||
|
||||
### ⚠ Do the firewall first. Read this before you add the DHCP client.
|
||||
|
||||
A stock RouterOS `input` chain typically protects the WAN with **one rule**:
|
||||
`drop chain=input in-interface=ether1`. That is not a catch-all — the chain otherwise
|
||||
**default-accepts**, and `api` (8728), `api-ssl` (8729) and `ssh` (22) commonly listen with
|
||||
no address restriction. They were safe only because `ether1` was the single public interface.
|
||||
|
||||
On the line this was built against, the new VLAN interface got its public IP **before** it
|
||||
was added to that drop — and the router's API was reachable from the internet and being
|
||||
brute-forced within minutes. (Failed logins only, no breach: verified no rogue users, no
|
||||
scheduler entries, no socks, and the only successful logins were from the LAN.)
|
||||
|
||||
**The rule: never let a public IP land on a MikroTik interface that is not already in the
|
||||
WAN drop set.** Two ordering facts make this concrete:
|
||||
|
||||
- An **`input`** rule matching an interface **list** is valid immediately, so you can lock
|
||||
it down before any address exists.
|
||||
- A **`forward`** rule with `in-interface=X` is flagged `I`/INVALID while `X` has no IP, so
|
||||
it cannot be pre-created — it goes valid the moment DHCP binds.
|
||||
|
||||
So: add the interface to the WAN list and fix the input drop **first**, then add the DHCP
|
||||
client, then add the forward drop.
|
||||
|
||||
### Config
|
||||
|
||||
```
|
||||
# 1) VLAN sub-interface on the stick port. Verify the VLAN ID for YOUR ISP (below).
|
||||
/interface vlan add name=sfp1.<WAN_VLAN> interface=sfp1 vlan-id=<WAN_VLAN> comment=gpon-wan
|
||||
|
||||
# 2) FIRST: treat it as WAN. Add to the interface-list your input drop keys on...
|
||||
/interface list member add list=WAN interface=sfp1.<WAN_VLAN> comment=gpon-wan
|
||||
|
||||
# ...and convert the single-interface input drop to match the list
|
||||
/ip firewall filter ... chain=input action=drop in-interface-list=WAN \
|
||||
comment="WAN mgmt lockout (all WAN interfaces)"
|
||||
|
||||
# 3) THEN the DHCP client. add-default-route=no keeps your existing WAN primary
|
||||
# while you test; use-peer-dns=no avoids clobbering your resolvers.
|
||||
/ip dhcp-client add interface=sfp1.<WAN_VLAN> add-default-route=no use-peer-dns=no \
|
||||
comment=gpon-wan-v4 disabled=no
|
||||
|
||||
# 4) Once the lease binds: the "drop from WAN not dstnat" forward rule for it
|
||||
/ip firewall filter add chain=forward action=drop connection-state=new \
|
||||
connection-nat-state=!dstnat ipsec-policy=in,none in-interface=sfp1.<WAN_VLAN> \
|
||||
comment="drop from WAN not dstnat (fibre)"
|
||||
```
|
||||
|
||||
Then confirm the fibre actually passes traffic without disturbing your primary WAN:
|
||||
|
||||
```
|
||||
/ping 8.8.8.8 src-address=<LEASED_IP> interface=sfp1.<WAN_VLAN>
|
||||
```
|
||||
|
||||
### Finding your WAN VLAN
|
||||
|
||||
The VLAN IDs listed in the ONT's own WAN service table are **ONT-internal C-VLANs**. What
|
||||
your router sees through the stick is the tag on the Ethernet side, which OMCI translation
|
||||
may set differently — community reports of four-digit S-VLANs are about the OLT side of
|
||||
the fibre, which you never see here. On this line the Ethernet-side internet VLAN turned
|
||||
out to be the same low ID the ONT listed for INTERNET, and **untagged got no DHCP reply
|
||||
at all**.
|
||||
|
||||
Determine it empirically: with the stick registered at O5, either try a DHCP client on the
|
||||
ONT's listed internet VID first, or sniff for tagged frames —
|
||||
`/tool sniffer quick interface=sfp1` and read the VLAN column.
|
||||
|
||||
Note that WAN (tagged) and the stick's management (untagged) coexist on `sfp1` with no
|
||||
conflict, so you keep telnet access while the WAN is live.
|
||||
|
||||
### Cutting over for real
|
||||
|
||||
Deliberately *not* done in the reference setup — the fibre was left present-but-not-default.
|
||||
To actually use it:
|
||||
|
||||
1. **Masquerade:** add the VLAN interface to whatever your `srcnat` rules key on, or add
|
||||
`chain=srcnat out-interface=sfp1.<WAN_VLAN> action=masquerade`.
|
||||
2. **Default route:** `/ip dhcp-client set [find comment=gpon-wan-v4] add-default-route=yes`
|
||||
— either as primary, or with a higher `default-route-distance` for failover behind your
|
||||
existing WAN.
|
||||
3. **Re-add, deliberately, any service accepts and `dst-nat` port-forwards** you want
|
||||
reachable on the fibre IP. Until you do, nothing is forwarded in and management is
|
||||
dropped. Think before you duplicate port-forwards: doing so publishes those internal
|
||||
hosts on a second public IP.
|
||||
|
||||
Or route **one host** out of the fibre and leave everyone else alone. RouterOS 7.22 rejects
|
||||
the older mangle `action=mark-routing` form (comes up INVALID) — use `/routing rule`:
|
||||
|
||||
```
|
||||
/routing table add name=via-fibre fib
|
||||
/ip route add dst-address=0.0.0.0/0 gateway=<FIBRE_GATEWAY> routing-table=via-fibre comment="pbr: fibre default"
|
||||
/ip firewall nat add chain=srcnat out-interface=sfp1.<WAN_VLAN> action=masquerade comment="pbr: masq to fibre"
|
||||
# keep RFC1918 on the main table, send everything else out the fibre
|
||||
/routing rule add src-address=<HOST> dst-address=10.0.0.0/8 action=lookup table=main comment=pbr
|
||||
/routing rule add src-address=<HOST> dst-address=172.16.0.0/12 action=lookup table=main comment=pbr
|
||||
/routing rule add src-address=<HOST> dst-address=192.168.0.0/16 action=lookup table=main comment=pbr
|
||||
/routing rule add src-address=<HOST> action=lookup table=via-fibre comment="pbr SWITCH: disable to revert"
|
||||
```
|
||||
|
||||
Disabling that last rule is the whole revert. Existing connections stay on the old WAN
|
||||
until they reconnect — only new flows move. If new flows don't switch, exclude the host
|
||||
from FastTrack.
|
||||
|
||||
### RouterOS gotchas
|
||||
|
||||
- **`in-interface=""` does not clear an interface matcher** — it errors with "ambiguous
|
||||
value". To convert a rule: capture its `.id`, `add` the replacement with
|
||||
`place-before=$id`, then `remove $id`.
|
||||
- Over SSH, keep a `:local` and its use **on one line** — each line is its own scope.
|
||||
- Label everything with `comment=` (see the cleanup list at the end of this section) so
|
||||
you can find and remove your test objects later.
|
||||
|
||||
**Objects this procedure creates:** `/interface vlan sfp1.<WAN_VLAN>`; that interface in
|
||||
the `WAN` interface list; `/ip dhcp-client` on it; `/ip address 192.168.100.2/24` on
|
||||
`sfp1`; the `srcnat` masquerade to the stick; the input and forward firewall rules.
|
||||
|
||||
---
|
||||
|
||||
## 8. Lock down the stick's management
|
||||
|
||||
On stock firmware the telnet password **cannot be changed**, so control access at the
|
||||
router. The stick is not internet-reachable to begin with — private address, no `dst-nat`
|
||||
to it, the WAN forward-drop covers it, and it does not bridge management onto the WAN VLAN
|
||||
— but by default any LAN host can reach it via the connected route.
|
||||
|
||||
Restrict it to one admin host, and scope the return-path masquerade to match:
|
||||
|
||||
```
|
||||
/ip firewall filter add chain=forward action=accept src-address=<ADMIN_HOST> \
|
||||
dst-address=192.168.100.0/24 comment="gpon-stick: allow admin host" \
|
||||
place-before=[:pick [/ip firewall filter find chain=forward] 0]
|
||||
/ip firewall filter add chain=forward action=drop dst-address=192.168.100.0/24 \
|
||||
comment="gpon-stick: block LAN from stick mgmt" \
|
||||
place-before=[:pick [/ip firewall filter find chain=forward] 0]
|
||||
/ip firewall nat set [find comment=gpon-bench-test] src-address=<ADMIN_HOST>
|
||||
```
|
||||
|
||||
The router's own traffic isn't forward-filtered, so it can still reach the stick. Reopen by
|
||||
disabling the two `gpon-stick` rules. For a genuinely changeable password plus SSH you need
|
||||
community firmware — do that with the UART adapter as your recovery net.
|
||||
|
||||
---
|
||||
|
||||
## 9. Revert to factory
|
||||
|
||||
From the `STICK_*` values you recorded in step 4:
|
||||
|
||||
```bash
|
||||
set -a; . ./.env; set +a
|
||||
python3 tools/gpon_cli.py 'system' 'misc' \
|
||||
"vendor set \"$STICK_VENDOR\"" \
|
||||
"eqsn set \"$STICK_SERIAL\"" \
|
||||
"eqid set \"$STICK_EQID\"" \
|
||||
"eqvid set \"$STICK_HWVER\"" \
|
||||
"mac1 set $STICK_MAC"
|
||||
python3 tools/gpon_cmd.py 'rm /mnt/rwdir/sys.cfg' 'sync' 'reboot'
|
||||
```
|
||||
|
||||
Removing the overlay restores the base `SWVER` from the read-only cramfs `/etc/sys.cfg`.
|
||||
|
||||
To undo the router side: remove the objects listed at the end of step 7, and
|
||||
`/interface ethernet set sfp1 sfp-ignore-rx-los=no`.
|
||||
|
||||
If the stick is bricked rather than merely misconfigured, you need the UART adapter and a
|
||||
dump of its `mtd1` Config partition — which is why it's worth taking one first.
|
||||
|
||||
---
|
||||
|
||||
## 10. Troubleshooting
|
||||
|
||||
**Stuck at O1, or fake-O5 (registers then drops in a loop).** Fake-O5 usually means the OLT
|
||||
is trying a firmware-download handshake it doesn't like the answer to. In order: re-check
|
||||
`eqsn` for a typo (it is *the* identifier); confirm `ontver` reflects `ONT_SWVER`; then try
|
||||
`misc olt_vendor set 21`, then `1`, then `3`, rebooting between each. If the OLT complains
|
||||
about a password specifically, set `pon_passwd`. If it still refuses, the remaining lever is
|
||||
community firmware (below).
|
||||
|
||||
**No DHCP on the WAN VLAN.** Confirm O5 first — nothing works below it. Then confirm you
|
||||
have the right VLAN ID (untagged will not answer), that the VLAN interface is on `sfp1`, and
|
||||
that the DHCP client is enabled and not stuck at "searching".
|
||||
|
||||
**`sfp1` link never comes up.** `sfp-ignore-rx-los=yes` is required without fibre. Check
|
||||
`/interface ethernet monitor sfp1` and re-seat the module.
|
||||
|
||||
**Stick doesn't come back after reboot.** `gpon_verify.py` gives up after ~140 s; raise
|
||||
`--wait`. If it truly doesn't return, re-seat it and check `sfp1`.
|
||||
|
||||
**`flash get` / `flash set` fail.** Broken on this stock build — `libmib.so.0` is missing.
|
||||
Use the OMCI CLI (`misc <field> set`) for identity instead. The `flash` tool only becomes
|
||||
usable after a community reflash, so ignore guides that lead with it.
|
||||
|
||||
**Temperature reads 0 / the router reports a bad SFP EEPROM checksum.** Both are expected
|
||||
and neither is fixable in software: the temperature sensor isn't populated
|
||||
(`misc showtemp` → `temp = 0`), and the transceiver's DDM block has a bad checksum, so
|
||||
RouterOS shows `eeprom-checksum: bad` and no DDM. These sticks run genuinely hot; manage it
|
||||
physically (airflow, a small heatsink on the exposed end). **Your real health signal is the
|
||||
registration state, not temperature** — a thermal or link fault shows up as PON dropping
|
||||
`O5 → O1` or `sfp1` flapping. Also note `misc alarmstat` reports `ALARM1`/`ALARM2`/`DOOROPEN`
|
||||
as "yes" — spurious on an SFP form factor, ignore it.
|
||||
|
||||
**Read-only diagnostics** (need the stick's management address reachable):
|
||||
|
||||
| Command | Shows |
|
||||
|---|---|
|
||||
| `traffic/pon > show link` | PON registration state — want `ACTIVE` / `OPERATION (O5)` |
|
||||
| `traffic/pon > show alarm` | PON MAC alarms |
|
||||
| `system > ontver` | OMCI software version (should equal `ONT_SWVER`) |
|
||||
| `system/misc > eth_status`, `CPU_inform` | Ethernet status, CPU (reports RTL8672) |
|
||||
| `system/misc > showtemp`, `alarmstat` | unusable on this stick, see above |
|
||||
|
||||
**Reflashing (last resort, functional reasons only).** Newer community builds have working
|
||||
`flash get/set` and better Nokia OMCI / fake-O5 handling. **Match the flash size** —
|
||||
this stick is 16 MB, so a 16 MB build is the right target and an 8 MB one is not. Do it
|
||||
with the UART adapter connected as your recovery path, never blind over telnet.
|
||||
|
||||
---
|
||||
|
||||
## 11. Tools reference
|
||||
|
||||
Python 3, standard library only, no dependencies. Connection settings come from `.env`
|
||||
(see step 4); run them from the repo root.
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `tools/gpon_cli.py` | Drive the `#ONT` OMCI CLI — navigates menus, one argument per CLI line |
|
||||
| `tools/gpon_cmd.py` | Drive the busybox root shell (enters `system` → `shell` for you) |
|
||||
| `tools/gpon_verify.py` | Wait out a reboot (down, then up), then run CLI reads |
|
||||
| `tools/gpon_backup.py` | Dump one MTD partition over telnet, md5-verified |
|
||||
| `tools/gpon_telnet.py` | Minimal raw telnet driver, for poking at anything else |
|
||||
| `tools/gpon_env.py` | Shared `.env` loader (not run directly) |
|
||||
|
||||
```bash
|
||||
python3 tools/gpon_cli.py 'traffic' 'pon' 'show link'
|
||||
python3 tools/gpon_cmd.py 'cat /proc/mtd' 'ls -la /mnt/rwdir'
|
||||
python3 tools/gpon_backup.py 1 mtd1_config.bin
|
||||
python3 tools/gpon_telnet.py --host 192.168.100.1 --port 23 --wait 3 '?'
|
||||
```
|
||||
|
||||
All of them speak enough of the telnet protocol to refuse option negotiation and strip
|
||||
IAC sequences, and read until the prompt returns or an idle timeout expires (`--idle`,
|
||||
`--cap`) — the stick is slow and chatty, so raise those rather than assuming a hang.
|
||||
|
||||
**Repo layout**
|
||||
|
||||
```
|
||||
README.md this guide
|
||||
example.env template -> copy to .env (gitignored)
|
||||
tools/ Python telnet drivers for the stick
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Known unknowns
|
||||
|
||||
- **`OMCI_SW_VER2` — the standby image version.** A Nokia ONT reports **two different**
|
||||
software versions (active and standby). `SWVER` sets the active ME7 value, and on this
|
||||
stick both slots then report it. It registered fine — but if an OLT ever runs a
|
||||
fake-O5 firmware-download sequence that requires the two to differ, this is what to
|
||||
revisit, and the standby string is not obtainable from the ONT's UI.
|
||||
- **ISP fleet firmware updates.** If your ISP pushes a software update to its ONT fleet,
|
||||
the cloned `ONT_SWVER` may need updating to match — reported by others on Nokia OLTs.
|
||||
|
||||
## Credits
|
||||
|
||||
The hack-gpon project's DPN-100 / Realtek documentation, and the Latvian GPON-SFP
|
||||
community discussion (notably `@KristapsStudy`'s thread on cloning to a Nokia OLT) for the
|
||||
`OMCI_SW_VER1`/`VER2` + `OMCI_OLT_MODE` leads.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Copy to .env and fill in your own values. .env is gitignored — it holds the
|
||||
# values that identify YOUR line and YOUR network, and must never be committed.
|
||||
#
|
||||
# cp example.env .env
|
||||
#
|
||||
# GPON_* are read by the scripts in tools/ (via tools/gpon_env.py).
|
||||
# The ONT_*/STICK_*/router values are consumed by the copy-paste snippets in
|
||||
# README.md (shell snippets source this file; RouterOS snippets use <PLACEHOLDERS>).
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stick management (telnet) — these are the DPN-100 factory defaults and are
|
||||
# safe to share; override only if your stick differs.
|
||||
# ---------------------------------------------------------------------------
|
||||
GPON_HOST=192.168.100.1
|
||||
GPON_PORT=23
|
||||
GPON_USER=root
|
||||
GPON_PASS=admin
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Clone target — YOUR ISP-issued ONT's identity. Read these off the ONT's own
|
||||
# web UI before you unplug it: Maintenance -> Log, first few lines, has all of
|
||||
# them in one place (MAC is on the box label). See README step 2.
|
||||
# ---------------------------------------------------------------------------
|
||||
# GPON vendor ID: first 4 chars of the GPON serial (e.g. ALCL for Nokia/Alcatel)
|
||||
ONT_VENDOR=XXXX
|
||||
# Full 12-char GPON serial number, e.g. XXXXnnnnnnnn
|
||||
ONT_SERIAL=XXXXXXXXXXXX
|
||||
# Equipment ID = the ONT model string (OMCI ME257)
|
||||
ONT_EQID=X-0000X-X
|
||||
# Hardware version (OMCI ME256); often printed as "ONT P/N" on the box label
|
||||
ONT_HWVER=0XX00000XXXX
|
||||
# Software version (OMCI ME7) — the 14-char prefix of the UI's "Software Version"
|
||||
ONT_SWVER=0XX00000XXXX00
|
||||
# The ONT's own MAC. Only matters if the stick runs in router mode; cloned for
|
||||
# faithfulness. In bridge mode your router's MAC is what the ISP's DHCP sees.
|
||||
ONT_MAC=00:00:00:00:00:00
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The stick's OWN factory values — record these BEFORE overwriting them, they
|
||||
# are your revert path (README step 9). Device-unique.
|
||||
# ---------------------------------------------------------------------------
|
||||
STICK_VENDOR=DLNK
|
||||
STICK_SERIAL=DLNKxxxxxxxx
|
||||
STICK_EQID=DPN-100
|
||||
STICK_HWVER=1.0.0
|
||||
STICK_MAC=00:00:00:00:00:00
|
||||
# Factory PLOAM password, from `misc pon_passwd get` — usually not needed
|
||||
STICK_PLOAM=xxxxxxxxxx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Your network (used by README's router-side steps)
|
||||
# ---------------------------------------------------------------------------
|
||||
# How you reach the MikroTik itself
|
||||
ROUTER_ADDR=192.0.2.1
|
||||
# The single host allowed to reach the stick's telnet management
|
||||
ADMIN_HOST=192.0.2.2
|
||||
# Address the router puts on sfp1 to talk to the stick (same /24 as GPON_HOST)
|
||||
STICK_MGMT_ADDR=192.168.100.2/24
|
||||
# The ISP's WAN VLAN on the Ethernet side of the stick — VERIFY THIS YOURSELF
|
||||
WAN_VLAN=4
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Back up one MTD partition of the GPON stick over telnet, md5-verified.
|
||||
Usage: gpon_backup.py <mtd_index> <outfile> [idle_timeout] [hard_cap]
|
||||
Streams `cat /dev/mtdN | uuencode` over the telnet session and uudecodes locally
|
||||
(Python's uu module was removed in 3.13, so we decode by hand)."""
|
||||
import socket, sys, time, re, hashlib
|
||||
from gpon_env import HOST, PORT, USER, PASS
|
||||
|
||||
idx = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
idle_to = float(sys.argv[3]) if len(sys.argv)>3 else 12.0
|
||||
hard_cap = float(sys.argv[4]) if len(sys.argv)>4 else 300.0
|
||||
|
||||
def strip_iac(data, outbuf):
|
||||
res=bytearray(); i=0
|
||||
while i<len(data):
|
||||
b=data[i]
|
||||
if b==0xFF and i+1<len(data):
|
||||
c=data[i+1]
|
||||
if c in (251,252,253,254) and i+2<len(data):
|
||||
o=data[i+2]
|
||||
if c==253: outbuf.append(bytes([255,252,o]))
|
||||
elif c==251: outbuf.append(bytes([255,254,o]))
|
||||
i+=3; continue
|
||||
elif c==250:
|
||||
j=data.find(b'\xff\xf0', i+2); i=len(data) if j<0 else j+2; continue
|
||||
else: i+=2; continue
|
||||
res.append(b); i+=1
|
||||
return bytes(res)
|
||||
|
||||
s=socket.create_connection((HOST,PORT),timeout=8)
|
||||
def rx(idle, cap, stop=None):
|
||||
s.settimeout(0.5); buf=bytearray(); last=time.time(); start=time.time()
|
||||
while True:
|
||||
try:
|
||||
d=s.recv(16384)
|
||||
if not d: break
|
||||
ob=[]; buf.extend(strip_iac(d,ob))
|
||||
for o in ob: s.sendall(o)
|
||||
last=time.time()
|
||||
if stop and stop(buf): break
|
||||
except socket.timeout:
|
||||
if time.time()-last>idle: break
|
||||
if time.time()-start>cap: break
|
||||
return buf.decode('latin-1','replace')
|
||||
|
||||
def send(x): s.sendall(x.encode()+b'\r\n')
|
||||
|
||||
out=rx(2,8)
|
||||
if re.search(r'(ogin|sername)',out): send(USER); out+=rx(2,6)
|
||||
if re.search(r'assword',out,re.I): send(PASS); out+=rx(2,6)
|
||||
send('system'); rx(1.5,5)
|
||||
send('shell'); rx(1.5,5)
|
||||
|
||||
send('md5sum /dev/mtd%s'%idx)
|
||||
m=rx(90,150, stop=lambda b: re.search(rb'[0-9a-f]{32}\s+/dev/mtd',b))
|
||||
mm=re.search(r'([0-9a-f]{32})\s+/dev/mtd%s'%re.escape(idx), m)
|
||||
expected=mm.group(1) if mm else None
|
||||
|
||||
t0=time.time()
|
||||
send('cat /dev/mtd%s | uuencode p'%idx)
|
||||
def done(b):
|
||||
bi=b.find(b'begin ')
|
||||
return bi>=0 and (b'\nend\r' in b[bi:] or b'\nend\n' in b[bi:])
|
||||
d=rx(idle_to, hard_cap, stop=done)
|
||||
dt=time.time()-t0
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
|
||||
def uudecode(text):
|
||||
out=bytearray(); started=False
|
||||
for ln in text.split('\n'):
|
||||
ln=ln.rstrip('\r')
|
||||
if not started:
|
||||
if ln.startswith('begin '): started=True
|
||||
continue
|
||||
if ln=='end': break
|
||||
if ln=='' or ln=='`': continue
|
||||
n=(ord(ln[0])-0x20)&0x3f
|
||||
if n<=0: continue
|
||||
data=ln[1:]; dec=bytearray()
|
||||
for i in range(0,len(data),4):
|
||||
g=data[i:i+4]; v=[(ord(c)-0x20)&0x3f for c in g]
|
||||
while len(v)<4: v.append(0)
|
||||
dec.append(((v[0]<<2)|(v[1]>>4))&0xff)
|
||||
dec.append(((v[1]<<4)|(v[2]>>2))&0xff)
|
||||
dec.append(((v[2]<<6)|v[3])&0xff)
|
||||
out.extend(dec[:n])
|
||||
return bytes(out)
|
||||
|
||||
raw=uudecode(d)
|
||||
with open(outfile,'wb') as f: f.write(raw)
|
||||
actual=hashlib.md5(raw).hexdigest()
|
||||
print("mtd%s -> %s : %d bytes in %.1fs"%(idx,outfile,len(raw),dt))
|
||||
print(" expected md5 (stick): %s"%expected)
|
||||
print(" actual md5 (local): %s"%actual)
|
||||
print(" VERIFY: %s"%("OK" if (expected and actual==expected) else "*** MISMATCH / INCOMPLETE ***"))
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drive the stick's #ONT OMCI CLI (NOT the busybox shell). Stays at #ONT> and navigates menus.
|
||||
Usage: gpon_cli.py [--idle S] [--cap S] 'cmd1' 'cmd2' ...
|
||||
Sends each cmd as a CLI line (e.g. 'system', 'misc', '?', 'eqsn get')."""
|
||||
import socket, sys, time, re
|
||||
from gpon_env import HOST, PORT, USER, PASS
|
||||
args=sys.argv[1:]; idle=8.0; cap=60.0; cmds=[]
|
||||
while args:
|
||||
a=args.pop(0)
|
||||
if a=='--idle': idle=float(args.pop(0))
|
||||
elif a=='--cap': cap=float(args.pop(0))
|
||||
else: cmds.append(a)
|
||||
|
||||
def strip_iac(data,outbuf):
|
||||
res=bytearray(); i=0
|
||||
while i<len(data):
|
||||
b=data[i]
|
||||
if b==0xFF and i+1<len(data):
|
||||
c=data[i+1]
|
||||
if c in (251,252,253,254) and i+2<len(data):
|
||||
o=data[i+2]
|
||||
if c==253: outbuf.append(bytes([255,252,o]))
|
||||
elif c==251: outbuf.append(bytes([255,254,o]))
|
||||
i+=3; continue
|
||||
elif c==250:
|
||||
j=data.find(b'\xff\xf0',i+2); i=len(data) if j<0 else j+2; continue
|
||||
else: i+=2; continue
|
||||
res.append(b); i+=1
|
||||
return bytes(res)
|
||||
|
||||
s=socket.create_connection((HOST,PORT),timeout=8)
|
||||
def rx(idle,cap,stop=None):
|
||||
s.settimeout(0.5); buf=bytearray(); last=time.time(); start=time.time()
|
||||
while True:
|
||||
try:
|
||||
d=s.recv(16384)
|
||||
if not d: break
|
||||
ob=[]; buf.extend(strip_iac(d,ob))
|
||||
for o in ob: s.sendall(o)
|
||||
last=time.time()
|
||||
if stop and stop(buf): break
|
||||
except socket.timeout:
|
||||
if time.time()-last>idle: break
|
||||
if time.time()-start>cap: break
|
||||
return buf.decode('latin-1','replace')
|
||||
def send(x): s.sendall(x.encode()+b'\r\n')
|
||||
|
||||
o=rx(3,8)
|
||||
if re.search(r'(ogin|sername)',o): send(USER); o+=rx(2,6)
|
||||
if re.search(r'assword',o,re.I): send(PASS); o+=rx(3,8)
|
||||
# now sitting at #ONT> (do NOT enter shell)
|
||||
promptend=lambda b: b.rstrip().endswith(b'>')
|
||||
for c in cmds:
|
||||
send(c)
|
||||
out=rx(idle,cap,stop=promptend)
|
||||
print("\n>>> %s\n%s"%(c,out))
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run commands in the stick's root shell over telnet (idle-tolerant, waits for prompt).
|
||||
Usage: gpon_cmd.py [--idle S] [--cap S] 'cmd1' 'cmd2' ..."""
|
||||
import socket, sys, time, re
|
||||
from gpon_env import HOST, PORT, USER, PASS
|
||||
args=sys.argv[1:]; idle=8.0; cap=120.0; cmds=[]
|
||||
while args:
|
||||
a=args.pop(0)
|
||||
if a=='--idle': idle=float(args.pop(0))
|
||||
elif a=='--cap': cap=float(args.pop(0))
|
||||
else: cmds.append(a)
|
||||
|
||||
def strip_iac(data,outbuf):
|
||||
res=bytearray(); i=0
|
||||
while i<len(data):
|
||||
b=data[i]
|
||||
if b==0xFF and i+1<len(data):
|
||||
c=data[i+1]
|
||||
if c in (251,252,253,254) and i+2<len(data):
|
||||
o=data[i+2]
|
||||
if c==253: outbuf.append(bytes([255,252,o]))
|
||||
elif c==251: outbuf.append(bytes([255,254,o]))
|
||||
i+=3; continue
|
||||
elif c==250:
|
||||
j=data.find(b'\xff\xf0',i+2); i=len(data) if j<0 else j+2; continue
|
||||
else: i+=2; continue
|
||||
res.append(b); i+=1
|
||||
return bytes(res)
|
||||
|
||||
s=socket.create_connection((HOST,PORT),timeout=8)
|
||||
def rx(idle,cap,stop=None):
|
||||
s.settimeout(0.5); buf=bytearray(); last=time.time(); start=time.time()
|
||||
while True:
|
||||
try:
|
||||
d=s.recv(16384)
|
||||
if not d: break
|
||||
ob=[]; buf.extend(strip_iac(d,ob))
|
||||
for o in ob: s.sendall(o)
|
||||
last=time.time()
|
||||
if stop and stop(buf): break
|
||||
except socket.timeout:
|
||||
if time.time()-last>idle: break
|
||||
if time.time()-start>cap: break
|
||||
return buf.decode('latin-1','replace')
|
||||
def send(x): s.sendall(x.encode()+b'\r\n')
|
||||
|
||||
o=rx(2,8)
|
||||
if re.search(r'(ogin|sername)',o): send(USER); o+=rx(2,6)
|
||||
if re.search(r'assword',o,re.I): send(PASS); o+=rx(2,6)
|
||||
send('system'); rx(1.5,5)
|
||||
send('shell'); rx(1.5,5)
|
||||
promptend=lambda b: b.rstrip().endswith(b'shell>')
|
||||
for c in cmds:
|
||||
send(c)
|
||||
out=rx(idle,cap,stop=promptend)
|
||||
print("\n$ %s\n%s"%(c,out))
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared connection settings for the telnet drivers in this directory.
|
||||
|
||||
Resolved highest-priority-first from: the process environment, `../.env`,
|
||||
`../example.env`, then built-in defaults. Keeps addresses and credentials out
|
||||
of the scripts (and out of git) — see ../example.env.
|
||||
"""
|
||||
import os
|
||||
|
||||
KEYS = ('GPON_HOST', 'GPON_PORT', 'GPON_USER', 'GPON_PASS')
|
||||
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _parse(path):
|
||||
vals = {}
|
||||
try:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
vals[k.strip()] = v.strip().strip('"').strip("'")
|
||||
except OSError:
|
||||
pass
|
||||
return vals
|
||||
|
||||
|
||||
_cfg = _parse(os.path.join(_ROOT, 'example.env'))
|
||||
_cfg.update(_parse(os.path.join(_ROOT, '.env')))
|
||||
_cfg.update({k: v for k, v in os.environ.items() if k in KEYS})
|
||||
|
||||
HOST = _cfg.get('GPON_HOST') or '192.168.100.1'
|
||||
PORT = int(_cfg.get('GPON_PORT') or 23)
|
||||
USER = _cfg.get('GPON_USER') or 'root'
|
||||
PASS = _cfg.get('GPON_PASS') or 'admin'
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal scriptable telnet client for probing the GPON stick CLI.
|
||||
Usage: gpon_telnet.py [--host H] [--port P] [--user U] [--pass P] [--wait S] [cmd ...]
|
||||
Host/port/user/pass default to the .env settings (see ../example.env).
|
||||
Each positional cmd is sent as one line after login; output captured per cmd.
|
||||
Reads extra commands (one per line) from stdin if --stdin given.
|
||||
"""
|
||||
import socket, sys, time, re
|
||||
import gpon_env
|
||||
|
||||
args = sys.argv[1:]
|
||||
HOST, PORT = gpon_env.HOST, gpon_env.PORT
|
||||
USER, PASS, WAIT, USE_STDIN = gpon_env.USER, gpon_env.PASS, 2.5, False
|
||||
cmds = []
|
||||
while args:
|
||||
a = args.pop(0)
|
||||
if a == '--host': HOST = args.pop(0)
|
||||
elif a == '--port': PORT = int(args.pop(0))
|
||||
elif a == '--user': USER = args.pop(0)
|
||||
elif a == '--pass': PASS = args.pop(0)
|
||||
elif a == '--wait': WAIT = float(args.pop(0))
|
||||
elif a == '--stdin': USE_STDIN = True
|
||||
else: cmds.append(a)
|
||||
if USE_STDIN:
|
||||
cmds += [l.rstrip('\n') for l in sys.stdin if l.strip() != '']
|
||||
|
||||
def strip_iac(data, outbuf):
|
||||
res = bytearray(); i = 0
|
||||
while i < len(data):
|
||||
b = data[i]
|
||||
if b == 0xFF and i+1 < len(data):
|
||||
cmd = data[i+1]
|
||||
if cmd in (251,252,253,254) and i+2 < len(data):
|
||||
opt = data[i+2]
|
||||
if cmd == 253: outbuf.append(bytes([255,252,opt])) # DO->WONT
|
||||
elif cmd == 251: outbuf.append(bytes([255,254,opt])) # WILL->DONT
|
||||
i += 3; continue
|
||||
elif cmd == 250: # SB..SE
|
||||
j = data.find(b'\xff\xf0', i+2)
|
||||
i = len(data) if j == -1 else j+2; continue
|
||||
else:
|
||||
i += 2; continue
|
||||
res.append(b); i += 1
|
||||
return bytes(res)
|
||||
|
||||
s = socket.create_connection((HOST, PORT), timeout=8)
|
||||
|
||||
def rx(timeout):
|
||||
s.settimeout(0.6); buf = bytearray(); end = time.time()+timeout
|
||||
while time.time() < end:
|
||||
try:
|
||||
data = s.recv(4096)
|
||||
if not data: break
|
||||
ob = []
|
||||
buf.extend(strip_iac(data, ob))
|
||||
for o in ob: s.sendall(o)
|
||||
except socket.timeout:
|
||||
if buf: break
|
||||
return buf.decode('latin-1','replace')
|
||||
|
||||
out = rx(3.0)
|
||||
if re.search(r'(ogin|sername)', out):
|
||||
s.sendall(USER.encode()+b'\r\n'); out += rx(2.0)
|
||||
if re.search(r'assword', out, re.I):
|
||||
s.sendall(PASS.encode()+b'\r\n'); out += rx(3.0)
|
||||
print("===== BANNER/LOGIN =====\n"+out)
|
||||
for c in cmds:
|
||||
s.sendall(c.encode()+b'\r\n'); o = rx(WAIT)
|
||||
print("\n===== CMD: %r =====\n%s" % (c, o))
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wait for the stick's telnet to cycle after a reboot, then run #ONT CLI reads to verify.
|
||||
Usage: gpon_verify.py [--wait S] [--idle S] [--cap S] 'cmd1' 'cmd2' ..."""
|
||||
import socket, sys, time, re
|
||||
from gpon_env import HOST, PORT, USER, PASS
|
||||
args=sys.argv[1:]; idle=5.0; cap=25.0; wait=140.0; cmds=[]
|
||||
while args:
|
||||
a=args.pop(0)
|
||||
if a=='--idle': idle=float(args.pop(0))
|
||||
elif a=='--cap': cap=float(args.pop(0))
|
||||
elif a=='--wait': wait=float(args.pop(0))
|
||||
else: cmds.append(a)
|
||||
|
||||
def strip_iac(data,outbuf):
|
||||
res=bytearray(); i=0
|
||||
while i<len(data):
|
||||
b=data[i]
|
||||
if b==0xFF and i+1<len(data):
|
||||
c=data[i+1]
|
||||
if c in (251,252,253,254) and i+2<len(data):
|
||||
o=data[i+2]
|
||||
if c==253: outbuf.append(bytes([255,252,o]))
|
||||
elif c==251: outbuf.append(bytes([255,254,o]))
|
||||
i+=3; continue
|
||||
elif c==250:
|
||||
j=data.find(b'\xff\xf0',i+2); i=len(data) if j<0 else j+2; continue
|
||||
else: i+=2; continue
|
||||
res.append(b); i+=1
|
||||
return bytes(res)
|
||||
|
||||
def port_open():
|
||||
try:
|
||||
c=socket.create_connection((HOST,PORT),timeout=3); c.close(); return True
|
||||
except (OSError,) : return False
|
||||
|
||||
t0=time.time()
|
||||
# 1) give the reboot a moment to take the stick down
|
||||
time.sleep(5)
|
||||
# 2) wait for it to go DOWN (confirms the reboot began)
|
||||
went_down=False
|
||||
while time.time()-t0 < 30:
|
||||
if not port_open(): went_down=True; break
|
||||
time.sleep(2)
|
||||
print("stick went DOWN: %s (t=%ds)"%(went_down, time.time()-t0))
|
||||
# 3) wait for it to come back UP
|
||||
came_up=False
|
||||
while time.time()-t0 < wait:
|
||||
if port_open(): came_up=True; break
|
||||
time.sleep(3)
|
||||
if not came_up:
|
||||
print("STICK DID NOT COME BACK within %ds -- check MikroTik sfp1 / re-seat"%wait); sys.exit(1)
|
||||
print("stick back UP (t=%ds); giving telnetd a few s to settle"%(time.time()-t0))
|
||||
time.sleep(4)
|
||||
|
||||
s=socket.create_connection((HOST,PORT),timeout=8)
|
||||
def rx(idle,cap,stop=None):
|
||||
s.settimeout(0.5); buf=bytearray(); last=time.time(); start=time.time()
|
||||
while True:
|
||||
try:
|
||||
d=s.recv(16384)
|
||||
if not d: break
|
||||
ob=[]; buf.extend(strip_iac(d,ob))
|
||||
for o in ob: s.sendall(o)
|
||||
last=time.time()
|
||||
if stop and stop(buf): break
|
||||
except socket.timeout:
|
||||
if time.time()-last>idle: break
|
||||
if time.time()-start>cap: break
|
||||
return buf.decode('latin-1','replace')
|
||||
def send(x): s.sendall(x.encode()+b'\r\n')
|
||||
|
||||
o=rx(3,10)
|
||||
if re.search(r'(ogin|sername)',o): send(USER); o+=rx(2,6)
|
||||
if re.search(r'assword',o,re.I): send(PASS); o+=rx(3,8)
|
||||
promptend=lambda b: b.rstrip().endswith(b'>')
|
||||
for c in cmds:
|
||||
send(c); out=rx(idle,cap,stop=promptend)
|
||||
print("\n>>> %s\n%s"%(c,out))
|
||||
try: s.close()
|
||||
except Exception: pass
|
||||
Reference in New Issue
Block a user