Files
2026-07-24 20:26:37 +03:00

24 KiB

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.


Contents

  1. Before you start
  2. Read your ONT's identity
  3. Bench setup — talk to the stick
  4. Configure .env
  5. Write the clone values
  6. Reboot, verify, connect the fibre
  7. Router side — WAN VLAN, DHCP, firewall
  8. Lock down the stick's management
  9. Revert to factory
  10. Troubleshooting
  11. Tools reference
  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:

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

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 → .envexample.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:

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

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:

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:

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/mibreset 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:

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.


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:

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 showtemptemp = 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 systemshell 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)
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.