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) — seeexample.env.
Contents
- Before you start
- Read your ONT's identity
- Bench setup — talk to the stick
- Configure
.env - Write the clone values
- Reboot, verify, connect the fibre
- Router side — WAN VLAN, DHCP, firewall
- Lock down the stick's management
- Revert to factory
- Troubleshooting
- Tools reference
- 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.pydumps 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.
mac1is 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'ssfp1.<vlan>MAC — which you never change, and it still works. Cloningmac1is 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 → .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:
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
eqsnmay 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/mib→resetas 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
inputrule matching an interface list is valid immediately, so you can lock it down before any address exists. - A
forwardrule within-interface=Xis flaggedI/INVALID whileXhas 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:
- Masquerade: add the VLAN interface to whatever your
srcnatrules key on, or addchain=srcnat out-interface=sfp1.<WAN_VLAN> action=masquerade. - Default route:
/ip dhcp-client set [find comment=gpon-wan-v4] add-default-route=yes— either as primary, or with a higherdefault-route-distancefor failover behind your existing WAN. - Re-add, deliberately, any service accepts and
dst-natport-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,addthe replacement withplace-before=$id, thenremove $id.- Over SSH, keep a
:localand 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 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) |
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).SWVERsets 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_SWVERmay 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.